Generate flake.nix from module options. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic nix inputs

feat: add deps backend (#72)

feat: update npins, default, formatter for deps backend

fix: move inputs-lib to dev/_lib, fix test imports

feat: include deps backend in default.nix

feat: comprehensive deps URL parser, url.bash helpers, full spec coverage

style: nixfmt parse-url test and lib files

feat: attrset input forms, progress output, rich deps.nix metadata, normalize.nix

refactor: split fetch-forge.bash, fix git test URL, add fetch-forge to deps.nix

fix: empty seedBlock invalid bash, write-deps SC2016 shellcheck, add deps CI bootstrap

authored by oeiuwq.com and committed by

GitHub 1bf87eeb c9d91bd7

+1205 -113
+9 -33
.github/workflows/flake-check.yaml
··· 21 21 runs-on: ubuntu-latest 22 22 strategy: 23 23 matrix: 24 - bootstrap: [inputs, flake, npins, unflake] 24 + bootstrap: [inputs, flake, npins, unflake, deps] 25 25 env: 26 26 NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz" 27 27 steps: ··· 39 39 if: ${{ matrix.bootstrap == 'unflake' }} 40 40 - run: cat bootstrap/npins/sources.json 41 41 if: ${{ matrix.bootstrap == 'npins' }} 42 + - run: cat bootstrap/deps.nix 43 + if: ${{ matrix.bootstrap == 'deps' }} 44 + - name: Assert bootstrap deps has pinned nixpkgs 45 + if: ${{ matrix.bootstrap == 'deps' }} 46 + run: | 47 + nix-instantiate --parse bootstrap/deps.nix 48 + grep -E '^\s+nixpkgs\s*=' bootstrap/deps.nix 49 + grep -E '^\s+flake-parts\s*=' bootstrap/deps.nix 42 50 - name: Assert bootstrap npins transitive discovery (flake-parts -> nixpkgs-lib) 43 51 if: ${{ matrix.bootstrap == 'npins' }} 44 52 run: | ··· 89 97 sed -i 's/# flake-file = import/flake-file = import/' default.nix 90 98 echo "{ inputs, ... }: { npins.pkgs = import inputs.nixpkgs {}; }" | tee modules/pkgs.nix 91 99 nix-shell . -A npins.env --run write-npins 92 - npins-transitive: 93 - name: Check npins transitive discovery 94 - runs-on: ubuntu-latest 95 - env: 96 - NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz" 97 - steps: 98 - - uses: actions/checkout@v4 99 - - uses: wimpysworld/nothing-but-nix@main 100 - - uses: cachix/install-nix-action@v31 101 - - uses: DeterminateSystems/magic-nix-cache-action@main 102 - - name: Run write-npins with neomacs (has transitive deps) 103 - run: | 104 - mkdir test-out 105 - nix-shell ./default.nix -A flake-file.sh \ 106 - --run write-npins \ 107 - --arg modules ./modules/npins-transitive-test.nix \ 108 - --argstr outdir test-out 109 - - name: Assert declared inputs are pinned 110 - run: | 111 - jq -e '.pins | has("neomacs")' test-out/npins/sources.json 112 - jq -e '.pins | has("nixpkgs")' test-out/npins/sources.json 113 - - name: Assert transitive deps are auto-discovered 114 - run: | 115 - jq -e '.pins | has("crane")' test-out/npins/sources.json 116 - jq -e '.pins | has("rust-overlay")' test-out/npins/sources.json 117 - jq -e '.pins | has("nix-wpe-webkit")' test-out/npins/sources.json 118 - - name: Assert dedup-by-name (nixpkgs stays as Channel, not re-pinned from neomacs dep) 119 - run: | 120 - jq -e '.pins.nixpkgs.type == "Channel"' test-out/npins/sources.json 121 - - name: Show pinned sources 122 - if: always() 123 - run: cat test-out/npins/sources.json 124 100 unflake: 125 101 name: Check unflake 126 102 runs-on: ubuntu-latest
+1
default.nix
··· 39 39 ./modules 40 40 ./modules/options 41 41 ./modules/npins.nix 42 + ./modules/deps.nix 42 43 ./modules/unflake.nix 43 44 ./modules/write-inputs.nix 44 45 ./modules/write-flake.nix
+28
dev/modules/_lib/inputs-lib.nix
··· 1 + lib: esc: inputs: 2 + let 3 + pinnableInputs = lib.filterAttrs (_: v: v.url or "" != "") inputs; 4 + followsInputs = lib.filterAttrs (_: v: v.url or "" == "") inputs; 5 + 6 + seedBlock = 7 + entries: mkLine: 8 + let 9 + cmds = lib.mapAttrsToList mkLine entries; 10 + in 11 + if cmds == [ ] then ":" else "{\n" + lib.concatStringsSep "\n" cmds + "\n}"; 12 + 13 + followsSeed = 14 + seedBlock followsInputs (name: _: " printf '%s\\n' ${esc name}") + " >> \"$FOLLOWS_FILE\""; 15 + 16 + queueSeed = 17 + seedBlock pinnableInputs (name: input: " printf '%s\\t%s\\n' ${esc name} ${esc (input.url or "")}") 18 + + " >> \"$QUEUE_FILE\""; 19 + 20 + in 21 + { 22 + inherit 23 + pinnableInputs 24 + followsInputs 25 + followsSeed 26 + queueSeed 27 + ; 28 + }
+250
dev/modules/_lib/parse-url.nix
··· 1 + # Pure Nix flake URL parser. Returns a structured attrset for any flake URL. 2 + # Covers all types defined in the Nix flake reference spec. 3 + lib: 4 + let 5 + splitFirst = 6 + sep: s: 7 + let 8 + parts = lib.splitString sep s; 9 + in 10 + { 11 + head = lib.head parts; 12 + tail = lib.concatStringsSep sep (lib.tail parts); 13 + }; 14 + 15 + queryParam = 16 + key: queryStr: 17 + let 18 + pairs = if queryStr == "" then [ ] else lib.splitString "&" queryStr; 19 + m = lib.filter (lib.hasPrefix "${key}=") pairs; 20 + in 21 + if m == [ ] then "" else lib.removePrefix "${key}=" (lib.head m); 22 + 23 + parseUrl = 24 + rawUrl: 25 + let 26 + q = splitFirst "?" rawUrl; 27 + base = q.head; 28 + queryStr = q.tail; 29 + param = key: queryParam key queryStr; 30 + 31 + s = splitFirst ":" base; 32 + scheme = s.head; 33 + rest = s.tail; 34 + 35 + decodeSlash = lib.replaceStrings [ "%2F" "%2f" ] [ "/" "/" ]; 36 + 37 + parseOwnerRepo = 38 + path: 39 + let 40 + parts = lib.splitString "/" (decodeSlash path); 41 + owner = lib.elemAt parts 0; 42 + repo = if lib.length parts > 1 then lib.elemAt parts 1 else ""; 43 + pathRef = if lib.length parts > 2 then lib.concatStringsSep "/" (lib.drop 2 parts) else ""; 44 + in 45 + { 46 + inherit owner repo pathRef; 47 + }; 48 + 49 + inferType = 50 + if 51 + lib.elem scheme [ 52 + "github" 53 + "gitlab" 54 + "sourcehut" 55 + "indirect" 56 + "path" 57 + "tarball" 58 + "file" 59 + ] 60 + then 61 + scheme 62 + else if 63 + lib.elem scheme [ 64 + "git" 65 + "git+https" 66 + "git+http" 67 + "git+ssh" 68 + "git+git" 69 + "git+file" 70 + ] 71 + then 72 + "git" 73 + else if 74 + lib.elem scheme [ 75 + "hg" 76 + "hg+https" 77 + "hg+http" 78 + "hg+ssh" 79 + "hg+file" 80 + ] 81 + then 82 + "mercurial" 83 + else if 84 + lib.elem scheme [ 85 + "tarball+https" 86 + "tarball+http" 87 + "tarball+file" 88 + ] 89 + then 90 + "tarball" 91 + else if 92 + lib.elem scheme [ 93 + "file+https" 94 + "file+http" 95 + "file+file" 96 + ] 97 + then 98 + "file" 99 + else if 100 + lib.hasSuffix ".tar.gz" base 101 + || lib.hasSuffix ".tar.xz" base 102 + || lib.hasSuffix ".tgz" base 103 + || lib.hasSuffix ".tar.bz2" base 104 + || lib.hasSuffix ".tar.zst" base 105 + || lib.hasSuffix ".zip" base 106 + then 107 + "tarball" 108 + else 109 + "indirect"; 110 + 111 + type = inferType; 112 + 113 + ghgl = owner: repo: pathRef: defHost: { 114 + inherit type; 115 + owner = owner; 116 + repo = repo; 117 + ref = 118 + let 119 + r = param "ref"; 120 + in 121 + if r != "" then r else pathRef; 122 + rev = param "rev"; 123 + host = 124 + let 125 + h = param "host"; 126 + in 127 + if h != "" then h else defHost; 128 + dir = param "dir"; 129 + url = ""; 130 + }; 131 + 132 + p = parseOwnerRepo rest; 133 + 134 + in 135 + if type == "github" then 136 + ghgl p.owner p.repo p.pathRef "github.com" 137 + else if type == "gitlab" then 138 + ghgl p.owner p.repo p.pathRef "gitlab.com" 139 + else if type == "sourcehut" then 140 + let 141 + p2 = parseOwnerRepo rest; 142 + owner = lib.removePrefix "~" p2.owner; 143 + in 144 + { 145 + inherit type; 146 + owner = owner; 147 + repo = p2.repo; 148 + ref = 149 + let 150 + r = param "ref"; 151 + in 152 + if r != "" then r else p2.pathRef; 153 + rev = param "rev"; 154 + host = 155 + let 156 + h = param "host"; 157 + in 158 + if h != "" then h else "git.sr.ht"; 159 + dir = ""; 160 + url = ""; 161 + } 162 + else if type == "git" then 163 + { 164 + inherit type; 165 + owner = ""; 166 + repo = ""; 167 + ref = param "ref"; 168 + rev = param "rev"; 169 + host = ""; 170 + dir = param "dir"; 171 + url = "${scheme}:${rest}"; 172 + } 173 + else if type == "mercurial" then 174 + { 175 + inherit type; 176 + owner = ""; 177 + repo = ""; 178 + ref = param "ref"; 179 + rev = param "rev"; 180 + host = ""; 181 + dir = ""; 182 + url = "${scheme}:${rest}"; 183 + } 184 + else if type == "tarball" then 185 + { 186 + inherit type; 187 + owner = ""; 188 + repo = ""; 189 + ref = ""; 190 + rev = ""; 191 + host = ""; 192 + dir = ""; 193 + url = 194 + if 195 + lib.elem scheme [ 196 + "tarball+https" 197 + "tarball+http" 198 + "tarball+file" 199 + ] 200 + then 201 + lib.removePrefix "tarball+" rawUrl 202 + else 203 + rawUrl; 204 + } 205 + else if type == "file" then 206 + { 207 + inherit type; 208 + owner = ""; 209 + repo = ""; 210 + ref = ""; 211 + rev = ""; 212 + host = ""; 213 + dir = ""; 214 + url = 215 + if 216 + lib.elem scheme [ 217 + "file+https" 218 + "file+http" 219 + "file+file" 220 + ] 221 + then 222 + lib.removePrefix "file+" rawUrl 223 + else 224 + rawUrl; 225 + } 226 + else if type == "path" then 227 + { 228 + inherit type; 229 + owner = ""; 230 + repo = ""; 231 + ref = ""; 232 + rev = ""; 233 + host = ""; 234 + dir = ""; 235 + url = rawUrl; 236 + } 237 + else 238 + { 239 + type = "indirect"; 240 + owner = ""; 241 + repo = ""; 242 + ref = ""; 243 + rev = ""; 244 + host = ""; 245 + dir = ""; 246 + url = rawUrl; 247 + }; 248 + 249 + in 250 + parseUrl
+1
dev/modules/formatter.nix
··· 34 34 "**/unflake.nix" # generated by: nix-shell . -A unflake.env --run write-unflake 35 35 "**/inputs.nix" # generated by: nix-shell . -A unflake.env --run write-inputs 36 36 "**/npins/default.nix" # generated by write-npins 37 + "modules/deps/*.bash" # shell fragments included via builtins.readFile 37 38 "docs/*" 38 39 ]; 39 40 };
+73
dev/modules/unit-tests/inputs-lib.nix
··· 1 + { lib, ... }: 2 + let 3 + esc = lib.escapeShellArg; 4 + subject = inputs: import ./../_lib/inputs-lib.nix lib esc inputs; 5 + 6 + tests.inputs-lib."pinnableInputs excludes empty-url entries" = { 7 + expr = 8 + (subject { 9 + foo.url = "github:owner/foo"; 10 + bar.url = ""; 11 + baz = { }; 12 + }).pinnableInputs; 13 + expected = { 14 + foo.url = "github:owner/foo"; 15 + }; 16 + }; 17 + 18 + tests.inputs-lib."pinnableInputs keeps all non-empty urls" = { 19 + expr = 20 + lib.attrNames 21 + (subject { 22 + a.url = "github:a/a"; 23 + b.url = "github:b/b"; 24 + c.url = ""; 25 + }).pinnableInputs; 26 + expected = [ 27 + "a" 28 + "b" 29 + ]; 30 + }; 31 + 32 + tests.inputs-lib."followsInputs excludes inputs with urls" = { 33 + expr = 34 + (subject { 35 + nixpkgs.url = "github:NixOS/nixpkgs"; 36 + nixpkgs-lib.follows = "nixpkgs"; 37 + baz = { }; 38 + }).followsInputs; 39 + expected = { 40 + nixpkgs-lib.follows = "nixpkgs"; 41 + baz = { }; 42 + }; 43 + }; 44 + 45 + tests.inputs-lib."followsInputs is empty when all inputs have urls" = { 46 + expr = (subject { nixpkgs.url = "github:NixOS/nixpkgs"; }).followsInputs; 47 + expected = { }; 48 + }; 49 + 50 + tests.inputs-lib."queueSeed contains name and url for each pinnable input" = { 51 + expr = (subject { foo.url = "github:owner/foo"; }).queueSeed; 52 + expected = "{\n printf '%s\\t%s\\n' 'foo' 'github:owner/foo'\n} >> \"$QUEUE_FILE\""; 53 + }; 54 + 55 + tests.inputs-lib."queueSeed is empty block on no pinnable inputs" = { 56 + expr = (subject { foo.follows = "bar"; }).queueSeed; 57 + expected = ": >> \"$QUEUE_FILE\""; 58 + }; 59 + 60 + tests.inputs-lib."followsSeed emits printf for each follows-only input" = { 61 + expr = (subject { nixpkgs-lib.follows = "nixpkgs"; }).followsSeed; 62 + expected = "{\n printf '%s\\n' 'nixpkgs-lib'\n} >> \"$FOLLOWS_FILE\""; 63 + }; 64 + 65 + tests.inputs-lib."followsSeed is empty block when all inputs have urls" = { 66 + expr = (subject { nixpkgs.url = "github:NixOS/nixpkgs"; }).followsSeed; 67 + expected = ": >> \"$FOLLOWS_FILE\""; 68 + }; 69 + 70 + in 71 + { 72 + flake = { inherit tests; }; 73 + }
+81
dev/modules/unit-tests/normalize.nix
··· 1 + { inputs, ... }: 2 + let 3 + normalize = import "${inputs.flake-file}/modules/deps/normalize.nix"; 4 + 5 + mkFlake = content: builtins.toFile "flake.nix" content; 6 + 7 + norm = content: normalize (mkFlake content); 8 + 9 + tests.normalize."url form: github" = { 10 + expr = norm ''{ inputs.nixpkgs.url = "github:NixOS/nixpkgs"; outputs = _: {}; }''; 11 + expected = { 12 + nixpkgs = "github:NixOS/nixpkgs"; 13 + }; 14 + }; 15 + 16 + tests.normalize."attrset form: github basic" = { 17 + expr = norm ''{ inputs.nixpkgs = { type = "github"; owner = "NixOS"; repo = "nixpkgs"; }; outputs = _: {}; }''; 18 + expected = { 19 + nixpkgs = "github:NixOS/nixpkgs"; 20 + }; 21 + }; 22 + 23 + tests.normalize."attrset form: github with ref" = { 24 + expr = norm ''{ inputs.nixpkgs = { type = "github"; owner = "NixOS"; repo = "nixpkgs"; ref = "nixos-23.11"; }; outputs = _: {}; }''; 25 + expected = { 26 + nixpkgs = "github:NixOS/nixpkgs/nixos-23.11"; 27 + }; 28 + }; 29 + 30 + tests.normalize."attrset form: github with rev" = { 31 + expr = norm ''{ inputs.nixpkgs = { type = "github"; owner = "NixOS"; repo = "nixpkgs"; rev = "abc123"; }; outputs = _: {}; }''; 32 + expected = { 33 + nixpkgs = "github:NixOS/nixpkgs/abc123"; 34 + }; 35 + }; 36 + 37 + tests.normalize."attrset form: github with host" = { 38 + expr = norm ''{ inputs.foo = { type = "github"; owner = "org"; repo = "bar"; host = "ghe.example.com"; }; outputs = _: {}; }''; 39 + expected = { 40 + foo = "github:org/bar?host=ghe.example.com"; 41 + }; 42 + }; 43 + 44 + tests.normalize."attrset form: gitlab" = { 45 + expr = norm ''{ inputs.foo = { type = "gitlab"; owner = "veloren"; repo = "veloren"; }; outputs = _: {}; }''; 46 + expected = { 47 + foo = "gitlab:veloren/veloren"; 48 + }; 49 + }; 50 + 51 + tests.normalize."attrset form: sourcehut" = { 52 + expr = norm ''{ inputs.foo = { type = "sourcehut"; owner = "misterio"; repo = "nix-colors"; }; outputs = _: {}; }''; 53 + expected = { 54 + foo = "sourcehut:~misterio/nix-colors"; 55 + }; 56 + }; 57 + 58 + tests.normalize."attrset form: git" = { 59 + expr = norm ''{ inputs.foo = { type = "git"; url = "git+https://example.org/my/repo"; ref = "main"; }; outputs = _: {}; }''; 60 + expected = { 61 + foo = "git+https://example.org/my/repo?ref=main"; 62 + }; 63 + }; 64 + 65 + tests.normalize."follows: omitted from output" = { 66 + expr = norm ''{ inputs.nixpkgs-lib.follows = "nixpkgs"; inputs.nixpkgs.url = "github:NixOS/nixpkgs"; outputs = _: {}; }''; 67 + expected = { 68 + nixpkgs = "github:NixOS/nixpkgs"; 69 + nixpkgs-lib = ""; 70 + }; 71 + }; 72 + 73 + tests.normalize."no inputs: empty attrset" = { 74 + expr = norm "{ outputs = _: {}; }"; 75 + expected = { }; 76 + }; 77 + 78 + in 79 + { 80 + flake = { inherit tests; }; 81 + }
+10 -54
dev/modules/unit-tests/npins.nix
··· 1 1 { lib, ... }: 2 2 let 3 3 esc = lib.escapeShellArg; 4 - 5 - # Mirrors pinnableInputs from modules/npins.nix 6 - pinnableInputs = inputs: lib.filterAttrs (_: v: v.url or "" != "") inputs; 7 - 8 - # Mirrors queueSeed from modules/npins.nix 9 - queueSeed = 10 - pinnable: 11 - let 12 - lines = lib.mapAttrsToList ( 13 - name: input: " printf '%s\\t%s\\n' ${esc name} ${esc (input.url or "")}" 14 - ) pinnable; 15 - in 16 - "{\n" + lib.concatStringsSep "\n" lines + "\n} >> \"$QUEUE_FILE\""; 17 - 18 - tests.npins."pinnableInputs excludes empty-url entries" = { 19 - expr = pinnableInputs { 20 - foo.url = "github:owner/foo"; 21 - bar.url = ""; 22 - baz = { }; 23 - }; 24 - expected = { 25 - foo.url = "github:owner/foo"; 26 - }; 27 - }; 28 - 29 - tests.npins."pinnableInputs keeps all non-empty urls" = { 30 - expr = lib.attrNames (pinnableInputs { 31 - a.url = "github:a/a"; 32 - b.url = "github:b/b"; 33 - c.url = ""; 34 - }); 35 - expected = [ 36 - "a" 37 - "b" 38 - ]; 39 - }; 40 - 41 - tests.npins."pinnableInputs is empty on no-url inputs" = { 42 - expr = pinnableInputs { foo.follows = "bar"; }; 43 - expected = { }; 44 - }; 45 - 46 - tests.npins."queueSeed contains name and url for each pinnable input" = { 47 - expr = queueSeed { foo.url = "github:owner/foo"; }; 48 - expected = "{\n printf '%s\\t%s\\n' 'foo' 'github:owner/foo'\n} >> \"$QUEUE_FILE\""; 49 - }; 4 + subject = inputs: import ./../_lib/inputs-lib.nix lib esc inputs; 50 5 51 - tests.npins."queueSeed wraps all printfs in one redirect block" = { 6 + tests.npins."queueSeed wraps all entries in one redirect block" = { 52 7 expr = 53 8 let 54 - seed = queueSeed { 55 - a.url = "github:a/a"; 56 - b.url = "github:b/b"; 57 - }; 9 + seed = 10 + (subject { 11 + a.url = "github:a/a"; 12 + b.url = "github:b/b"; 13 + }).queueSeed; 58 14 in 59 15 lib.hasPrefix "{" seed && lib.hasSuffix ">> \"$QUEUE_FILE\"" seed; 60 16 expected = true; 61 17 }; 62 18 63 - tests.npins."queueSeed is empty block on no pinnable inputs" = { 64 - expr = queueSeed { }; 65 - expected = "{\n\n} >> \"$QUEUE_FILE\""; 19 + tests.npins."pinnableInputs is empty on no-url inputs" = { 20 + expr = (subject { foo.follows = "bar"; }).pinnableInputs; 21 + expected = { }; 66 22 }; 67 23 68 24 in
+380
dev/modules/unit-tests/parse-url.nix
··· 1 + { lib, ... }: 2 + let 3 + parse = import ./../_lib/parse-url.nix lib; 4 + p = url: parse url; 5 + 6 + # ── github ────────────────────────────────────────────────────────────────── 7 + tests.parse-url."github: basic" = { 8 + expr = p "github:NixOS/nixpkgs"; 9 + expected = { 10 + type = "github"; 11 + owner = "NixOS"; 12 + repo = "nixpkgs"; 13 + ref = ""; 14 + rev = ""; 15 + host = "github.com"; 16 + dir = ""; 17 + url = ""; 18 + }; 19 + }; 20 + 21 + tests.parse-url."github: branch ref" = { 22 + expr = p "github:NixOS/nixpkgs/nixos-23.11"; 23 + expected = { 24 + type = "github"; 25 + owner = "NixOS"; 26 + repo = "nixpkgs"; 27 + ref = "nixos-23.11"; 28 + rev = ""; 29 + host = "github.com"; 30 + dir = ""; 31 + url = ""; 32 + }; 33 + }; 34 + 35 + tests.parse-url."github: commit hash" = { 36 + expr = p "github:NixOS/nixpkgs/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293"; 37 + expected = { 38 + type = "github"; 39 + owner = "NixOS"; 40 + repo = "nixpkgs"; 41 + ref = "a3a3dda3bacf61e8a39258a0ed9c924eeca8e293"; 42 + rev = ""; 43 + host = "github.com"; 44 + dir = ""; 45 + url = ""; 46 + }; 47 + }; 48 + 49 + tests.parse-url."github: pull request ref" = { 50 + expr = p "github:NixOS/nixpkgs/pull/357207/head"; 51 + expected = { 52 + type = "github"; 53 + owner = "NixOS"; 54 + repo = "nixpkgs"; 55 + ref = "pull/357207/head"; 56 + rev = ""; 57 + host = "github.com"; 58 + dir = ""; 59 + url = ""; 60 + }; 61 + }; 62 + 63 + tests.parse-url."github: custom host" = { 64 + expr = p "github:internal/project?host=company-github.example.org"; 65 + expected = { 66 + type = "github"; 67 + owner = "internal"; 68 + repo = "project"; 69 + ref = ""; 70 + rev = ""; 71 + host = "company-github.example.org"; 72 + dir = ""; 73 + url = ""; 74 + }; 75 + }; 76 + 77 + tests.parse-url."github: dir parameter" = { 78 + expr = p "github:edolstra/nix-warez?dir=blender"; 79 + expected = { 80 + type = "github"; 81 + owner = "edolstra"; 82 + repo = "nix-warez"; 83 + ref = ""; 84 + rev = ""; 85 + host = "github.com"; 86 + dir = "blender"; 87 + url = ""; 88 + }; 89 + }; 90 + 91 + tests.parse-url."github: rev query param" = { 92 + expr = p "github:NixOS/nixpkgs?rev=a3a3dda3bacf61e8a39258a0ed9c924eeca8e293"; 93 + expected = { 94 + type = "github"; 95 + owner = "NixOS"; 96 + repo = "nixpkgs"; 97 + ref = ""; 98 + rev = "a3a3dda3bacf61e8a39258a0ed9c924eeca8e293"; 99 + host = "github.com"; 100 + dir = ""; 101 + url = ""; 102 + }; 103 + }; 104 + 105 + # ── gitlab ────────────────────────────────────────────────────────────────── 106 + tests.parse-url."gitlab: basic" = { 107 + expr = p "gitlab:veloren/veloren"; 108 + expected = { 109 + type = "gitlab"; 110 + owner = "veloren"; 111 + repo = "veloren"; 112 + ref = ""; 113 + rev = ""; 114 + host = "gitlab.com"; 115 + dir = ""; 116 + url = ""; 117 + }; 118 + }; 119 + 120 + tests.parse-url."gitlab: branch ref" = { 121 + expr = p "gitlab:veloren/veloren/master"; 122 + expected = { 123 + type = "gitlab"; 124 + owner = "veloren"; 125 + repo = "veloren"; 126 + ref = "master"; 127 + rev = ""; 128 + host = "gitlab.com"; 129 + dir = ""; 130 + url = ""; 131 + }; 132 + }; 133 + 134 + tests.parse-url."gitlab: commit hash" = { 135 + expr = p "gitlab:veloren/veloren/80a4d7f13492d916e47d6195be23acae8001985a"; 136 + expected = { 137 + type = "gitlab"; 138 + owner = "veloren"; 139 + repo = "veloren"; 140 + ref = "80a4d7f13492d916e47d6195be23acae8001985a"; 141 + rev = ""; 142 + host = "gitlab.com"; 143 + dir = ""; 144 + url = ""; 145 + }; 146 + }; 147 + 148 + tests.parse-url."gitlab: custom host" = { 149 + expr = p "gitlab:openldap/openldap?host=git.openldap.org"; 150 + expected = { 151 + type = "gitlab"; 152 + owner = "openldap"; 153 + repo = "openldap"; 154 + ref = ""; 155 + rev = ""; 156 + host = "git.openldap.org"; 157 + dir = ""; 158 + url = ""; 159 + }; 160 + }; 161 + 162 + tests.parse-url."gitlab: percent-encoded subgroup" = { 163 + expr = p "gitlab:veloren%2Fdev/rfcs"; 164 + expected = { 165 + type = "gitlab"; 166 + owner = "veloren/dev"; 167 + repo = "rfcs"; 168 + ref = ""; 169 + rev = ""; 170 + host = "gitlab.com"; 171 + dir = ""; 172 + url = ""; 173 + }; 174 + }; 175 + 176 + # ── sourcehut ─────────────────────────────────────────────────────────────── 177 + tests.parse-url."sourcehut: basic with tilde" = { 178 + expr = p "sourcehut:~misterio/nix-colors"; 179 + expected = { 180 + type = "sourcehut"; 181 + owner = "misterio"; 182 + repo = "nix-colors"; 183 + ref = ""; 184 + rev = ""; 185 + host = "git.sr.ht"; 186 + dir = ""; 187 + url = ""; 188 + }; 189 + }; 190 + 191 + tests.parse-url."sourcehut: branch ref" = { 192 + expr = p "sourcehut:~misterio/nix-colors/main"; 193 + expected = { 194 + type = "sourcehut"; 195 + owner = "misterio"; 196 + repo = "nix-colors"; 197 + ref = "main"; 198 + rev = ""; 199 + host = "git.sr.ht"; 200 + dir = ""; 201 + url = ""; 202 + }; 203 + }; 204 + 205 + tests.parse-url."sourcehut: custom host" = { 206 + expr = p "sourcehut:~misterio/nix-colors?host=git.example.org"; 207 + expected = { 208 + type = "sourcehut"; 209 + owner = "misterio"; 210 + repo = "nix-colors"; 211 + ref = ""; 212 + rev = ""; 213 + host = "git.example.org"; 214 + dir = ""; 215 + url = ""; 216 + }; 217 + }; 218 + 219 + tests.parse-url."sourcehut: commit hash" = { 220 + expr = p "sourcehut:~misterio/nix-colors/182b4b8709b8ffe4e9774a4c5d6877bf6bb9a21c"; 221 + expected = { 222 + type = "sourcehut"; 223 + owner = "misterio"; 224 + repo = "nix-colors"; 225 + ref = "182b4b8709b8ffe4e9774a4c5d6877bf6bb9a21c"; 226 + rev = ""; 227 + host = "git.sr.ht"; 228 + dir = ""; 229 + url = ""; 230 + }; 231 + }; 232 + 233 + tests.parse-url."sourcehut: mercurial host" = { 234 + expr = p "sourcehut:~misterio/nix-colors/21c1a380?host=hg.sr.ht"; 235 + expected = { 236 + type = "sourcehut"; 237 + owner = "misterio"; 238 + repo = "nix-colors"; 239 + ref = "21c1a380"; 240 + rev = ""; 241 + host = "hg.sr.ht"; 242 + dir = ""; 243 + url = ""; 244 + }; 245 + }; 246 + 247 + # ── git ───────────────────────────────────────────────────────────────────── 248 + tests.parse-url."git+https: basic" = { 249 + expr = (p "git+https://example.org/my/repo").type; 250 + expected = "git"; 251 + }; 252 + 253 + tests.parse-url."git+https: with ref" = { 254 + expr = p "git+https://example.org/my/repo?ref=master"; 255 + expected = { 256 + type = "git"; 257 + owner = ""; 258 + repo = ""; 259 + ref = "master"; 260 + rev = ""; 261 + host = ""; 262 + dir = ""; 263 + url = "git+https://example.org/my/repo"; 264 + }; 265 + }; 266 + 267 + tests.parse-url."git+https: with ref and rev" = { 268 + expr = p "git+https://example.org/my/repo?ref=master&rev=f34751b88bd07d7f44f5cd3200fb4122bf916c7e"; 269 + expected = { 270 + type = "git"; 271 + owner = ""; 272 + repo = ""; 273 + ref = "master"; 274 + rev = "f34751b88bd07d7f44f5cd3200fb4122bf916c7e"; 275 + host = ""; 276 + dir = ""; 277 + url = "git+https://example.org/my/repo"; 278 + }; 279 + }; 280 + 281 + tests.parse-url."git: bare scheme with ref and rev" = { 282 + expr = p "git://github.com/edolstra/dwarffs?ref=unstable&rev=e486d8d40e626a20e06d792db8cc5ac5aba9a5b4"; 283 + expected = { 284 + type = "git"; 285 + owner = ""; 286 + repo = ""; 287 + ref = "unstable"; 288 + rev = "e486d8d40e626a20e06d792db8cc5ac5aba9a5b4"; 289 + host = ""; 290 + dir = ""; 291 + url = "git://github.com/edolstra/dwarffs"; 292 + }; 293 + }; 294 + 295 + tests.parse-url."git+ssh: with ref" = { 296 + expr = p "git+ssh://git@github.com/NixOS/nix?ref=v1.2.3"; 297 + expected = { 298 + type = "git"; 299 + owner = ""; 300 + repo = ""; 301 + ref = "v1.2.3"; 302 + rev = ""; 303 + host = ""; 304 + dir = ""; 305 + url = "git+ssh://git@github.com/NixOS/nix"; 306 + }; 307 + }; 308 + 309 + tests.parse-url."git+file: local" = { 310 + expr = (p "git+file:///home/my-user/some-repo").type; 311 + expected = "git"; 312 + }; 313 + 314 + # ── tarball ───────────────────────────────────────────────────────────────── 315 + tests.parse-url."tarball: https scheme" = { 316 + expr = p "https://github.com/NixOS/patchelf/archive/master.tar.gz"; 317 + expected = { 318 + type = "tarball"; 319 + owner = ""; 320 + repo = ""; 321 + ref = ""; 322 + rev = ""; 323 + host = ""; 324 + dir = ""; 325 + url = "https://github.com/NixOS/patchelf/archive/master.tar.gz"; 326 + }; 327 + }; 328 + 329 + tests.parse-url."tarball+https: explicit scheme" = { 330 + expr = (p "tarball+https://example.org/repo.tar.gz").type; 331 + expected = "tarball"; 332 + }; 333 + 334 + tests.parse-url."tarball+https: strips prefix from url" = { 335 + expr = (p "tarball+https://example.org/repo.tar.gz").url; 336 + expected = "https://example.org/repo.tar.gz"; 337 + }; 338 + 339 + tests.parse-url."tarball: nixos channel url" = { 340 + expr = (p "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz").type; 341 + expected = "tarball"; 342 + }; 343 + 344 + tests.parse-url."tarball: zip extension" = { 345 + expr = (p "https://example.org/archive.zip").type; 346 + expected = "tarball"; 347 + }; 348 + 349 + # ── file ──────────────────────────────────────────────────────────────────── 350 + tests.parse-url."file+http: type" = { 351 + expr = (p "file+http://example.org/foo").type; 352 + expected = "file"; 353 + }; 354 + 355 + tests.parse-url."file+https: strips prefix" = { 356 + expr = (p "file+https://example.org/foo").url; 357 + expected = "https://example.org/foo"; 358 + }; 359 + 360 + # ── mercurial ─────────────────────────────────────────────────────────────── 361 + tests.parse-url."hg+https: type" = { 362 + expr = (p "hg+https://example.org/my/repo").type; 363 + expected = "mercurial"; 364 + }; 365 + 366 + tests.parse-url."hg+https: with ref" = { 367 + expr = (p "hg+https://example.org/my/repo?ref=default").ref; 368 + expected = "default"; 369 + }; 370 + 371 + # ── path ──────────────────────────────────────────────────────────────────── 372 + tests.parse-url."path: absolute" = { 373 + expr = (p "path:/home/user/sub/dir").type; 374 + expected = "path"; 375 + }; 376 + 377 + in 378 + { 379 + flake = { inherit tests; }; 380 + }
+6
modules/default.nix
··· 7 7 dendritic 8 8 import-tree 9 9 npins 10 + deps 10 11 unflake 11 12 flake-options 12 13 ; ··· 22 23 npins.imports = [ 23 24 base 24 25 ./npins.nix 26 + ]; 27 + 28 + deps.imports = [ 29 + base 30 + ./deps.nix 25 31 ]; 26 32 27 33 unflake.imports = [
+52
modules/deps.nix
··· 1 + { lib, config, ... }: 2 + let 3 + inherit (config) flake-file; 4 + inherit (import ./../dev/modules/_lib lib) inputsExpr; 5 + 6 + inputs = inputsExpr flake-file.inputs; 7 + esc = lib.escapeShellArg; 8 + 9 + inherit (import ./inputs-lib.nix lib esc inputs) 10 + followsSeed 11 + queueSeed 12 + ; 13 + 14 + write-deps = 15 + pkgs: 16 + let 17 + normalizeNix = pkgs.writeText "normalize.nix" (builtins.readFile ./deps/normalize.nix); 18 + in 19 + pkgs.writeShellApplication { 20 + name = "write-deps"; 21 + runtimeInputs = [ 22 + pkgs.curl 23 + pkgs.jq 24 + pkgs.nix 25 + pkgs.git 26 + ]; 27 + text = '' 28 + cd ${flake-file.intoPath} 29 + 30 + ${builtins.readFile ./deps/url.bash} 31 + ${builtins.readFile ./deps/fetch-forge.bash} 32 + ${builtins.readFile ./deps/fetch.bash} 33 + ${builtins.readFile ./deps/bfs.bash} 34 + 35 + FOLLOWS_FILE=$(mktemp) 36 + SEEN_FILE=$(mktemp) 37 + QUEUE_FILE=$(mktemp) 38 + DEPS_FILE=$(mktemp) 39 + NORMALIZE_NIX=${normalizeNix} 40 + trap 'rm -f "$FOLLOWS_FILE" "$SEEN_FILE" "$QUEUE_FILE" "$DEPS_FILE"' EXIT 41 + 42 + ${followsSeed} 43 + ${queueSeed} 44 + 45 + bfs_main 46 + write_deps_nix 47 + ''; 48 + }; 49 + in 50 + { 51 + config.flake-file.apps = { inherit write-deps; }; 52 + }
+36
modules/deps/bfs.bash
··· 1 + # BFS: consume QUEUE_FILE, fetch each new item, discover transitive deps. 2 + bfs_main() { 3 + log_info "starting BFS..." 4 + local count=0 5 + while true; do 6 + local name="" url="" 7 + while IFS=$'\t' read -r qname qurl; do 8 + if ! grep -qxF "$qname" "$SEEN_FILE" 2>/dev/null \ 9 + && ! grep -qxF "$qname" "$FOLLOWS_FILE" 2>/dev/null; then 10 + name="$qname" url="$qurl" 11 + break 12 + fi 13 + done < "$QUEUE_FILE" 14 + [ -z "$name" ] && break 15 + printf '%s\n' "$name" >> "$SEEN_FILE" 16 + local entry 17 + entry=$(fetch_entry "$name" "$url") \ 18 + && { printf '%s\n' "$entry" >> "$DEPS_FILE"; count=$((count + 1)); } \ 19 + || true 20 + discover_transitive "$name" "$url" 21 + done 22 + log_info "done: $count inputs fetched" 23 + } 24 + 25 + # Write deps.nix from accumulated DEPS_FILE lines. 26 + write_deps_nix() { 27 + { 28 + printf '# DO-NOT-EDIT: Generated by github:vic/flake-file\n' 29 + printf '# To re-generate: nix-shell . -A flake-file.sh --run write-deps\n' 30 + printf '# Usage: let deps = import ./deps.nix; in deps.nixpkgs.outPath\n' 31 + printf '{\n' 32 + cat "$DEPS_FILE" 33 + printf '}\n' 34 + } > deps.nix 35 + log_info "wrote deps.nix" 36 + }
+74
modules/deps/fetch-forge.bash
··· 1 + fetch_github() { 2 + local name="$1" raw="$2" 3 + local body host dir rev ref owner repo sha256 outpath_nix 4 + body="$(url_base "${raw#github:}")" 5 + host="$(url_param host "$raw")"; host="${host:-github.com}" 6 + dir="$(url_param dir "$raw")" 7 + rev="$(url_param rev "$raw")" 8 + owner="${body%%/*}"; body="${body#*/}" 9 + repo="${body%%/*}"; ref="${body#"$repo"}"; ref="${ref#/}" 10 + [ -n "$rev" ] || rev=$(git_rev "https://${host}/${owner}/${repo}.git" "${ref:-HEAD}") 11 + local archive="https://${host}/${owner}/${repo}/archive/${rev}.tar.gz" 12 + log_info "fetching ${name} (github ${owner}/${repo} @ ${rev:0:12})" 13 + sha256=$(prefetch_tarball "$archive") || { log_info "warning: failed $name"; return 1; } 14 + if [ -n "$dir" ]; then 15 + outpath_nix="(builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }) + \"/$dir\"" 16 + else 17 + outpath_nix="builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }" 18 + fi 19 + emit_record "$name" "$outpath_nix" \ 20 + "type=github" "owner=$owner" "repo=$repo" "rev=$rev" "ref=$ref" "host=$host" "dir=$dir" 21 + } 22 + 23 + fetch_gitlab() { 24 + local name="$1" raw="$2" 25 + local body host dir rev ref owner repo sha256 archive 26 + body="$(url_base "${raw#gitlab:}")" 27 + host="$(url_param host "$raw")"; host="${host:-gitlab.com}" 28 + dir="$(url_param dir "$raw")" 29 + rev="$(url_param rev "$raw")" 30 + owner="$(url_decode "${body%%/*}")"; body="${body#*/}" 31 + repo="${body%%/*}"; ref="${body#"$repo"}"; ref="${ref#/}" 32 + [ -n "$rev" ] || rev=$(git_rev "https://${host}/${owner}/${repo}.git" "${ref:-HEAD}") 33 + archive="https://${host}/${owner}/${repo}/-/archive/${rev}/${repo}-${rev}.tar.gz" 34 + log_info "fetching ${name} (gitlab ${owner}/${repo} @ ${rev:0:12})" 35 + local sha256 36 + sha256=$(prefetch_tarball "$archive") || { log_info "warning: failed $name"; return 1; } 37 + emit_record "$name" "builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }" \ 38 + "type=gitlab" "owner=$owner" "repo=$repo" "rev=$rev" "ref=$ref" "host=$host" 39 + } 40 + 41 + fetch_sourcehut_hg() { 42 + local name="$1" raw="$2" 43 + local body host rev owner repo ref remote sha256 archive 44 + host="$(url_param host "$raw")"; host="${host:-hg.sr.ht}" 45 + body="$(url_base "${raw#sourcehut:}")" 46 + rev="$(url_param rev "$raw")" 47 + owner="${body%%/*}"; owner="${owner#\~}" 48 + body="${body#*/}"; repo="${body%%/*}"; ref="${body#"$repo"}"; ref="${ref#/}" 49 + remote="https://${host}/~${owner}/${repo}" 50 + [ -n "$rev" ] || rev=$(curl -sfL "${remote}/log/${ref:-tip}/rss" \ 51 + | grep -m1 '<guid>' | sed 's|.*<guid>.*rev=\([^<]*\).*|\1|') 52 + archive="${remote}/archive/${rev}.tar.gz" 53 + log_info "fetching ${name} (sourcehut-hg ${owner}/${repo} @ ${rev:0:12})" 54 + sha256=$(prefetch_tarball "$archive") || { log_info "warning: failed $name"; return 1; } 55 + emit_record "$name" "builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }" \ 56 + "type=sourcehut" "owner=$owner" "repo=$repo" "rev=$rev" "ref=$ref" "host=$host" 57 + } 58 + 59 + fetch_sourcehut() { 60 + local name="$1" raw="$2" 61 + local body host ref rev owner repo remote sha256 archive 62 + host="$(url_param host "$raw")"; host="${host:-git.sr.ht}" 63 + body="$(url_base "${raw#sourcehut:}")" 64 + rev="$(url_param rev "$raw")" 65 + owner="${body%%/*}"; owner="${owner#\~}" 66 + body="${body#*/}"; repo="${body%%/*}"; ref="${body#"$repo"}"; ref="${ref#/}" 67 + remote="https://${host}/~${owner}/${repo}" 68 + [ -n "$rev" ] || rev=$(git_rev "${remote}" "${ref:-HEAD}") 69 + archive="${remote}/archive/${rev}.tar.gz" 70 + log_info "fetching ${name} (sourcehut ${owner}/${repo} @ ${rev:0:12})" 71 + sha256=$(prefetch_tarball "$archive") || { log_info "warning: failed $name"; return 1; } 72 + emit_record "$name" "builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }" \ 73 + "type=sourcehut" "owner=$owner" "repo=$repo" "rev=$rev" "ref=$ref" "host=$host" 74 + }
+81
modules/deps/fetch.bash
··· 1 + fetch_git_url() { 2 + local name="$1" raw="$2" 3 + local url ref rev 4 + url="$(url_base "$raw")"; url="${url#git+}" 5 + ref="$(url_param ref "$raw")" 6 + rev="$(url_param rev "$raw")" 7 + [ -n "$rev" ] || rev=$(git_rev "$url" "${ref:-HEAD}") 8 + log_info "fetching ${name} (git ${url} @ ${rev:0:12})" 9 + emit_record "$name" "builtins.fetchGit { url = \"$url\"; rev = \"$rev\"; }" \ 10 + "type=git" "url=$url" "rev=$rev" "ref=$ref" 11 + } 12 + 13 + fetch_tarball_url() { 14 + local name="$1" raw="$2" url sha256 15 + url="$(url_base "$raw")"; url="${url#tarball+}" 16 + log_info "fetching ${name} (tarball)" 17 + sha256=$(prefetch_tarball "$url") || { log_info "warning: failed $name"; return 1; } 18 + emit_record "$name" "builtins.fetchTarball { url = \"$url\"; sha256 = \"$sha256\"; }" \ 19 + "type=tarball" "url=$url" 20 + } 21 + 22 + fetch_file_url() { 23 + local name="$1" raw="$2" url sha256 24 + url="$(url_base "$raw")"; url="${url#file+}" 25 + log_info "fetching ${name} (file)" 26 + sha256=$(nix-prefetch-url "$url" 2>/dev/null) || { log_info "warning: failed $name"; return 1; } 27 + emit_record "$name" "builtins.fetchurl { url = \"$url\"; sha256 = \"$sha256\"; }" \ 28 + "type=file" "url=$url" 29 + } 30 + 31 + fetch_mercurial() { 32 + local name="$1" raw="$2" url ref rev sha256 archive 33 + url="$(url_base "$raw")"; url="${url#hg+}" 34 + ref="$(url_param ref "$raw")" 35 + rev="$(url_param rev "$raw")" 36 + [ -n "$rev" ] || rev=$(curl -sfL "${url}/log/${ref:-tip}/rss" \ 37 + | grep -m1 '<guid>' | sed 's|.*<guid>.*rev=\([^<]*\).*|\1|') 38 + archive="${url}/archive/${rev}.tar.gz" 39 + log_info "fetching ${name} (mercurial @ ${rev:0:12})" 40 + sha256=$(prefetch_tarball "$archive") || { log_info "warning: failed $name"; return 1; } 41 + emit_record "$name" "builtins.fetchTarball { url = \"$archive\"; sha256 = \"$sha256\"; }" \ 42 + "type=mercurial" "url=$url" "rev=$rev" "ref=$ref" 43 + } 44 + 45 + fetch_entry() { 46 + local name="$1" url="$2" 47 + case "$url" in 48 + github:*) fetch_github "$name" "$url" ;; 49 + gitlab:*) fetch_gitlab "$name" "$url" ;; 50 + sourcehut:*hg.sr.ht*) fetch_sourcehut_hg "$name" "$url" ;; 51 + sourcehut:*) fetch_sourcehut "$name" "$url" ;; 52 + git+http://*|git+https://*|git+ssh://*|git+git://*|git+file://*) 53 + fetch_git_url "$name" "$url" ;; 54 + git://*) fetch_git_url "$name" "git+${url}" ;; 55 + hg+*) fetch_mercurial "$name" "$url" ;; 56 + tarball+*) fetch_tarball_url "$name" "$url" ;; 57 + file+*) fetch_file_url "$name" "$url" ;; 58 + *.tar.gz|*.tar.bz2|*.tar.xz|*.tar.zst|*.zip|http://*|https://*) 59 + fetch_tarball_url "$name" "$url" ;; 60 + path:*) log_info "skipping ${name} (local path)" ;; 61 + *) log_info "skipping ${name} (indirect/unknown: $url)" ;; 62 + esac 63 + } 64 + 65 + discover_transitive() { 66 + local name="$1" url="$2" 67 + [[ "$url" != github:* ]] && return 0 68 + local spec owner repo ref flake_tmp expr_tmp 69 + spec="${url#github:}" owner="${spec%%/*}" spec="${spec#*/}" 70 + repo="${spec%%/*}" ref="${spec#*/}" 71 + [ "$ref" = "$repo" ] && ref="HEAD" 72 + flake_tmp=$(mktemp --suffix=.nix) 73 + expr_tmp=$(mktemp --suffix=.nix) 74 + curl -sf "https://raw.githubusercontent.com/${owner}/${repo}/${ref}/flake.nix" \ 75 + > "$flake_tmp" || { rm -f "$flake_tmp" "$expr_tmp"; return 0; } 76 + printf '(import %s) %s\n' "$NORMALIZE_NIX" "$flake_tmp" > "$expr_tmp" 77 + nix-instantiate --eval --strict --json "$expr_tmp" 2>/dev/null \ 78 + | jq -r 'to_entries[] | select(.value | . != "" and test("^[a-z]")) | [.key, .value] | @tsv' \ 79 + >> "$QUEUE_FILE" || true 80 + rm -f "$flake_tmp" "$expr_tmp" 81 + }
+66
modules/deps/normalize.nix
··· 1 + flakeFile: 2 + let 3 + f = import flakeFile; 4 + 5 + withRef = 6 + owner: repo: ref: 7 + "${owner}/${repo}${if ref != "" then "/" + ref else ""}"; 8 + withHost = host: def: if host != "" && host != def then "?host=${host}" else ""; 9 + 10 + refOf = v: if v ? rev then v.rev else (v.ref or ""); 11 + 12 + github = v: "github:${withRef v.owner v.repo (refOf v)}${withHost (v.host or "") "github.com"}"; 13 + gitlab = v: "gitlab:${withRef v.owner v.repo (refOf v)}${withHost (v.host or "") "gitlab.com"}"; 14 + sourcehut = 15 + v: "sourcehut:~${withRef v.owner v.repo (refOf v)}${withHost (v.host or "") "git.sr.ht"}"; 16 + 17 + gitParams = 18 + v: 19 + let 20 + ref = if v ? ref then "ref=${v.ref}" else ""; 21 + rev = if v ? rev then "rev=${v.rev}" else ""; 22 + params = builtins.filter (s: s != "") [ 23 + ref 24 + rev 25 + ]; 26 + in 27 + if params == [ ] then "" else "?" + builtins.concatStringsSep "&" params; 28 + 29 + fromAttrs = 30 + v: 31 + if !(v ? type) then 32 + (v.url or "") 33 + else if v.type == "github" then 34 + github v 35 + else if v.type == "gitlab" then 36 + gitlab v 37 + else if v.type == "sourcehut" then 38 + sourcehut v 39 + else if v.type == "git" then 40 + "${v.url or ""}${gitParams v}" 41 + else if v.type == "tarball" then 42 + v.url or "" 43 + else if v.type == "file" then 44 + v.url or "" 45 + else if v.type == "path" then 46 + "path:${v.path or ""}" 47 + else if v.type == "indirect" then 48 + v.id or "" 49 + else if v.type == "mercurial" then 50 + v.url or "" 51 + else 52 + ""; 53 + 54 + norm = 55 + v: 56 + if builtins.isString v then 57 + v 58 + else if v ? url then 59 + v.url 60 + else if v ? follows then 61 + "" 62 + else 63 + fromAttrs v; 64 + 65 + in 66 + builtins.mapAttrs (_: norm) (f.inputs or { })
+40
modules/deps/url.bash
··· 1 + # Extract a query parameter value from a URL: url_param KEY URL 2 + url_param() { 3 + local key="$1" url="$2" qs 4 + qs="${url#*\?}" 5 + [ "$qs" = "$url" ] && { printf ''; return; } 6 + printf '%s' "$qs" | tr '&' '\n' | grep "^${key}=" | cut -d= -f2- | head -1 7 + } 8 + 9 + url_base() { printf '%s' "${1%%\?*}"; } 10 + url_decode() { printf '%s' "$1" | sed 's/%2[Ff]/\//g'; } 11 + 12 + # Resolve a git ref to exact commit SHA (deref tags, fallback to ref as-is). 13 + git_rev() { 14 + local remote="$1" ref="$2" resolved deref 15 + resolved=$(git ls-remote "$remote" \ 16 + "refs/heads/$ref" "refs/tags/$ref" "HEAD" 2>/dev/null \ 17 + | grep -v '\^{}' | awk 'NR==1{print $1; exit}') 18 + deref=$(git ls-remote "$remote" "refs/tags/$ref^{}" 2>/dev/null | awk '{print $1; exit}') 19 + printf '%s' "${deref:-${resolved:-$ref}}" 20 + } 21 + 22 + # Fetch sha256 for a tarball URL (nix-prefetch-url --unpack). 23 + prefetch_tarball() { nix-prefetch-url --unpack "$1" 2>/dev/null; } 24 + 25 + # Progress output to stderr. 26 + log_info() { printf '[deps] %s\n' "$*" >&2; } 27 + 28 + # Emit a rich Nix attrset record. 29 + # Usage: emit_record NAME OUTPATH_NIX_EXPR [key=value ...] 30 + emit_record() { 31 + local name="$1" outpath="$2" 32 + shift 2 33 + printf ' %s = {\n outPath = %s;\n' "$name" "$outpath" 34 + local kv k v 35 + for kv in "$@"; do 36 + k="${kv%%=*}" v="${kv#*=}" 37 + [ -n "$v" ] && printf ' %s = "%s";\n' "$k" "$v" 38 + done 39 + printf ' };\n' 40 + }
+1
modules/inputs-lib.nix
··· 1 + import ./../dev/modules/_lib/inputs-lib.nix
-13
modules/npins-transitive-test.nix
··· 1 - { lib, ... }: 2 - { 3 - # Declares neomacs with nixpkgs-follows so nixpkgs is not re-pinned separately. 4 - # Transitive discovery must find crane, rust-overlay, nix-wpe-webkit 5 - # from neomacs's own flake.nix at runtime. 6 - flake-file.inputs = { 7 - nixpkgs.url = lib.mkDefault "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; 8 - neomacs = { 9 - url = "github:eval-exec/neomacs"; 10 - inputs.nixpkgs.follows = "nixpkgs"; 11 - }; 12 - }; 13 - }
+16 -13
modules/npins.nix
··· 6 6 inputs = inputsExpr flake-file.inputs; 7 7 esc = lib.escapeShellArg; 8 8 9 - pinnableInputs = lib.filterAttrs (_: v: v.url or "" != "") inputs; 10 - 11 - # Seed the runtime queue with one tab-separated "name\turl" line per declared input. 12 - queueSeed = 13 - let 14 - lines = lib.mapAttrsToList ( 15 - name: input: " printf '%s\\t%s\\n' ${esc name} ${esc (input.url or "")}" 16 - ) pinnableInputs; 17 - in 18 - "{\n" + lib.concatStringsSep "\n" lines + "\n} >> \"$QUEUE_FILE\""; 9 + inherit (import ./inputs-lib.nix lib esc inputs) 10 + followsSeed 11 + queueSeed 12 + ; 19 13 20 14 write-npins = 21 15 pkgs: ··· 31 25 cd ${flake-file.intoPath} 32 26 npins init --bare 2>/dev/null || true 33 27 28 + FOLLOWS_FILE=$(mktemp) 34 29 SEEN_FILE=$(mktemp) 35 30 QUEUE_FILE=$(mktemp) 36 - trap 'rm -f "$SEEN_FILE" "$QUEUE_FILE"' EXIT 31 + trap 'rm -f "$FOLLOWS_FILE" "$SEEN_FILE" "$QUEUE_FILE"' EXIT 37 32 38 33 # Add a pin by its flake-style URL (github:o/r, gitlab:o/r, channel URL, etc.) 39 34 npins_add_url() { ··· 43 38 spec="''${url#github:}" owner="''${spec%%/*}" spec="''${spec#*/}" 44 39 repo="''${spec%%/*}" ref="''${spec#*/}" 45 40 if [ "$ref" != "$repo" ]; then 46 - npins add github --name "$name" -b "$ref" "$owner" "$repo" 41 + # Explicit ref: try as branch, then as release tag, then common branches. 42 + npins add github --name "$name" -b "$ref" "$owner" "$repo" 2>/dev/null \ 43 + || npins add github --name "$name" "$owner" "$repo" 2>/dev/null \ 44 + || npins add github --name "$name" -b main "$owner" "$repo" 2>/dev/null \ 45 + || npins add github --name "$name" -b master "$owner" "$repo" 47 46 else 48 47 # No explicit ref: prefer a release tag, fall back to common branches. 49 48 npins add github --name "$name" "$owner" "$repo" 2>/dev/null \ ··· 96 95 rm -f "$flake_tmp" "$nix_tmp" 97 96 } 98 97 98 + # Pre-mark follows-only inputs so BFS never enqueues them. 99 + ${followsSeed} 100 + 99 101 # Seed the BFS queue with all declared inputs. 100 102 ${queueSeed} 101 103 ··· 103 105 while true; do 104 106 name="" url="" 105 107 while IFS=$'\t' read -r qname qurl; do 106 - if ! grep -qxF "$qname" "$SEEN_FILE" 2>/dev/null; then 108 + if ! grep -qxF "$qname" "$SEEN_FILE" 2>/dev/null && \ 109 + ! grep -qxF "$qname" "$FOLLOWS_FILE" 2>/dev/null; then 107 110 name="$qname" url="$qurl" 108 111 break 109 112 fi