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) ]; + }; + }; +}