tangled
alpha
login
or
join now
oeiuwq.com
/
flake-file
5
fork
atom
Generate flake.nix from module options. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/
dendrix.oeiuwq.com/Dendritic.html
dendritic
nix
inputs
5
fork
atom
overview
issues
1
pulls
pipelines
npins (#71)
test npins
authored by
oeiuwq.com
and committed by
GitHub
3 weeks ago
c9d91bd7
8a118dae
1/1
mirror.yml
success
3s
+217
-124
4 changed files
expand all
collapse all
unified
split
.github
workflows
flake-check.yaml
dev
modules
unit-tests
npins.nix
modules
npins-transitive-test.nix
npins.nix
+37
.github/workflows/flake-check.yaml
···
39
39
if: ${{ matrix.bootstrap == 'unflake' }}
40
40
- run: cat bootstrap/npins/sources.json
41
41
if: ${{ matrix.bootstrap == 'npins' }}
42
42
+
- name: Assert bootstrap npins transitive discovery (flake-parts -> nixpkgs-lib)
43
43
+
if: ${{ matrix.bootstrap == 'npins' }}
44
44
+
run: |
45
45
+
jq -e '.pins | has("flake-parts")' bootstrap/npins/sources.json
46
46
+
jq -e '.pins | has("nixpkgs-lib")' bootstrap/npins/sources.json
42
47
template:
43
48
name: Check template ${{matrix.template}}
44
49
needs: [find-templates]
···
84
89
sed -i 's/# flake-file = import/flake-file = import/' default.nix
85
90
echo "{ inputs, ... }: { npins.pkgs = import inputs.nixpkgs {}; }" | tee modules/pkgs.nix
86
91
nix-shell . -A npins.env --run write-npins
92
92
+
npins-transitive:
93
93
+
name: Check npins transitive discovery
94
94
+
runs-on: ubuntu-latest
95
95
+
env:
96
96
+
NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz"
97
97
+
steps:
98
98
+
- uses: actions/checkout@v4
99
99
+
- uses: wimpysworld/nothing-but-nix@main
100
100
+
- uses: cachix/install-nix-action@v31
101
101
+
- uses: DeterminateSystems/magic-nix-cache-action@main
102
102
+
- name: Run write-npins with neomacs (has transitive deps)
103
103
+
run: |
104
104
+
mkdir test-out
105
105
+
nix-shell ./default.nix -A flake-file.sh \
106
106
+
--run write-npins \
107
107
+
--arg modules ./modules/npins-transitive-test.nix \
108
108
+
--argstr outdir test-out
109
109
+
- name: Assert declared inputs are pinned
110
110
+
run: |
111
111
+
jq -e '.pins | has("neomacs")' test-out/npins/sources.json
112
112
+
jq -e '.pins | has("nixpkgs")' test-out/npins/sources.json
113
113
+
- name: Assert transitive deps are auto-discovered
114
114
+
run: |
115
115
+
jq -e '.pins | has("crane")' test-out/npins/sources.json
116
116
+
jq -e '.pins | has("rust-overlay")' test-out/npins/sources.json
117
117
+
jq -e '.pins | has("nix-wpe-webkit")' test-out/npins/sources.json
118
118
+
- name: Assert dedup-by-name (nixpkgs stays as Channel, not re-pinned from neomacs dep)
119
119
+
run: |
120
120
+
jq -e '.pins.nixpkgs.type == "Channel"' test-out/npins/sources.json
121
121
+
- name: Show pinned sources
122
122
+
if: always()
123
123
+
run: cat test-out/npins/sources.json
87
124
unflake:
88
125
name: Check unflake
89
126
runs-on: ubuntu-latest
+71
dev/modules/unit-tests/npins.nix
···
1
1
+
{ lib, ... }:
2
2
+
let
3
3
+
esc = lib.escapeShellArg;
4
4
+
5
5
+
# Mirrors pinnableInputs from modules/npins.nix
6
6
+
pinnableInputs = inputs: lib.filterAttrs (_: v: v.url or "" != "") inputs;
7
7
+
8
8
+
# Mirrors queueSeed from modules/npins.nix
9
9
+
queueSeed =
10
10
+
pinnable:
11
11
+
let
12
12
+
lines = lib.mapAttrsToList (
13
13
+
name: input: " printf '%s\\t%s\\n' ${esc name} ${esc (input.url or "")}"
14
14
+
) pinnable;
15
15
+
in
16
16
+
"{\n" + lib.concatStringsSep "\n" lines + "\n} >> \"$QUEUE_FILE\"";
17
17
+
18
18
+
tests.npins."pinnableInputs excludes empty-url entries" = {
19
19
+
expr = pinnableInputs {
20
20
+
foo.url = "github:owner/foo";
21
21
+
bar.url = "";
22
22
+
baz = { };
23
23
+
};
24
24
+
expected = {
25
25
+
foo.url = "github:owner/foo";
26
26
+
};
27
27
+
};
28
28
+
29
29
+
tests.npins."pinnableInputs keeps all non-empty urls" = {
30
30
+
expr = lib.attrNames (pinnableInputs {
31
31
+
a.url = "github:a/a";
32
32
+
b.url = "github:b/b";
33
33
+
c.url = "";
34
34
+
});
35
35
+
expected = [
36
36
+
"a"
37
37
+
"b"
38
38
+
];
39
39
+
};
40
40
+
41
41
+
tests.npins."pinnableInputs is empty on no-url inputs" = {
42
42
+
expr = pinnableInputs { foo.follows = "bar"; };
43
43
+
expected = { };
44
44
+
};
45
45
+
46
46
+
tests.npins."queueSeed contains name and url for each pinnable input" = {
47
47
+
expr = queueSeed { foo.url = "github:owner/foo"; };
48
48
+
expected = "{\n printf '%s\\t%s\\n' 'foo' 'github:owner/foo'\n} >> \"$QUEUE_FILE\"";
49
49
+
};
50
50
+
51
51
+
tests.npins."queueSeed wraps all printfs in one redirect block" = {
52
52
+
expr =
53
53
+
let
54
54
+
seed = queueSeed {
55
55
+
a.url = "github:a/a";
56
56
+
b.url = "github:b/b";
57
57
+
};
58
58
+
in
59
59
+
lib.hasPrefix "{" seed && lib.hasSuffix ">> \"$QUEUE_FILE\"" seed;
60
60
+
expected = true;
61
61
+
};
62
62
+
63
63
+
tests.npins."queueSeed is empty block on no pinnable inputs" = {
64
64
+
expr = queueSeed { };
65
65
+
expected = "{\n\n} >> \"$QUEUE_FILE\"";
66
66
+
};
67
67
+
68
68
+
in
69
69
+
{
70
70
+
flake = { inherit tests; };
71
71
+
}
+13
modules/npins-transitive-test.nix
···
1
1
+
{ lib, ... }:
2
2
+
{
3
3
+
# Declares neomacs with nixpkgs-follows so nixpkgs is not re-pinned separately.
4
4
+
# Transitive discovery must find crane, rust-overlay, nix-wpe-webkit
5
5
+
# from neomacs's own flake.nix at runtime.
6
6
+
flake-file.inputs = {
7
7
+
nixpkgs.url = lib.mkDefault "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
8
8
+
neomacs = {
9
9
+
url = "github:eval-exec/neomacs";
10
10
+
inputs.nixpkgs.follows = "nixpkgs";
11
11
+
};
12
12
+
};
13
13
+
}
+96
-124
modules/npins.nix
···
1
1
{ lib, config, ... }:
2
2
let
3
3
inherit (config) flake-file;
4
4
-
5
4
inherit (import ./../dev/modules/_lib lib) inputsExpr;
6
5
7
6
inputs = inputsExpr flake-file.inputs;
8
8
-
9
9
-
parseFlakeUrl =
10
10
-
url:
11
11
-
let
12
12
-
parts = lib.splitString ":" url;
13
13
-
scheme = lib.head parts;
14
14
-
rest = lib.concatStringsSep ":" (lib.tail parts);
15
15
-
in
16
16
-
if scheme == "github" then
17
17
-
parseGithub rest
18
18
-
else if scheme == "gitlab" then
19
19
-
parseGitlab rest
20
20
-
else if isChannelUrl url then
21
21
-
parseChannel url
22
22
-
else if lib.hasPrefix "https://" url || lib.hasPrefix "http://" url then
23
23
-
{
24
24
-
type = "tarball";
25
25
-
inherit url;
26
26
-
}
27
27
-
else
28
28
-
{
29
29
-
type = "git";
30
30
-
inherit url;
31
31
-
};
32
32
-
33
33
-
parseGithub =
34
34
-
rest:
35
35
-
let
36
36
-
segments = lib.splitString "/" rest;
37
37
-
owner = lib.elemAt segments 0;
38
38
-
repo = lib.elemAt segments 1;
39
39
-
ref = if lib.length segments > 2 then lib.elemAt segments 2 else null;
40
40
-
in
41
41
-
{
42
42
-
type = "github";
43
43
-
inherit owner repo ref;
44
44
-
};
45
45
-
46
46
-
parseGitlab =
47
47
-
rest:
48
48
-
let
49
49
-
segments = lib.splitString "/" rest;
50
50
-
owner = lib.elemAt segments 0;
51
51
-
repo = lib.elemAt segments 1;
52
52
-
ref = if lib.length segments > 2 then lib.elemAt segments 2 else null;
53
53
-
in
54
54
-
{
55
55
-
type = "gitlab";
56
56
-
inherit owner repo ref;
57
57
-
};
58
58
-
59
59
-
isChannelUrl =
60
60
-
url:
61
61
-
lib.hasPrefix "https://channels.nixos.org/" url || lib.hasPrefix "https://releases.nixos.org/" url;
62
62
-
63
63
-
parseChannel =
64
64
-
url:
65
65
-
let
66
66
-
path = lib.removePrefix "https://channels.nixos.org/" url;
67
67
-
channel = lib.head (lib.splitString "/" path);
68
68
-
in
69
69
-
{
70
70
-
type = "channel";
71
71
-
inherit channel;
72
72
-
};
73
73
-
74
74
-
branchFlag =
75
75
-
parsed:
76
76
-
if parsed ? ref && parsed.ref != null then " -b ${lib.escapeShellArg parsed.ref}" else " -b main";
77
77
-
78
7
esc = lib.escapeShellArg;
79
8
80
80
-
npinsAddCmd =
81
81
-
name: parsed:
82
82
-
if parsed.type == "github" then
83
83
-
"npins add github --name ${esc name}${branchFlag parsed} ${esc parsed.owner} ${esc parsed.repo}"
84
84
-
else if parsed.type == "gitlab" then
85
85
-
"npins add gitlab --name ${esc name}${branchFlag parsed} ${esc parsed.owner} ${esc parsed.repo}"
86
86
-
else if parsed.type == "channel" then
87
87
-
"npins add channel --name ${esc name} ${esc parsed.channel}"
88
88
-
else if parsed.type == "tarball" then
89
89
-
"npins add tarball --name ${esc name} ${esc parsed.url}"
90
90
-
else
91
91
-
"npins add git --name ${esc name} ${esc parsed.url}";
92
92
-
93
93
-
hasFollows = sub: sub ? follows && sub.follows != null;
94
94
-
95
95
-
transitiveInputs =
96
96
-
name: input:
97
97
-
let
98
98
-
subs = input.inputs or { };
99
99
-
nonFollowed = lib.filterAttrs (_: sub: !(hasFollows sub)) subs;
100
100
-
in
101
101
-
lib.mapAttrs' (sub: _: lib.nameValuePair "${name}/${sub}" sub) nonFollowed;
102
102
-
103
103
-
collectTransitive = lib.foldlAttrs (
104
104
-
acc: name: input:
105
105
-
acc // (transitiveInputs name input)
106
106
-
) { };
107
107
-
108
9
pinnableInputs = lib.filterAttrs (_: v: v.url or "" != "") inputs;
109
10
110
110
-
allTransitive = collectTransitive inputs;
111
111
-
112
112
-
pinnableTransitive = lib.filterAttrs (_: v: v.url or "" != "") allTransitive;
113
113
-
114
114
-
allPins = pinnableInputs // pinnableTransitive;
115
115
-
116
116
-
addIfMissing =
117
117
-
name: input:
11
11
+
# Seed the runtime queue with one tab-separated "name\turl" line per declared input.
12
12
+
queueSeed =
118
13
let
119
119
-
cmd = npinsAddCmd name (parseFlakeUrl (input.url or ""));
14
14
+
lines = lib.mapAttrsToList (
15
15
+
name: input: " printf '%s\\t%s\\n' ${esc name} ${esc (input.url or "")}"
16
16
+
) pinnableInputs;
120
17
in
121
121
-
''
122
122
-
if ! jq -e --arg n ${esc name} '.pins | has($n)' npins/sources.json >/dev/null 2>&1; then
123
123
-
${cmd} || (printf "\ncommand FAILED:\n ${cmd}" >&2 && exit 1)
124
124
-
fi
125
125
-
'';
126
126
-
127
127
-
addCommands = lib.concatStringsSep "\n" (lib.mapAttrsToList addIfMissing allPins);
128
128
-
129
129
-
pinNames = lib.concatStringsSep " " (lib.attrNames allPins);
18
18
+
"{\n" + lib.concatStringsSep "\n" lines + "\n} >> \"$QUEUE_FILE\"";
130
19
131
20
write-npins =
132
21
pkgs:
···
135
24
runtimeInputs = [
136
25
pkgs.npins
137
26
pkgs.jq
27
27
+
pkgs.curl
28
28
+
pkgs.nix
138
29
];
139
30
text = ''
140
31
cd ${flake-file.intoPath}
141
32
npins init --bare 2>/dev/null || true
142
142
-
${addCommands}
143
143
-
wanted="${pinNames}"
33
33
+
34
34
+
SEEN_FILE=$(mktemp)
35
35
+
QUEUE_FILE=$(mktemp)
36
36
+
trap 'rm -f "$SEEN_FILE" "$QUEUE_FILE"' EXIT
37
37
+
38
38
+
# Add a pin by its flake-style URL (github:o/r, gitlab:o/r, channel URL, etc.)
39
39
+
npins_add_url() {
40
40
+
local name="$1" url="$2" spec owner repo ref channel
41
41
+
case "$url" in
42
42
+
github:*)
43
43
+
spec="''${url#github:}" owner="''${spec%%/*}" spec="''${spec#*/}"
44
44
+
repo="''${spec%%/*}" ref="''${spec#*/}"
45
45
+
if [ "$ref" != "$repo" ]; then
46
46
+
npins add github --name "$name" -b "$ref" "$owner" "$repo"
47
47
+
else
48
48
+
# No explicit ref: prefer a release tag, fall back to common branches.
49
49
+
npins add github --name "$name" "$owner" "$repo" 2>/dev/null \
50
50
+
|| npins add github --name "$name" -b main "$owner" "$repo" 2>/dev/null \
51
51
+
|| npins add github --name "$name" -b master "$owner" "$repo"
52
52
+
fi ;;
53
53
+
gitlab:*)
54
54
+
spec="''${url#gitlab:}" owner="''${spec%%/*}" spec="''${spec#*/}"
55
55
+
repo="''${spec%%/*}" ref="''${spec#*/}"
56
56
+
if [ "$ref" != "$repo" ]; then
57
57
+
npins add gitlab --name "$name" -b "$ref" "$owner" "$repo"
58
58
+
else
59
59
+
npins add gitlab --name "$name" "$owner" "$repo" 2>/dev/null \
60
60
+
|| npins add gitlab --name "$name" -b main "$owner" "$repo" 2>/dev/null \
61
61
+
|| npins add gitlab --name "$name" -b master "$owner" "$repo"
62
62
+
fi ;;
63
63
+
https://channels.nixos.org/*|https://releases.nixos.org/*)
64
64
+
channel=$(printf '%s' "$url" | sed 's|https://[^/]*/||;s|/.*||')
65
65
+
npins add channel --name "$name" "$channel" ;;
66
66
+
https://*|http://*)
67
67
+
npins add tarball --name "$name" "$url" ;;
68
68
+
*)
69
69
+
npins add git --name "$name" "$url" ;;
70
70
+
esac
71
71
+
}
72
72
+
73
73
+
# Fetch a github input's flake.nix and append its deps to QUEUE_FILE.
74
74
+
discover_transitive() {
75
75
+
local name="$1" url="$2" spec owner repo ref
76
76
+
[[ "$url" != github:* ]] && return 0
77
77
+
spec="''${url#github:}" owner="''${spec%%/*}" spec="''${spec#*/}"
78
78
+
repo="''${spec%%/*}" ref="''${spec#*/}"
79
79
+
[ "$ref" = "$repo" ] && ref="HEAD"
80
80
+
81
81
+
local flake_tmp nix_tmp
82
82
+
flake_tmp=$(mktemp --suffix=.nix)
83
83
+
nix_tmp=$(mktemp --suffix=.nix)
84
84
+
85
85
+
curl -sf "https://raw.githubusercontent.com/''${owner}/''${repo}/''${ref}/flake.nix" \
86
86
+
> "$flake_tmp" || { rm -f "$flake_tmp" "$nix_tmp"; return 0; }
87
87
+
88
88
+
# Write a nix expression that extracts just the inputs URLs (no network at eval time).
89
89
+
printf 'let f = import %s; norm = v: if builtins.isString v then v else v.url or ""; in builtins.mapAttrs (_: norm) (f.inputs or {})\n' \
90
90
+
"$flake_tmp" > "$nix_tmp"
91
91
+
92
92
+
nix-instantiate --eval --strict --json "$nix_tmp" 2>/dev/null \
93
93
+
| jq -r 'to_entries[] | select(.value != "") | [.key, .value] | @tsv' \
94
94
+
>> "$QUEUE_FILE" || true
95
95
+
96
96
+
rm -f "$flake_tmp" "$nix_tmp"
97
97
+
}
98
98
+
99
99
+
# Seed the BFS queue with all declared inputs.
100
100
+
${queueSeed}
101
101
+
102
102
+
# BFS: process queue items until none remain unvisited.
103
103
+
while true; do
104
104
+
name="" url=""
105
105
+
while IFS=$'\t' read -r qname qurl; do
106
106
+
if ! grep -qxF "$qname" "$SEEN_FILE" 2>/dev/null; then
107
107
+
name="$qname" url="$qurl"
108
108
+
break
109
109
+
fi
110
110
+
done < "$QUEUE_FILE"
111
111
+
[ -z "$name" ] && break
112
112
+
printf '%s\n' "$name" >> "$SEEN_FILE"
113
113
+
if ! jq -e --arg n "$name" '.pins | has($n)' npins/sources.json >/dev/null 2>&1; then
114
114
+
npins_add_url "$name" "$url" || true
115
115
+
fi
116
116
+
discover_transitive "$name" "$url"
117
117
+
done
118
118
+
119
119
+
# Remove any pins that were not reachable in this run.
144
120
if [ -f npins/sources.json ]; then
145
121
for existing in $(jq -r '.pins | keys[]' npins/sources.json); do
146
146
-
keep=false
147
147
-
for w in $wanted; do
148
148
-
if [ "$existing" = "$w" ]; then keep=true; break; fi
149
149
-
done
150
150
-
if [ "$keep" = false ]; then
122
122
+
if ! grep -qxF "$existing" "$SEEN_FILE"; then
151
123
npins remove "$existing"
152
124
fi
153
125
done