diff --git a/.github/workflows/flake-check.yaml b/.github/workflows/flake-check.yaml
index 9eabbe3..a28f80a 100644
--- a/.github/workflows/flake-check.yaml
+++ b/.github/workflows/flake-check.yaml
@@ -42,6 +42,8 @@ jobs:
runs-on: ubuntu-latest
env:
NIX_PATH: "nixpkgs=https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
+ # write-tack resolves refs via api.github.com; anonymous rate limits flake on shared runners
+ GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: wimpysworld/nothing-but-nix@v10
- uses: cachix/install-nix-action@v31
@@ -58,6 +60,7 @@ jobs:
exclude:
- template: minimal
- template: flakeless-parts
+ - template: tack
steps:
- uses: wimpysworld/nothing-but-nix@v10
- uses: cachix/install-nix-action@v31
@@ -127,23 +130,34 @@ jobs:
cd templates/unflake
sed -i 's/# flake-file = import/flake-file = import/' default.nix
nix-shell . -A flake-file.sh --run 'write-unflake --verbose'
- nixlock:
+ tack:
needs: [nix-fmt]
- name: Check nixlock
+ name: Check tack
runs-on: ubuntu-latest
- if: ${{ contains(github.event.pull_request.labels.*.name, 'nixlock') }}
+ if: ${{ contains(github.event.pull_request.labels.*.name, 'tack') }}
env:
- NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz"
+ GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@v10
- uses: cachix/install-nix-action@v31
- uses: DeterminateSystems/magic-nix-cache-action@main
+ # The tack template is lock-less (its flake.nix declares no inputs), so
+ # the generic template job cannot --override-input it. Instead the
+ # template's recomposable tackOverrides arg injects this checkout's
+ # flake-file, exercising the override path at the same time.
- run: |
set -e -o pipefail
- cd templates/nixlock
- sed -i 's/# flake-file = import/flake-file = import/' default.nix
- nix-shell . -A flake-file.sh --run write-nixlock
+ cd templates/tack
+ local_flake_file='tackOverrides.flake-file = import ./../../modules;'
+ nix build -L --impure --expr "
+ ((import ./.) { $local_flake_file }).packages.\${builtins.currentSystem}.write-tack
+ "
+ ./result/bin/write-tack
+ git diff --exit-code .tack/pins.toml
+ nix build -L --impure --expr "
+ ((import ./.) { $local_flake_file }).devShells.\${builtins.currentSystem}.default
+ "
dev:
needs: [bootstrap-minimal, bootstrap-tests, nix-fmt]
name: Check flake dev
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e71bcca
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/bootstrap-tests/
diff --git a/README.md b/README.md
index a2a04a7..4b1103b 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ This means
- Your inputs follow a **typed Input Schema**.
- Your outputs can be defined on a **typed Output Schema** [example](templates/minimal/flake-file.nix).
-> Despite the original flake-oriented name, it NOW also works on _stable Nix_, non-flakes environments via [npins](templates/npins), [unflake](templates/unflake), [nixlock](https://codeberg.org/FrdrCkII/nixlock)
+> Despite the original flake-oriented name, it NOW also works on _stable Nix_, non-flakes environments via [npins](templates/npins), [unflake](templates/unflake)
@@ -39,7 +39,7 @@ This means
- Incrementally add [flake-parts-builder](#parts_templates) templates.
- Pick flakeModules for different feature sets.
- [Dendritic](https://vic.github.io/dendrix/Dendritic.html) flake template.
-- Works on stable Nix, [npins](templates/npins), [unflake](templates/unflake), [nixlock](templates/nixlock) environments.
+- Works on stable Nix, [npins](templates/npins), [unflake](templates/unflake) environments.
diff --git a/dev/_bootstrap-tests.nix b/dev/_bootstrap-tests.nix
index e017d82..d535220 100644
--- a/dev/_bootstrap-tests.nix
+++ b/dev/_bootstrap-tests.nix
@@ -172,43 +172,6 @@ let
'';
};
- test-nixlock-update = pkgs.writeShellApplication {
- name = "test-nixlock-update";
- runtimeInputs = [
- (empty.flake-file.apps.write-nixlock pkgs)
- ];
- text = ''
- write-nixlock lock
- grep empty ${outdir}/nixlock.lock.nix
- write-nixlock update
- grep empty ${outdir}/nixlock.lock.nix
- '';
- };
-
- test-nixlock-update-one = pkgs.writeShellApplication {
- name = "test-nixlock-update-one";
- runtimeInputs = [
- (empty.flake-file.apps.write-nixlock pkgs)
- ];
- text = ''
- write-nixlock lock
- write-nixlock update empty
- grep empty ${outdir}/nixlock.lock.nix
- if write-nixlock update nonexistent 2>/dev/null; then exit 1; fi
- '';
- };
-
- test-nixlock = pkgs.writeShellApplication {
- name = "test-nixlock";
- runtimeInputs = [
- (empty.flake-file.apps.write-nixlock pkgs)
- ];
- text = ''
- write-nixlock
- grep empty ${outdir}/nixlock.lock.nix
- '';
- };
-
test-write-lock-flake = pkgs.writeShellApplication {
name = "test-write-lock-flake";
runtimeInputs = [
@@ -230,21 +193,38 @@ let
];
text = ''
mkdir -p ${outdir}/npins
- echo '{"pins":{},"version":7}' > ${outdir}/npins/sources.json
+ echo '{"pins":{},"version":8}' > ${outdir}/npins/sources.json
write-lock
jq -e '.pins | has("empty")' ${outdir}/npins/sources.json
'';
};
- test-write-lock-nixlock = pkgs.writeShellApplication {
- name = "test-write-lock-nixlock";
+ test-tack = pkgs.writeShellApplication {
+ name = "test-tack";
+ runtimeInputs = [
+ (empty.flake-file.apps.write-tack pkgs)
+ pkgs.jq
+ ];
+ text = ''
+ write-tack
+ cat ${outdir}/.tack/pins.toml
+ grep github:vic/empty-flake ${outdir}/.tack/pins.toml
+ jq -e 'has("empty")' ${outdir}/.tack/pins.lock.json
+ [ -e ${outdir}/.tack/default.nix ]
+ '';
+ };
+
+ test-write-lock-tack = pkgs.writeShellApplication {
+ name = "test-write-lock-tack";
runtimeInputs = [
(empty.flake-file.apps.write-lock pkgs)
+ pkgs.jq
];
text = ''
- echo '{ }' > ${outdir}/nixlock.lock.nix
+ mkdir -p ${outdir}/.tack
+ echo '{ }' > ${outdir}/.tack/pins.lock.json
write-lock
- grep empty ${outdir}/nixlock.lock.nix
+ jq -e 'has("empty")' ${outdir}/.tack/pins.lock.json
'';
};
@@ -260,28 +240,6 @@ let
'';
};
- test-nixlock-schemes = pkgs.writeShellApplication {
- name = "test-nixlock-schemes";
- runtimeInputs = [
- (all-inputs-schemes.flake-file.apps.write-nixlock pkgs)
- ];
- text = ''
- write-nixlock
- cat ${outdir}/nixlock.lock.nix
- grep '"simple"' ${outdir}/nixlock.lock.nix
- grep '"withBranch"' ${outdir}/nixlock.lock.nix
- grep '"noflake"' ${outdir}/nixlock.lock.nix
- grep '"gitHttps"' ${outdir}/nixlock.lock.nix
- grep '"tarball"' ${outdir}/nixlock.lock.nix
- grep '"tarballPlus"' ${outdir}/nixlock.lock.nix
- grep '"fileHttps"' ${outdir}/nixlock.lock.nix
- grep '"attrGh"' ${outdir}/nixlock.lock.nix
- grep '"attrGhRef"' ${outdir}/nixlock.lock.nix
- if grep '"followsSimple"' ${outdir}/nixlock.lock.nix; then exit 1; fi
- grep vic/empty-flake ${outdir}/nixlock.lock.nix
- '';
- };
-
in
pkgs.mkShell {
buildInputs = [
@@ -293,13 +251,10 @@ pkgs.mkShell {
test-npins-skip
test-npins-follows
test-npins-transitive
- test-nixlock
- test-nixlock-update
- test-nixlock-update-one
- test-nixlock-schemes
+ test-tack
test-write-lock-flake
test-write-lock-npins
- test-write-lock-nixlock
+ test-write-lock-tack
test-write-lock-unflake
];
}
diff --git a/dev/flake.lock b/dev/flake.lock
index 9b35fdf..d3c1892 100644
--- a/dev/flake.lock
+++ b/dev/flake.lock
@@ -38,11 +38,11 @@
]
},
"locked": {
- "lastModified": 1769996383,
- "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
+ "lastModified": 1778716662,
+ "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
- "rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
+ "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -53,11 +53,11 @@
},
"import-tree": {
"locked": {
- "lastModified": 1771045967,
- "narHash": "sha256-oYO4poyw0Sb/db2PigqugMlDwsvwLg6CSpFrMUWxA3Q=",
+ "lastModified": 1778781969,
+ "narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=",
"owner": "vic",
"repo": "import-tree",
- "rev": "c968d3b54d12cf5d9c13f16f7c545a06c9d1fde6",
+ "rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274",
"type": "github"
},
"original": {
@@ -109,9 +109,6 @@
},
"nix-unit": {
"inputs": {
- "flake-parts": [
- "flake-parts"
- ],
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
@@ -121,11 +118,11 @@
]
},
"locked": {
- "lastModified": 1762774186,
- "narHash": "sha256-hRADkHjNt41+JUHw2EiSkMaL4owL83g5ZppjYUdF/Dc=",
+ "lastModified": 1779338171,
+ "narHash": "sha256-affUbv/bwE8SLGhuWKniDr7SVO+Lo1XEPjCZdyU5kgQ=",
"owner": "nix-community",
"repo": "nix-unit",
- "rev": "1c9ab50554eed0b768f9e5b6f646d63c9673f0f7",
+ "rev": "6ab1f232562a01d18b40d5ed6a58718c4f3a74bc",
"type": "github"
},
"original": {
@@ -136,11 +133,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1771423170,
- "narHash": "sha256-V/p5M4cAMbu/MJBDn5YABy5QJgCFpsgrnXVVc2Uo5+k=",
- "rev": "bcc4a9d9533c033d806a46b37dc444f9b0da49dd",
+ "lastModified": 1781141223,
+ "narHash": "sha256-2VqJHoxNTbg68GJ4VbV2jRLol3VX1iB4IlCrsnsz1oc=",
+ "rev": "b503dde361500433ca25a32e8f4d218bf58fb659",
"type": "tarball",
- "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre948338.bcc4a9d9533c/nixexprs.tar.xz"
+ "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1014844.b503dde36150/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
@@ -166,11 +163,11 @@
]
},
"locked": {
- "lastModified": 1770228511,
- "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
+ "lastModified": 1780220602,
+ "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
- "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
+ "rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
diff --git a/dev/modules/formatter.nix b/dev/modules/formatter.nix
index 6f127d7..38da44c 100644
--- a/dev/modules/formatter.nix
+++ b/dev/modules/formatter.nix
@@ -36,7 +36,9 @@
"**/npins/default.nix" # generated by write-npins
"*.bash"
"docs/*"
- "**/nixlock.lock.nix" # generated by write-nixlock
+ "**/.tack/default.nix" # generated by: tack init --resolver
+ "**/.tack/pins.toml" # generated by write-tack
+ "**/.tack/pins.lock.json" # generated by: tack update
];
};
};
diff --git a/dev/modules/unit-tests/tackSerialize.nix b/dev/modules/unit-tests/tackSerialize.nix
new file mode 100644
index 0000000..6ac9bf6
--- /dev/null
+++ b/dev/modules/unit-tests/tackSerialize.nix
@@ -0,0 +1,135 @@
+{ lib, inputs, ... }:
+let
+ serialize = import "${inputs.flake-file}/modules/tack/serialize.nix" lib;
+
+ header = "# tack pins. generated by flake-file write-tack.\n# nix reads pins.lock.json; this file drives tack update";
+
+ tests.tackSerialize."test empty config renders header only" = {
+ expr = serialize { };
+ expected = header + "\n";
+ };
+
+ tests.tackSerialize."test recomposable table" = {
+ expr = serialize { recomposable = true; };
+ expected = header + "\n\n[tack]\nrecomposable = true\n";
+ };
+
+ tests.tackSerialize."test url passthrough" = {
+ expr = serialize { inputs.foo.url = "github:a/b"; };
+ expected = header + "\n\n[inputs.foo]\nurl = \"github:a/b\"\n";
+ };
+
+ tests.tackSerialize."test url synthesis from type owner repo ref" = {
+ expr = serialize {
+ inputs.foo = {
+ type = "github";
+ owner = "a";
+ repo = "b";
+ ref = "main";
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"github:a/b/main\"\n";
+ };
+
+ tests.tackSerialize."test missing url throws" = {
+ expr =
+ (builtins.tryEval (serialize {
+ inputs.foo = { };
+ })).success;
+ expected = false;
+ };
+
+ tests.tackSerialize."test flake false becomes fetch pin" = {
+ expr = serialize {
+ inputs.foo = {
+ url = "x";
+ flake = false;
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\"\ntype = \"fetch\"\n";
+ };
+
+ tests.tackSerialize."test tackType wins over flake false" = {
+ expr = serialize {
+ inputs.foo = {
+ url = "x";
+ flake = false;
+ tackType = "fixed";
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\"\ntype = \"fixed\"\n";
+ };
+
+ tests.tackSerialize."test fixed pin with unpack" = {
+ expr = serialize {
+ inputs.foo = {
+ url = "x";
+ tackType = "fixed";
+ unpack = "tarball";
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\"\ntype = \"fixed\"\nunpack = \"tarball\"\n";
+ };
+
+ tests.tackSerialize."test follows table" = {
+ expr = serialize {
+ inputs.foo = {
+ url = "x";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\"\nfollows = { nixpkgs = \"nixpkgs\" }\n";
+ };
+
+ tests.tackSerialize."test dir and submodules" = {
+ expr = serialize {
+ inputs.foo = {
+ url = "x";
+ dir = "sub";
+ submodules = true;
+ };
+ };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\"\ndir = \"sub\"\nsubmodules = true\n";
+ };
+
+ tests.tackSerialize."test quoted input name" = {
+ expr = serialize { inputs."dotted.name".url = "x"; };
+ expected = header + "\n\n[inputs.\"dotted.name\"]\nurl = \"x\"\n";
+ };
+
+ tests.tackSerialize."test url escaping" = {
+ expr = serialize { inputs.foo.url = "x\"y\\z"; };
+ expected = header + "\n\n[inputs.foo]\nurl = \"x\\\"y\\\\z\"\n";
+ };
+
+ tests.tackSerialize."test shorturls" = {
+ expr = serialize { shorturls.gh = "github:{path}"; };
+ expected = header + "\n\n[shorturls]\ngh = \"github:{path}\"\n";
+ };
+
+ tests.tackSerialize."test all_follow string and list" = {
+ expr = serialize {
+ allFollow = {
+ nixpkgs = [
+ "nixpkgs-stable"
+ "nixpkgs-unstable"
+ ];
+ systems = "systems";
+ };
+ };
+ expected =
+ header
+ + "\n\n[all_follow]\nnixpkgs = [\"nixpkgs-stable\", \"nixpkgs-unstable\"]\nsystems = \"systems\"\n";
+ };
+
+ tests.tackSerialize."test inputs sorted by name" = {
+ expr = serialize {
+ inputs.zeta.url = "z";
+ inputs.alpha.url = "a";
+ };
+ expected = header + "\n\n[inputs.alpha]\nurl = \"a\"\n\n[inputs.zeta]\nurl = \"z\"\n";
+ };
+in
+{
+ flake = { inherit tests; };
+}
diff --git a/docs/src/content/docs/explanation/what-is-flake-file.mdx b/docs/src/content/docs/explanation/what-is-flake-file.mdx
index 1c4f7da..7b0a3dc 100644
--- a/docs/src/content/docs/explanation/what-is-flake-file.mdx
+++ b/docs/src/content/docs/explanation/what-is-flake-file.mdx
@@ -36,7 +36,7 @@ move your outputs function into `outputs.nix` and use something like:
Even if some people use unstable flakes, others should not be forced out of stable Nix.
Each module defines inputs and flake-file can extract to whatever input-locking backend you need.
-Be it `flake.nix`, `unflake.nix`, `nixlock`, `npins`.
+Be it `flake.nix`, `unflake.nix`, `npins`, tack pins.
## Everybody `.follows`
@@ -66,7 +66,7 @@ Running `nix run .#write-flake` materialises all declared inputs into `flake.nix
- **Modular:** Each module declares only its own dependencies.
- **Composable:** Modules can be shared across projects — including their input declarations.
-- **Backend-agnostic:** The same module options generate `flake.nix`, `unflake.nix`, `nixlock` or `npins/` depending on the chosen backend.
+- **Backend-agnostic:** The same module options generate `flake.nix`, `unflake.nix`, `npins/` or tack pins depending on the chosen backend.
- **Standard Nix:** Uses the Nix module system — `lib.mkDefault`, priority overrides, conditional inputs — all work as expected.
## Real-world Usage
diff --git a/docs/src/content/docs/guides/flake-modules.mdx b/docs/src/content/docs/guides/flake-modules.mdx
index 8f39ecd..3aba24f 100644
--- a/docs/src/content/docs/guides/flake-modules.mdx
+++ b/docs/src/content/docs/guides/flake-modules.mdx
@@ -86,8 +86,8 @@ Defines `flake-file` options for [unflake](https://codeberg.org/goldstein/unflak
Source: [`modules/unflake`](https://github.com/vic/flake-file/tree/main/modules/unflake)
-## `flakeModules.nixlock`
+## `flakeModules.tack`
-Defines `flake-file` options for [nixlock](https://codeberg.org/FrdrCkII/nixlock)-based environments. Exposes `write-nixlock`.
+Defines `flake-file` options for [tack](https://github.com/manic-systems/tack)-based environments: flake-like TOML pins, lazily fetched, with no `flake.lock`. Exposes `write-tack`, which renders `flake-file.inputs` into `.tack/pins.toml` and runs `tack update` to produce `.tack/pins.lock.json`. Extends `flake-file.inputs.` with `tackType` (`flake`/`fetch`/`fixed`) and `unpack` (`tarball`/`file`) pin fields. Configure via `flake-file.tack.*` (package, shorturls, all-follow rules, recomposability, resolver overrides).
-Source: [`modules/nixlock`](https://github.com/vic/flake-file/tree/main/modules/nixlock)
+Source: [`modules/tack`](https://github.com/vic/flake-file/tree/main/modules/tack)
diff --git a/docs/src/content/docs/guides/templates.mdx b/docs/src/content/docs/guides/templates.mdx
index 84f4238..46582f3 100644
--- a/docs/src/content/docs/guides/templates.mdx
+++ b/docs/src/content/docs/guides/templates.mdx
@@ -60,12 +60,12 @@ For **non-flake** (stable Nix) environments. Uses [goldstein/unflake](https://co
nix flake init -t github:vic/flake-file#unflake
```
-## `nixlock`
+## `tack`
-For **non-flake** (stable Nix) environments. Uses [FrdrCkII/nixlock](https://codeberg.org/FrdrCkII/nixlock) to pin inputs.
+Uses [tack](https://github.com/manic-systems/tack) to pin inputs: flake-like TOML pins (`.tack/pins.toml`), lazily fetched and transformed, with no `flake.lock`. Supports recomposable overrides so a downstream tack project can override this project's inputs.
```shell
-nix flake init -t github:vic/flake-file#unflake
+nix flake init -t github:vic/flake-file#tack
```
## Tips
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index ba75448..53eb547 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -63,7 +63,7 @@ nix-shell https://github.com/vic/flake-file/archive/main.tar.gz -A flake-file.sh
Built-in support for automatic `flake.lock` flattening via `allfollow` or `nix-auto-follow`.
- Generate `flake.nix`, `unflake.nix`, `nixlock`, or `npins/` from the same module options. Switch backends without rewriting your modules.
+ Generate `flake.nix`, `unflake.nix`, `npins/`, or tack pins from the same module options. Switch backends without rewriting your modules.
Run `nix run .#write-flake` to regenerate `flake.nix` from your options. A flake check enforces that files stay in sync.
Also provides `nix-shell` commands for stable Nix world.
diff --git a/docs/src/content/docs/overview.mdx b/docs/src/content/docs/overview.mdx
index 01a049a..ae053c9 100644
--- a/docs/src/content/docs/overview.mdx
+++ b/docs/src/content/docs/overview.mdx
@@ -44,7 +44,7 @@ flake-file treats `flake.nix` as a generated artifact. You declare inputs and se
Learn More
- Adopt flake-file in a non-flake project using npins, nixlock or unflake.
+ Adopt flake-file in a non-flake project using npins, unflake or tack.
Learn More
@@ -53,11 +53,11 @@ flake-file treats `flake.nix` as a generated artifact. You declare inputs and se
- All built-in flakeModules: `default`, `dendritic`, `import-tree`, `npins`, `unflake`, `allfollow`, `nix-auto-follow`, and `flake-parts-builder`.
+ All built-in flakeModules: `default`, `dendritic`, `import-tree`, `npins`, `unflake`, `tack`, `allfollow`, `nix-auto-follow`, and `flake-parts-builder`.
Learn More
- Ready-made project templates: `default`, `dendritic`, `parts`, `npins`, and `unflake`.
+ Ready-made project templates: `default`, `dendritic`, `parts`, `npins`, `unflake`, and `tack`.
Learn More
diff --git a/docs/src/content/docs/reference/bootstrap.mdx b/docs/src/content/docs/reference/bootstrap.mdx
index df97d1a..d025aa3 100644
--- a/docs/src/content/docs/reference/bootstrap.mdx
+++ b/docs/src/content/docs/reference/bootstrap.mdx
@@ -5,7 +5,7 @@ description: All options for the flake-file bootstrap command.
import { Aside } from '@astrojs/starlight/components';
-The bootstrap command lets you generate `flake.nix`, `unflake.nix`, `nixlock.lock.nix`, or `npins/` from scratch — without being inside an existing flake.
+The bootstrap command lets you generate `flake.nix`, `unflake.nix`, `.tack/`, or `npins/` from scratch — without being inside an existing flake.
```shell
nix-shell https://github.com/vic/flake-file/archive/refs/heads/main.zip \
@@ -22,8 +22,8 @@ The `flake-file.sh` shell provides these commands:
| `write-flake` | Generate a `flake.nix` file |
| `write-inputs` | Generate an `inputs.nix` expression (for debugging) |
| `write-unflake` | Generate `unflake.nix` via [unflake](https://codeberg.org/goldstein/unflake) |
-| `write-nixlock` | Generate `nixlock.lock.nix` via [nixlock](https://codeberg.org/FrdrCkII/nixlock) |
| `write-npins` | Generate/update `npins/` directory via [npins](https://github.com/andir/npins) |
+| `write-tack` | Generate/update `.tack/` pins and lock via [tack](https://github.com/manic-systems/tack) |
## Arguments
diff --git a/docs/src/content/docs/tutorials/bootstrap.mdx b/docs/src/content/docs/tutorials/bootstrap.mdx
index 7e76d97..ef953ec 100644
--- a/docs/src/content/docs/tutorials/bootstrap.mdx
+++ b/docs/src/content/docs/tutorials/bootstrap.mdx
@@ -21,14 +21,14 @@ You can create a `flake.nix` from scratch — without running inside an existing
# Write a minimal flake-file.nix file (or copy a flake.nix of yours)
echo '{ inputs.flake-file.url = "github:vic/flake-file"; }' > flake-file.nix
-# Generate flake.nix or unflake.nix or npins or nixlock from flake-file.nix
+# Generate flake.nix or unflake.nix or npins from flake-file.nix
nix-shell https://github.com/vic/flake-file/archive/refs/heads/main.zip \
-A flake-file.sh --run write-flake --arg modules ./flake-file.nix
```
> See also: all [bootstrap command args](/reference/bootstrap)
-Replace `write-flake` with `write-inputs`, `write-unflake`, `write-nixlock`, or `write-npins` to target a different backend.
+Replace `write-flake` with `write-inputs`, `write-unflake`, `write-tack`, or `write-npins` to target a different backend.
## Using a modules directory
diff --git a/docs/src/content/docs/tutorials/migrate-no-flakes.mdx b/docs/src/content/docs/tutorials/migrate-no-flakes.mdx
index 14e8e5c..6c9aad1 100644
--- a/docs/src/content/docs/tutorials/migrate-no-flakes.mdx
+++ b/docs/src/content/docs/tutorials/migrate-no-flakes.mdx
@@ -12,7 +12,6 @@ of tools for that in the ecosystem.
flake-file has support
for [`unflake`](https://codeberg.org/goldstein/unflake)
-and [`nixlock`](https://codeberg.org/FrdrCkII/nixlock)
and [`npins`](https://github.com/andir/npins)
send PR for others.
diff --git a/modules/bootstrap/default.nix b/modules/bootstrap/default.nix
index c42b844..4d8698e 100644
--- a/modules/bootstrap/default.nix
+++ b/modules/bootstrap/default.nix
@@ -32,7 +32,7 @@ let
./../options
./../npins
./../unflake
- ./../nixlock
+ ./../tack
./../write-inputs.nix
./../write-flake.nix
./../write-lock.nix
diff --git a/modules/default.nix b/modules/default.nix
index e4ba56f..cb32a96 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -10,7 +10,7 @@ let
npins
flakeless-parts
unflake
- nixlock
+ tack
flake-options
;
};
@@ -46,9 +46,9 @@ let
./unflake
];
- nixlock.imports = [
+ tack.imports = [
base
- ./nixlock
+ ./tack
];
default.imports = [
@@ -99,9 +99,9 @@ let
path = ./../templates/unflake;
};
- templates.nixlock = {
- description = "nixlock template";
- path = ./../templates/nixlock;
+ templates.tack = {
+ description = "tack template";
+ path = ./../templates/tack;
};
templates.dendritic = {
diff --git a/modules/nixlock/default.nix b/modules/nixlock/default.nix
deleted file mode 100644
index 2d80c55..0000000
--- a/modules/nixlock/default.nix
+++ /dev/null
@@ -1,171 +0,0 @@
-{ lib, config, ... }:
-let
- inherit (config) flake-file;
- inherit (import ../lib.nix lib) inputsExpr;
-
- inputs = flake-file.preProcess (inputsExpr flake-file.inputs);
-
- nixlock-source = fetchTarball {
- url = flake-file.nixlock.url;
- sha256 = flake-file.nixlock.sha256;
- };
-
- nl = import "${nixlock-source}/${flake-file.nixlock.version}" flake-file.nixlock.customTypes;
- inherit (nl.nllib)
- lockFileStart
- lockFileEnd
- lockFileMerge
- getFetcher
- withInputs
- ;
- types = nl.types;
-
- inherit (import ./parse.nix lib) toNixlockInput;
-
- inputsFile = lib.filterAttrs (_: v: v != null) (lib.mapAttrs toNixlockInput inputs);
-
- lockFilePath = "${flake-file.intoPath}/${flake-file.nixlock.lockFileName}";
- existingLock =
- if lib.hasPrefix "/" lockFilePath && builtins.pathExists lockFilePath then
- import lockFilePath
- else
- { };
-
- sources = existingLock: withInputs fetchedInputs existingLock;
-
- cmdFor =
- name: v:
- if existingLock ? ${name} then
- lockFileMerge {
- inherit name;
- meta = v;
- lock = existingLock.${name}.lock;
- }
- else
- types.${v.type}.update name v;
-
- scriptWith = cmds: lockFileName: ''
- ${lockFileStart}
- ${lib.concatStringsSep "\n" cmds}
- ${lockFileEnd}
- echo $LOCKFILE | nixfmt > '${lockFileName}'
- '';
-
- lockScript =
- lockFileName:
- scriptWith (lib.mapAttrsToList (n: v: types.${v.type}.update n v) inputsFile) lockFileName;
-
- updateScript =
- lockFileName:
- scriptWith (lib.mapAttrsToList (
- n: v: if v.isFreeze or false then cmdFor n v else types.${v.type}.update n v
- ) inputsFile) lockFileName;
-
- updateOneScript =
- target: lockFileName:
- scriptWith (lib.mapAttrsToList (
- n: v: if n == target then types.${v.type}.update n v else cmdFor n v
- ) inputsFile) lockFileName;
-
- perInputCases =
- lockFileName:
- lib.concatStringsSep "\n" (
- lib.mapAttrsToList (name: _: ''
- ${name}) ${updateOneScript name lockFileName} ;;
- '') inputsFile
- );
-
- fetchedInputs = lib.mapAttrs (
- _name: entry:
- getFetcher {
- rootPath = flake-file.intoPath;
- inherit (entry) meta lock;
- type = "stable";
- }
- ) existingLock;
-
- write-nixlock =
- pkgs:
- let
- rootPath = flake-file.intoPath;
- lockFileName = flake-file.nixlock.lockFileName;
- in
- pkgs.writeShellApplication {
- name = "write-nixlock";
- meta.description = "Generate nixlock.lock.nix via nixlock.";
- excludeShellChecks = [
- "SC2016"
- "SC2086"
- "SC2089"
- "SC2090"
- ];
- runtimeInputs = with pkgs; [
- nix
- nixfmt
- git
- nix-prefetch-git
- curl
- coreutils
- gnugrep
- gnused
- gawk
- jq
- ];
- text = ''
- cd ${rootPath}
- case "''${1:-lock}" in
- lock)
- ${lockScript lockFileName}
- ;;
- update)
- case "''${2:-}" in
- "")
- ${updateScript lockFileName}
- ;;
- ${perInputCases lockFileName}
- *) echo "unknown input: ''${2}" >&2; exit 1 ;;
- esac
- ;;
- *) echo "usage: write-nixlock [lock|update []]" >&2; exit 1 ;;
- esac
- '';
- };
-in
-{
- config = {
- flake-file.apps = { inherit write-nixlock; };
- };
- options.flake-file.nixlock = {
- url = lib.mkOption {
- type = lib.types.str;
- description = "nixlock archive url";
- default = "https://codeberg.org/FrdrCkII/nixlock/archive/fc10414fc2a2db7ad5daeeaa9e92e0e139102f9f.tar.gz";
- };
- sha256 = lib.mkOption {
- type = lib.types.str;
- description = "nixlock archive sha256";
- default = "sha256:0zqxki955iizglm5qs7gp914p7vsv7wjdabx053nk9maba7zpdja";
- };
- version = lib.mkOption {
- type = lib.types.str;
- description = "nixlock version to load";
- default = "v3.1";
- };
- lockFileName = lib.mkOption {
- type = lib.types.str;
- description = "nixlock lockfile name";
- default = "nixlock.lock.nix";
- };
- sources = lib.mkOption {
- type = lib.types.functionTo lib.types.unspecified;
- readOnly = true;
- description = "Fetched inputs from the nixlock lockfile via withInputs, ready to use as flake inputs.";
- default = sources;
- };
- customTypes = lib.mkOption {
- type = lib.types.raw;
- description = "nixlock custom input types";
- default = { };
- };
- };
-}
diff --git a/modules/nixlock/parse.nix b/modules/nixlock/parse.nix
deleted file mode 100644
index ac26a16..0000000
--- a/modules/nixlock/parse.nix
+++ /dev/null
@@ -1,113 +0,0 @@
-lib:
-let
-
- parseRef =
- urlWithParams:
- let
- pairs = builtins.filter (lib.hasPrefix "ref=") (
- lib.splitString "&" (lib.last (lib.splitString "?" urlWithParams))
- );
- in
- if pairs != [ ] then lib.removePrefix "ref=" (builtins.head pairs) else null;
-
- baseUrl = urlWithParams: builtins.head (lib.splitString "?" urlWithParams);
-
- parseGitHost =
- base: path:
- let
- parts = lib.splitString "/" path;
- owner = builtins.elemAt parts 0;
- repo = builtins.elemAt parts 1;
- ref = if builtins.length parts > 2 then builtins.elemAt parts 2 else null;
- in
- {
- type = "gitArchive";
- url = "${base}/${owner}/${repo}";
- inherit ref;
- };
-
- parseGithub = parseGitHost "https://github.com";
- parseGitlab = parseGitHost "https://gitlab.com";
- parseSourcehut = parseGitHost "https://git.sr.ht";
-
- parseGitUrl = urlWithParams: {
- type = "git";
- url = baseUrl urlWithParams;
- ref = parseRef urlWithParams;
- };
-
- attrsetBases = input: {
- github = "https://github.com";
- gitlab = "https://${input.host or "gitlab.com"}";
- sourcehut = "https://${input.host or "git.sr.ht"}";
- };
-
- attrsetToNixlock =
- input:
- let
- mgithost = (attrsetBases input).${input.type} or null;
- in
- if mgithost != null then
- {
- type = "gitArchive";
- url = "${mgithost}/${input.owner}/${input.repo}";
- ref = input.ref or null;
- }
- else if input.type == "git" then
- {
- type = "git";
- url = input.url;
- ref = input.ref or null;
- }
- else if
- lib.elem input.type [
- "tarball"
- "file"
- ]
- then
- {
- type = "archive";
- url = input.url;
- }
- else
- null;
-
- flakeUrlToNixlock =
- url:
- let
- scheme = builtins.head (lib.splitString ":" url);
- rest = lib.concatStringsSep ":" (builtins.tail (lib.splitString ":" url));
- in
- if scheme == "github" then
- parseGithub rest
- else if scheme == "gitlab" then
- parseGitlab rest
- else if scheme == "sourcehut" then
- parseSourcehut rest
- else if lib.hasPrefix "git+" url then
- parseGitUrl (lib.removePrefix "git+" url)
- else if lib.hasPrefix "tarball+" url then
- flakeUrlToNixlock (lib.removePrefix "tarball+" url)
- else if lib.hasPrefix "file+" url then
- flakeUrlToNixlock (lib.removePrefix "file+" url)
- else if lib.hasPrefix "http" url then
- {
- type = "archive";
- inherit url;
- }
- else
- null;
-
- toNixlockInput =
- _name: input:
- if input ? url then
- flakeUrlToNixlock input.url
- else if input ? type then
- attrsetToNixlock input
- else
- null;
-
-in
-{
- inherit toNixlockInput;
-}
diff --git a/modules/tack/default.nix b/modules/tack/default.nix
new file mode 100644
index 0000000..35a9bc0
--- /dev/null
+++ b/modules/tack/default.nix
@@ -0,0 +1,148 @@
+{ lib, config, ... }:
+let
+ inherit (config) flake-file;
+ inherit (import ../lib.nix lib) inputsExpr;
+
+ serialize = import ./serialize.nix lib;
+
+ tackSrc = fetchTarball {
+ url = "https://github.com/manic-systems/tack/archive/8c574901340af860979500f24495417ce7e53cdc.tar.gz";
+ sha256 = "sha256-5dYWCjKBwjHOCJBlQK9iBKJxoUosTGQvI62vPBdQUgs=";
+ };
+
+ cfg = flake-file.tack;
+
+ tackFields = builtins.mapAttrs (
+ _: inp:
+ lib.filterAttrs (_: v: v != null) {
+ tackType = inp.tackType or null;
+ unpack = inp.unpack or null;
+ }
+ ) flake-file.inputs;
+
+ inputs = builtins.mapAttrs (name: inp: (tackFields.${name} or { }) // inp) (
+ flake-file.preProcess (inputsExpr flake-file.inputs)
+ );
+
+ pinsToml = serialize {
+ inherit inputs;
+ inherit (cfg) shorturls allFollow recomposable;
+ };
+
+ tackDir = "${flake-file.intoPath}/${cfg.lockDir}";
+
+ resolverExists = lib.hasPrefix "/" tackDir && builtins.pathExists "${tackDir}/default.nix";
+
+ sources = if resolverExists then (import tackDir) { overrides = cfg.overrides; } else { };
+
+ write-tack =
+ pkgs:
+ pkgs.writeShellApplication {
+ name = "write-tack";
+ meta.description = "Generate ${cfg.lockDir}/${cfg.pinsFile} from flake-file.inputs and run tack update.";
+ runtimeInputs = [
+ (cfg.package pkgs)
+ pkgs.coreutils
+ ];
+ text = ''
+ TACK_DIR=${lib.escapeShellArg tackDir}
+ export TACK_DIR
+ mkdir -p "$TACK_DIR"
+ cp ${pkgs.writeText "pins.toml" pinsToml} "$TACK_DIR/${cfg.pinsFile}"
+ chmod +w "$TACK_DIR/${cfg.pinsFile}"
+ [ -e "$TACK_DIR/default.nix" ] || tack init --resolver
+ exec tack update "$@"
+ '';
+ };
+in
+{
+ config.flake-file.apps = { inherit write-tack; };
+
+ options.flake-file.inputs = lib.mkOption {
+ type = lib.types.lazyAttrsOf (
+ lib.types.submodule {
+ options = {
+ tackType = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.enum [
+ "flake"
+ "fetch"
+ "fixed"
+ ]
+ );
+ default = null;
+ description = "tack pin type. null defaults to flake (or fetch when `flake = false`).";
+ };
+ unpack = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.enum [
+ "tarball"
+ "file"
+ ]
+ );
+ default = null;
+ description = "tack unpack mode for fixed pins.";
+ };
+ };
+ }
+ );
+ };
+
+ options.flake-file.tack = {
+ package = lib.mkOption {
+ type = lib.types.functionTo lib.types.package;
+ default = pkgs: pkgs.tack or (pkgs.callPackage "${tackSrc}/nix/package.nix" { });
+ defaultText = lib.literalExpression "pkgs: pkgs.tack or (pkgs.callPackage \"\${tackSrc}/nix/package.nix\" { })";
+ description = "Function from pkgs to the tack package providing the `tack` binary. Defaults to `pkgs.tack` when present, otherwise builds tack from a pinned source tarball.";
+ };
+
+ lockDir = lib.mkOption {
+ type = lib.types.str;
+ default = ".tack";
+ description = "Directory holding tack pins/lock/resolver, relative to intoPath.";
+ };
+
+ pinsFile = lib.mkOption {
+ type = lib.types.str;
+ default = "pins.toml";
+ description = "tack pins file name (the generated source of truth).";
+ };
+
+ lockFile = lib.mkOption {
+ type = lib.types.str;
+ default = "pins.lock.json";
+ description = "tack lock file name (written by tack update, read by nix).";
+ };
+
+ recomposable = lib.mkOption {
+ type = lib.types.nullOr lib.types.bool;
+ default = true;
+ description = "Emit `[tack] recomposable`. null omits the table.";
+ };
+
+ shorturls = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ description = "tack `[shorturls]` scheme expansions, e.g. { gh = \"github:{path}\"; }.";
+ };
+
+ allFollow = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.either lib.types.str (lib.types.listOf lib.types.str));
+ default = { };
+ description = "tack `[all_follow]` rules: name -> target, or target -> [aliases].";
+ };
+
+ overrides = lib.mkOption {
+ type = lib.types.raw;
+ default = { };
+ description = "tack resolver overrides passed as `tackOverrides`.";
+ };
+
+ sources = lib.mkOption {
+ type = lib.types.raw;
+ readOnly = true;
+ description = "Inputs resolved from the tack lockfile via the resolver, ready as flake inputs.";
+ default = sources;
+ };
+ };
+}
diff --git a/modules/tack/serialize.nix b/modules/tack/serialize.nix
new file mode 100644
index 0000000..6423207
--- /dev/null
+++ b/modules/tack/serialize.nix
@@ -0,0 +1,108 @@
+lib:
+let
+ inherit (lib)
+ concatStringsSep
+ mapAttrsToList
+ filterAttrs
+ isList
+ optional
+ optionals
+ boolToString
+ ;
+
+ needsQuote = k: builtins.match "[A-Za-z0-9_-]+" k == null;
+ renderKey = k: if needsQuote k then ''"${k}"'' else k;
+
+ str = s: ''"${lib.escape [ ''"'' "\\" ] s}"'';
+
+ inputUrl =
+ name: inp:
+ if (inp.url or "") != "" then
+ inp.url
+ else
+ let
+ ref = if (inp.ref or "") != "" then "/${inp.ref}" else "";
+ in
+ if (inp.type or null) != null && (inp.owner or "") != "" then
+ "${inp.type}:${inp.owner}/${inp.repo or ""}${ref}"
+ else
+ throw "tack serialize: input '${name}' has no url and cannot synthesise one";
+
+ pinType =
+ inp:
+ if (inp.tackType or null) != null then
+ inp.tackType
+ else if (inp.flake or true) == false then
+ "fetch"
+ else
+ null;
+
+ followsTable =
+ inp:
+ let
+ nested = inp.inputs or { };
+ pairs = filterAttrs (_: v: (v.follows or null) != null) nested;
+ in
+ if pairs == { } then
+ null
+ else
+ "{ "
+ + concatStringsSep ", " (mapAttrsToList (c: v: "${renderKey c} = ${str v.follows}") pairs)
+ + " }";
+
+ renderInput =
+ name: inp:
+ concatStringsSep "\n" (
+ [
+ "[inputs.${renderKey name}]"
+ "url = ${str (inputUrl name inp)}"
+ ]
+ ++ optional (pinType inp != null) "type = ${str (pinType inp)}"
+ ++ optional ((inp.dir or "") != "") "dir = ${str inp.dir}"
+ ++ optional ((inp.submodules or null) == true) "submodules = true"
+ ++ optional ((inp.unpack or null) != null) "unpack = ${str inp.unpack}"
+ ++ optional (followsTable inp != null) "follows = ${followsTable inp}"
+ );
+
+ renderShorturls =
+ su:
+ optionals (su != { }) ([ "[shorturls]" ] ++ mapAttrsToList (k: v: "${renderKey k} = ${str v}") su);
+
+ renderAllFollow =
+ af:
+ optionals (af != { }) (
+ [ "[all_follow]" ]
+ ++ mapAttrsToList (
+ k: v:
+ if isList v then
+ "${renderKey k} = [${concatStringsSep ", " (map str v)}]"
+ else
+ "${renderKey k} = ${str v}"
+ ) af
+ );
+
+ renderTack =
+ cfg:
+ optionals ((cfg.recomposable or null) != null) [
+ "[tack]"
+ "recomposable = ${boolToString cfg.recomposable}"
+ ];
+in
+cfg:
+let
+ header = concatStringsSep "\n" [
+ "# tack pins. generated by flake-file write-tack."
+ "# nix reads pins.lock.json; this file drives tack update"
+ ];
+
+ section = lines: optional (lines != [ ]) (concatStringsSep "\n" lines);
+
+ blocks = [
+ header
+ ]
+ ++ section (renderTack cfg)
+ ++ section (renderShorturls (cfg.shorturls or { }))
+ ++ section (renderAllFollow (cfg.allFollow or { }))
+ ++ mapAttrsToList renderInput (cfg.inputs or { });
+in
+concatStringsSep "\n\n" blocks + "\n"
diff --git a/modules/write-lock.nix b/modules/write-lock.nix
index 54007f6..9e3fd8c 100644
--- a/modules/write-lock.nix
+++ b/modules/write-lock.nix
@@ -13,8 +13,8 @@ let
app = "write-npins";
}
{
- file = flake-file.nixlock.lockFileName;
- app = "write-nixlock";
+ file = "${flake-file.tack.lockDir}/${flake-file.tack.lockFile}";
+ app = "write-tack";
}
{
file = "unflake.nix";
diff --git a/templates/nixlock/README.md b/templates/nixlock/README.md
deleted file mode 100644
index 05e3d29..0000000
--- a/templates/nixlock/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Nixlock
-
-This template is an example of using `flake-file.inputs` in a non-flakes project.
-
-It uses [nixlock](https://codeberg.org/FrdrCkII/nixlock) to pin and fetch inputs defined as options inside `./modules`.
-
-## Generate nixlock
-
-The following command is a convenience for generating `nixlock.lock.nix`:
-
-```shell
-nix-shell . -A flake-file.sh --run write-nixlock
-```
-
-You can also the `update` command.
-
-```shell
-nix-shell . -A flake-file.sh --run 'write-nixlock update'
-```
diff --git a/templates/nixlock/default.nix b/templates/nixlock/default.nix
deleted file mode 100644
index 7bcc08b..0000000
--- a/templates/nixlock/default.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-let
- with-inputs = import sources.with-inputs sources {
- # uncomment for local checkout on CI
- # flake-file = import ./../../modules;
- };
-
- outputs =
- inputs:
- (inputs.nixpkgs.lib.evalModules {
- modules = [ (inputs.import-tree ./modules) ];
- specialArgs = { inherit inputs; };
- }).config;
-
- sources = builtins.mapAttrs (
- _n: v:
- v
- // {
- outPath = fetchTarball {
- url = v.lock.url;
- sha256 = v.lock.hash;
- };
- }
- ) (import ./nixlock.lock.nix);
-in
-with-inputs outputs
diff --git a/templates/nixlock/modules/default.nix b/templates/nixlock/modules/default.nix
deleted file mode 100644
index 9cae782..0000000
--- a/templates/nixlock/modules/default.nix
+++ /dev/null
@@ -1,11 +0,0 @@
-{ inputs, ... }:
-{
- imports = [ inputs.flake-file.flakeModules.nixlock ];
-
- flake-file.inputs = {
- flake-file.url = "github:vic/flake-file";
- import-tree.url = "github:vic/import-tree";
- with-inputs.url = "github:vic/with-inputs";
- nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
- };
-}
diff --git a/templates/nixlock/nixlock.lock.nix b/templates/nixlock/nixlock.lock.nix
deleted file mode 100644
index d7f71b9..0000000
--- a/templates/nixlock/nixlock.lock.nix
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "flake-file" = {
- "lock" = {
- "hash" = "sha256-5FB/FQ+gCvFbEuyMxydkUHuMCe0UEgMra5jI2D6SAQU=";
- "url" = "https://github.com/vic/flake-file/archive/cdb4abe76b3ff0b611343514f584faa2688a12af.tar.gz";
- };
- "meta" = {
- "ref" = "HEAD";
- "type" = "gitArchive";
- "url" = "https://github.com/vic/flake-file";
- };
- };
- "import-tree" = {
- "lock" = {
- "hash" = "sha256-4TRO+jUycfR3g4KAbyK0wQEEJ7Xcmid2Ry+tWv4HPLw=";
- "url" =
- "https://github.com/vic/import-tree/archive/205675fa599ff8a74b6c8c2cc5149cdaeac76f5f.tar.gz";
- };
- "meta" = {
- "ref" = "HEAD";
- "type" = "gitArchive";
- "url" = "https://github.com/vic/import-tree";
- };
- };
- "nixpkgs" = {
- "lock" = {
- "hash" = "sha256-BHKMR414WpfUddNyUtx2GR1VPl0R9sWGQs/opgYm9rc=";
- "url" = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
- };
- "meta" = {
- "type" = "archive";
- "url" = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
- };
- };
- "with-inputs" = {
- "lock" = {
- "hash" = "sha256-WFCkvOrArY0rhIPLESqitQHDpl1NFBCK4/7/z6c1040=";
- "url" =
- "https://github.com/vic/with-inputs/archive/ca62018475be1999e57fbfa7ec14ebea7c6e483d.tar.gz";
- };
- "meta" = {
- "ref" = "HEAD";
- "type" = "gitArchive";
- "url" = "https://github.com/vic/with-inputs";
- };
- };
-}
diff --git a/templates/tack/.tack/default.nix b/templates/tack/.tack/default.nix
new file mode 100644
index 0000000..cfc8495
--- /dev/null
+++ b/templates/tack/.tack/default.nix
@@ -0,0 +1,293 @@
+# SPDX-License-Identifier: EUPL-1.2
+# tack-managed resolver. delete this line to take ownership; tack will leave it alone afterwards.
+
+let
+ inherit (builtins)
+ attrNames
+ attrValues
+ concatMap
+ elemAt
+ filter
+ foldl'
+ fromJSON
+ head
+ intersectAttrs
+ isList
+ isString
+ listToAttrs
+ mapAttrs
+ match
+ pathExists
+ readFile
+ tail
+ trace
+ ;
+
+ call =
+ {
+ overrides ? { },
+ }:
+ let
+ pins = fromTOML (readFile ./pins.toml);
+ lock = fromJSON (readFile ./pins.lock.json);
+ all_follow_raw = pins.all_follow or { };
+
+ # flatten `target = [aliases]` rows alongside `alias = "target"` rows
+ all_follow = foldl' (
+ acc: key:
+ let
+ val = all_follow_raw.${key};
+ in
+ if isList val then
+ acc
+ // {
+ ${key} = key;
+ }
+ // listToAttrs (
+ map (a: {
+ name = a;
+ value = key;
+ }) val
+ )
+ else if isString val then
+ acc // { ${key} = val; }
+ else
+ acc
+ ) { } (attrNames all_follow_raw);
+
+ fetchPin = name: fetchTree lock.${name};
+
+ fetchFixed =
+ name: entry:
+ let
+ raw = derivation {
+ inherit name;
+ inherit (entry) url;
+ builder = "builtin:fetchurl";
+ system = "builtin";
+ outputHash = entry.sha256;
+ outputHashAlgo = "sha256";
+ outputHashMode = "flat";
+ };
+ unpacked = derivation {
+ inherit name;
+ builder = "builtin:unpack-channel";
+ system = "builtin";
+ src = raw;
+ channelName = name;
+ };
+ in
+ if (entry.unpack or "file") == "tarball" then unpacked.outPath + "/" + name else raw.outPath;
+
+ resolveSpec = upLock: spec: if isList spec then walkPath upLock upLock.root spec else spec;
+
+ walkPath =
+ upLock: nodeName: path:
+ if path == [ ] then
+ nodeName
+ else
+ walkPath upLock (resolveSpec upLock upLock.nodes.${nodeName}.inputs.${head path}) (tail path);
+
+ followsFor =
+ pin:
+ let
+ rules = removeAttrs all_follow (pin.exclude_follow or [ ]);
+ in
+ {
+ level = (pin.follows or { }) // rules;
+ deep = rules;
+ };
+
+ resolveFollows = mapAttrs (
+ _: target: self.${target} or (throw "tack: follows target '${target}' is not a pin")
+ );
+
+ # a follows key is `flake:name`, `tack:name`, or a bare `name`.
+ # project a follows set onto one side, rekeyed to bare names.
+ followsForSide =
+ side: follows:
+ listToAttrs (
+ concatMap (
+ key:
+ let
+ m = match "(flake|tack):(.*)" key;
+ in
+ if m == null then
+ [
+ {
+ name = key;
+ value = follows.${key};
+ }
+ ]
+ else if head m == side then
+ [
+ {
+ name = elemAt m 1;
+ value = follows.${key};
+ }
+ ]
+ else
+ [ ]
+ ) (attrNames follows)
+ );
+
+ mkCallerInputs =
+ upLock: nodeName: rawInputs: levelFollows: deepFollows:
+ let
+ resolved = resolveFollows levelFollows;
+ in
+ mapAttrs (
+ n: _decl:
+ resolved.${n} or (
+ if upLock != null then
+ let
+ ref =
+ (upLock.nodes.${nodeName}.inputs or { }).${n}
+ or (throw "tack: input '${n}' declared but not in flake.lock node '${nodeName}'");
+ childName = resolveSpec upLock ref;
+ childNode = upLock.nodes.${childName};
+ childSrc = fetchTree childNode.locked;
+ in
+ if childNode.flake or true then evalTransitive upLock childName childSrc deepFollows else childSrc
+ else
+ throw "tack: no flake.lock; cannot resolve input '${n}'"
+ )
+ ) rawInputs;
+
+ mkFlakeResult =
+ sourceInfo: flakeDir: callerInputs: outputs:
+ outputs
+ // sourceInfo
+ // {
+ outPath = flakeDir;
+ inputs = callerInputs;
+ inherit outputs sourceInfo;
+ _type = "flake";
+ };
+
+ evalFlake =
+ sourceInfo: flakeDir: upLock: nodeName: levelFollows: deepFollows:
+ let
+ raw = import (flakeDir + "/flake.nix");
+
+ tackPinsPath = flakeDir + "/.tack/pins.toml";
+ hasTack = pathExists tackPinsPath;
+ upPins = if hasTack then fromTOML (readFile tackPinsPath) else { };
+
+ # project follows onto each side, then keep only the names that side has.
+ # note that a bare follow reaches both, a `flake:`/`tack:` follow just the one.
+ tackOverrides = resolveFollows (
+ intersectAttrs (upPins.inputs or { }) (followsForSide "tack" levelFollows)
+ );
+ flakeLevel = intersectAttrs (raw.inputs or { }) (followsForSide "flake" levelFollows);
+
+ # deep follows pass down raw, so each descendant re-projects per side
+ callerInputs = mkCallerInputs upLock nodeName (raw.inputs or { }) flakeLevel deepFollows;
+
+ # upstream's own claim that its outputs forward tackOverrides. a closed
+ # `{ self }:` upstream would throw on the extra kwarg, so forward only here.
+ supportsOverrides = (upPins.tack or { }).recomposable or false;
+
+ extraArgs = if supportsOverrides && tackOverrides != { } then { inherit tackOverrides; } else { };
+
+ outputs = raw.outputs (callerInputs // extraArgs // { self = result; });
+
+ result =
+ let
+ base = mkFlakeResult sourceInfo flakeDir callerInputs outputs;
+ in
+ if hasTack && tackOverrides != { } && !supportsOverrides then
+ trace "tack: ${flakeDir}: not marked recomposable (set [tack] recomposable = true); overrides will not reach upstream" base
+ else
+ base;
+ in
+ result;
+
+ evalTransitive =
+ upLock: nodeName: sourceInfo: follows:
+ evalFlake sourceInfo sourceInfo.outPath upLock nodeName follows follows;
+
+ evalTopFlake =
+ sourceInfo: pin:
+ let
+ flakeDir = sourceInfo.outPath + (if pin ? dir then "/" + pin.dir else "");
+ upLockPath = flakeDir + "/flake.lock";
+ upLock = if pathExists upLockPath then fromJSON (readFile upLockPath) else null;
+ rootNode = if upLock != null then upLock.root else null;
+ f = followsFor pin;
+ in
+ evalFlake sourceInfo flakeDir upLock rootNode f.level f.deep;
+
+ evalFetch =
+ sourceInfo: pin: subdir:
+ let
+ path = sourceInfo.outPath + subdir;
+ tackPinsPath = path + "/.tack/pins.toml";
+ hasTack = pathExists tackPinsPath;
+ upPins = if hasTack then fromTOML (readFile tackPinsPath) else { };
+ f = followsFor pin;
+ # a fetch drill-in is tack-only
+ tackOverrides = resolveFollows (
+ intersectAttrs (upPins.inputs or { }) (followsForSide "tack" f.level)
+ );
+ in
+ # a fetch pin is a source tree (a path). only when there are overrides to
+ # push into the upstream's own .tack do we hand back its resolved inputs
+ if hasTack && tackOverrides != { } then
+ let
+ upstream = import (path + "/.tack");
+ in
+ # old resolvers return a plain attrset, not a callable functor
+ if upstream ? __functor then
+ (upstream { overrides = tackOverrides; }) // { outPath = path; }
+ else
+ trace "tack: ${path}: upstream .tack predates override support; overrides will not reach it" path
+ else
+ path;
+
+ loadPin =
+ name: pin:
+ let
+ pinType = pin.type or (if pin.flake or true then "flake" else "fetch");
+ subdir = if pin ? dir then "/" + pin.dir else "";
+ in
+ if pinType == "fixed" then
+ fetchFixed name lock.${name}
+ else
+ let
+ sourceInfo = fetchPin name;
+ in
+ if pinType == "flake" then evalTopFlake sourceInfo pin else evalFetch sourceInfo pin subdir;
+
+ declared = pins.inputs or { };
+
+ # undeclared lock entries are auto-dedup synthetics only when they are
+ # referenced as [all_follow] targets. stale locks left after hand-editing
+ # pins.toml are ignored, and can be cleaned with `tack rm `.
+ autoTargets = listToAttrs (
+ map (target: {
+ name = target;
+ value = true;
+ }) (attrValues all_follow)
+ );
+ autoNames = filter (n: !(declared ? ${n}) && autoTargets ? ${n}) (attrNames lock);
+ autoPin =
+ name:
+ let
+ sourceInfo = fetchPin name;
+ in
+ if pathExists (sourceInfo.outPath + "/flake.nix") then evalTopFlake sourceInfo { } else sourceInfo;
+
+ self =
+ (mapAttrs loadPin declared)
+ // listToAttrs (
+ map (name: {
+ inherit name;
+ value = autoPin name;
+ }) autoNames
+ )
+ // overrides;
+ in
+ self // { __functor = _: call; };
+in
+call { }
diff --git a/templates/tack/.tack/pins.lock.json b/templates/tack/.tack/pins.lock.json
new file mode 100644
index 0000000..9e7e0b7
--- /dev/null
+++ b/templates/tack/.tack/pins.lock.json
@@ -0,0 +1,42 @@
+{
+ "flake-file": {
+ "lastModified": 1779051720,
+ "narHash": "sha256-+jbXnODsR19pFKB0x/6kHhFgW6yV6N+CGClFr45eDU8=",
+ "owner": "vic",
+ "repo": "flake-file",
+ "rev": "c58eb27d9434e5be0c8693f1eb18d47035bc21ba",
+ "type": "github"
+ },
+ "flake-parts": {
+ "lastModified": 1778716662,
+ "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
+ "type": "github"
+ },
+ "import-tree": {
+ "lastModified": 1778781969,
+ "narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=",
+ "owner": "vic",
+ "repo": "import-tree",
+ "rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274",
+ "type": "github"
+ },
+ "nixpkgs": {
+ "lastModified": 1780243769,
+ "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
+ "type": "github"
+ },
+ "tack": {
+ "lastModified": 1780213242,
+ "narHash": "sha256-5dYWCjKBwjHOCJBlQK9iBKJxoUosTGQvI62vPBdQUgs=",
+ "owner": "manic-systems",
+ "repo": "tack",
+ "rev": "8c574901340af860979500f24495417ce7e53cdc",
+ "type": "github"
+ }
+}
diff --git a/templates/tack/.tack/pins.toml b/templates/tack/.tack/pins.toml
new file mode 100644
index 0000000..15f87dc
--- /dev/null
+++ b/templates/tack/.tack/pins.toml
@@ -0,0 +1,20 @@
+# tack pins. generated by flake-file write-tack.
+# nix reads pins.lock.json; this file drives tack update
+
+[tack]
+recomposable = true
+
+[inputs.flake-file]
+url = "github:vic/flake-file"
+
+[inputs.flake-parts]
+url = "github:hercules-ci/flake-parts"
+
+[inputs.import-tree]
+url = "github:vic/import-tree"
+
+[inputs.nixpkgs]
+url = "github:NixOS/nixpkgs/nixos-unstable"
+
+[inputs.tack]
+url = "github:manic-systems/tack"
diff --git a/templates/tack/README.md b/templates/tack/README.md
new file mode 100644
index 0000000..e2ef3c1
--- /dev/null
+++ b/templates/tack/README.md
@@ -0,0 +1,28 @@
+# tack
+
+This template uses `flake-file.inputs` with
+[tack](https://github.com/manic-systems/tack): flake-like TOML pins, lazily
+fetched and transformed, with no `flake.lock`.
+
+Inputs are declared as options inside `./modules`. The `write-tack` writer
+renders them into `.tack/pins.toml`, then runs `tack update` to produce
+`.tack/pins.lock.json`. Nix reads the lock through the tack resolver
+(`.tack/default.nix`); `pins.toml` only drives `tack update`.
+
+## Update pins
+
+Regenerate `.tack/pins.toml` from your declared inputs and relock:
+
+```shell
+nix run .#write-lock
+```
+
+`write-lock` detects the existing `.tack/pins.lock.json` and delegates to
+`write-tack`. You can also call `write-tack` directly, passing through any
+`tack update` arguments (e.g. `write-tack ` to update one input).
+
+## Recomposability
+
+`[tack] recomposable = true` lets another tack flake pass `tackOverrides` into
+this one. The flake entrypoint threads the real `self.outPath` so module paths
+resolve against the locked flake source.
diff --git a/templates/tack/default.nix b/templates/tack/default.nix
new file mode 100644
index 0000000..f074bd1
--- /dev/null
+++ b/templates/tack/default.nix
@@ -0,0 +1,22 @@
+let
+ mkOutputs =
+ {
+ tackOverrides ? { },
+ flakeSelf ? null,
+ }:
+ let
+ rawInputs = (import ./.tack) { overrides = tackOverrides; };
+ inputs = rawInputs // {
+ self = self';
+ };
+ self' = outputs // {
+ inherit inputs;
+ outPath = if flakeSelf != null then flakeSelf.outPath else ./.;
+ };
+ outputs = rawInputs.flake-parts.lib.mkFlake { inherit inputs; } (rawInputs.import-tree ./modules);
+ in
+ outputs;
+in
+{
+ __functor = _: mkOutputs;
+}
diff --git a/templates/tack/flake.nix b/templates/tack/flake.nix
new file mode 100644
index 0000000..f45131c
--- /dev/null
+++ b/templates/tack/flake.nix
@@ -0,0 +1,8 @@
+{
+ outputs =
+ { self, ... }@args:
+ (import ./.) {
+ tackOverrides = args.tackOverrides or { };
+ flakeSelf = self;
+ };
+}
diff --git a/templates/tack/modules/default.nix b/templates/tack/modules/default.nix
new file mode 100644
index 0000000..192f058
--- /dev/null
+++ b/templates/tack/modules/default.nix
@@ -0,0 +1,30 @@
+{ inputs, config, ... }:
+{
+ imports = [ inputs.flake-file.flakeModules.tack ];
+
+ flake-file.inputs = {
+ flake-file.url = "github:vic/flake-file";
+ import-tree.url = "github:vic/import-tree";
+ flake-parts.url = "github:hercules-ci/flake-parts";
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ tack.url = "github:manic-systems/tack";
+ };
+
+ flake-file.tack.package = pkgs: inputs.tack.packages.${pkgs.stdenv.hostPlatform.system}.default;
+
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "aarch64-darwin"
+ ];
+
+ perSystem =
+ { pkgs, ... }:
+ {
+ packages.write-tack = config.flake-file.apps.write-tack pkgs;
+ packages.write-lock = config.flake-file.apps.write-lock pkgs;
+ devShells.default = pkgs.mkShellNoCC {
+ packages = [ (config.flake-file.apps.write-lock pkgs) ];
+ };
+ };
+}