1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
|
//! Shell code that runs on the target, where populate can't rely on more
//! than a POSIX shell and the usual tools. Values come as positional
//! parameters, never quoted into the code.
/// The directories below the sentinel, once that is a directory, which have
/// a directory for each source that needs one. Besides, there is backup,
/// which holds copies of the sources.
pub const STATE_KINDS: &str = "derived flake-inputs flake-uploads flakes patches";
/// Defines use_state SENTINEL NAME..., which prepares keeping state below
/// the sentinel for the sources named NAME. The sentinel only needs to
/// exist, so it may be a file, which becomes a directory. Populate changes
/// and removes what's in its state directories, and a symlink in place of
/// one would make it do that to whatever the link points to, so it refuses
/// to use those.
pub fn use_state() -> String {
format!(
r#"
refuse_link() {{
if test -L "$1"; then
>&2 printf 'error: %s is a symlink, but populate needs a directory of its own there\n' "$1"
exit 1
fi
}}
use_state() {{
sentinel=$1
shift
refuse_link "$sentinel"
refuse_link "$sentinel"/backup
for state_kind in {STATE_KINDS}; do
refuse_link "$sentinel"/"$state_kind"
for state_name; do
refuse_link "$sentinel"/"$state_kind"/"$state_name"
done
done
if test -f "$sentinel"; then
rm "$sentinel"
mkdir "$sentinel"
fi
}}
"#
)
}
/// Defines remove_tree PATH, which removes what's at PATH, with everything
/// below it, also what isn't writable. A symlink gets removed itself,
/// instead of followed. Only directories get made writable, which is what
/// removing what's in them needs: a file may be a hard link to one
/// elsewhere, which shares its mode.
pub const REMOVE_TREE: &str = r#"
remove_tree() {
if test -L "$1"; then
rm "$1"
elif test -e "$1"; then
find "$1" -type d -exec chmod u+w {} +
rm -fR "$1"
fi
}
"#;
/// Defines remove_link PATH NAME, which removes a symlink at PATH, saying
/// so, with NAME for PATH, because the link may be there on purpose, but
/// populate never follows it: what a source delivers would change or remove
/// what the link points to. remove_foreign_link PATH NAME does so unless
/// the link points into the store, like the links populate puts there
/// itself, and make_room_for_link PATH NAME makes room at PATH for a link
/// into the store, which Nix only puts in place of another link into the
/// store.
pub const LINKS: &str = r#"
remove_link() {
if test -L "$1"; then
>&2 printf "replacing symlink '%s', which points to '%s'...\n" \
"$2" "$(readlink "$1")"
rm "$1"
fi
}
remove_foreign_link() {
if test -L "$1"; then
# Resolved, so a link like /nix/store/../.. doesn't count.
case $(readlink -f "$1") in
/nix/store/*) ;;
*) remove_link "$1" "$2" ;;
esac
fi
}
make_room_for_link() {
remove_foreign_link "$1" "$2"
if ! test -L "$1" && test -e "$1"; then
rm -fR "$1"
fi
}
"#;
/// Defines remove_others DIR LOG NAME..., which removes the entries of DIR
/// that aren't named NAME. find, unlike a loop reading names, copes with
/// every name, e.g. ones that end in blanks or contain newlines. With LOG
/// empty, entries get removed like remove_tree does; for entries of the
/// target directory, LOG is the directory's name in the line printed for
/// each entry, and nothing gets made writable first. The names are
/// portable file names, which mean themselves as patterns of find.
pub const REMOVE_OTHERS: &str = r#"
remove_others() {
dir=$1
log=$2
shift 2
n=$#
while test "$n" -gt 0; do
set -- "$@" ! -name "$1"
shift
n=$((n - 1))
done
find "$dir" -mindepth 1 -maxdepth 1 "$@" -exec sh -c '
log=$1
shift
for path; do
if test -n "$log"; then
>&2 printf "pruning '\''%s/%s'\''...\n" "$log" "${path##*/}"
rm -fR "$path"
elif test -L "$path"; then
rm "$path"
else
find "$path" -type d -exec chmod u+w {\} +
rm -fR "$path"
fi
done
' sh "$log" {} +
}
"#;
/// check SENTINEL NAME DIR FORCE: fails unless the sentinel exists, creating
/// it and the target directory DIR first if FORCE isn't empty. NAME is the
/// sentinel's name in messages.
pub const CHECK: &str = r#"
sentinel=$1 name=$2 dir=$3 force=$4
# Before anything touches it, which would touch what it points to.
if test -L "$sentinel"; then
>&2 printf 'error: sentinel file is a symlink: %s\n' "$name"
exit 1
fi
if test -n "$force"; then
mkdir -vp "$dir" >&2
if ! test -e "$sentinel"; then
touch "$sentinel"
fi
fi
if ! test -e "$sentinel"; then
>&2 printf 'error: missing sentinel file: %s\n' "$name"
exit 1
fi
"#;
/// backup DIR NAME: copies the target directory DIR to the sentinel's
/// backup directory. NAME is the sentinel's name in messages.
pub fn backup() -> String {
use_state()
+ r#"
dir=$1 name=$2
use_state "$dir"/.populate
if ! test -d "$dir"/.populate; then
>&2 printf 'error: sentinel file is not a directory: %s\n' "$name"
exit 1
fi
rsync >&2 \
-aAXF \
--delete \
--exclude /.populate \
--link-dest="$dir" \
"$dir"/ \
"$dir"/.populate/backup/
"#
}
/// prune DIR LOG NAME...: removes the entries of the target directory DIR
/// that aren't sources named NAME, logging them with LOG for DIR, and
/// populate's state that only sources that are gone needed. The names
/// after -- are the ones of flake and flake-input sources, whose roots for
/// the garbage collector stay.
pub fn prune() -> String {
use_state()
+ REMOVE_TREE
+ REMOVE_OTHERS
+ r#"
dir=$1 log=$2
shift 2
names=
while test "$1" != --; do
names="$names $1"
shift
done
shift
use_state "$dir"/.populate
cd "$dir"
# The names are portable file names, so splitting them at blanks is fine.
# shellcheck disable=SC2086
remove_others . "$log" .populate $names
roots=.populate/flake-inputs
if test -d "$roots"; then
remove_others "$roots" "" "$@"
if test -z "$(ls -A "$roots")"; then
rmdir "$roots"
fi
fi
remove_tree .populate/flake-uploads
"#
}
/// break_hard_links PATH NAME: prepares PATH for rsync. rsync writes
/// through a symlink at the target path, e.g. one a symlink or flake source
/// left there, into what it points to. And rsync fixes the mode and time of
/// a file it takes for unchanged in place, which changes every hard link to
/// it, e.g. one elsewhere, or a backup's. So each file with other hard
/// links gets replaced by a copy of its own first.
pub fn break_hard_links() -> String {
LINKS.to_string()
+ r#"
remove_link "$1" "$2"
if test -d "$1"; then
find "$1" -type f -links +1 -exec sh -c '
for file; do
copy=$(mktemp "$file.XXXXXX")
cp -p "$file" "$copy"
mv -f "$copy" "$file"
done
' sh {} +
fi
"#
}
/// derivation PATH NAME TEXT: builds the Nix expression TEXT on the target,
/// with a link to the result at PATH.
pub fn derivation() -> String {
LINKS.to_string()
+ r#"
make_room_for_link "$1" "$2"
nix-build -E "$3" -o "$1" >&2
"#
}
/// symlink PATH TARGET: makes PATH a symlink to TARGET.
pub const SYMLINK: &str = r#"
ln -fnsT -- "$2" "$1"
"#;
/// Defines git_problems PATH, which prints why populate can't use the .git
/// directory of the Git work tree at PATH, or nothing. A .git directory
/// that git clone and populate made has no symlinks, no commondir file,
/// which names another repository, no linked work trees, and only the
/// configuration they write: the repository's format, the remote, branches,
/// and submodules, whose repositories have their work tree in the work
/// tree. Hooks don't matter: populate's Git doesn't run them. And each .git below the work tree, which Git uses for a
/// submodule there, is a file naming a repository in .git/modules.
pub const GIT_PROBLEMS: &str = r#"
git_problems() (
path=$1
git_dir=$1/.git
links=$(find "$git_dir" -type l | head -n 3 | paste -s -d ' ' -)
if test -n "$links"; then
printf '%s contains symlinks: %s' "$git_dir" "$links"
exit
fi
common=$(find "$git_dir" -name commondir | head -n 1)
if test -n "$common"; then
printf '%s names another repository' "$common"
exit
fi
if test -e "$git_dir"/worktrees; then
printf '%s has linked work trees' "$git_dir"
exit
fi
nested=$(
find "$path" -path "$git_dir" -prune -o -name .git -exec sh -c '
modules=$1
shift
for git; do
if ! test -L "$git" && test -f "$git"; then
gitdir=$(sed -n "s/^gitdir: //p" "$git")
resolved=$(cd "${git%/.git}" && cd "$gitdir" 2>/dev/null && pwd -P) || :
case $resolved in
"$modules"/?*) continue ;;
esac
fi
printf "%s\n" "$git"
done
' sh "$(cd "$git_dir" && pwd -P)/modules" {} + | head -n 1
)
if test -n "$nested"; then
printf '%s is not a file naming a repository in %s/modules' "$nested" "$git_dir"
exit
fi
find "$git_dir" -name config -type f | while IFS= read -r config; do
command git config --file "$config" --list --name-only |
while IFS= read -r key; do
case $key in
core.repositoryformatversion|core.filemode|core.bare|\
core.logallrefupdates|core.symlinks|core.ignorecase|\
core.precomposeunicode|\
extensions.objectformat|extensions.refstorage|\
remote.origin.url|remote.origin.fetch|\
branch.*.remote|branch.*.merge|\
submodule.active|submodule.*.url|submodule.*.active) ;;
core.worktree)
# Submodules' repositories have their work tree in the work tree.
worktree=$(command git config --file "$config" core.worktree)
dir=$(dirname "$config")
case $dir/$worktree in
"$git_dir"/modules/*)
resolved=$(cd "$dir" && cd "$worktree" 2>/dev/null && pwd -P) || :
case $resolved in
"$(cd "$path" && pwd -P)"/*) continue ;;
esac
;;
esac
printf '%s sets core.worktree to %s' "$config" "$worktree"
exit 1
;;
*)
printf '%s sets %s, which neither git clone nor populate does' "$config" "$key"
exit 1
;;
esac
done || exit 1
done || :
)
"#;
/// git PATH NAME URL REF SHALLOW FETCH_ALWAYS RECLONE EXCLUDE...: makes
/// PATH a Git work tree of the repository at URL, with REF checked out,
/// and everything removed that Git doesn't track, except what matches an
/// EXCLUDE pattern. SHALLOW, FETCH_ALWAYS, and RECLONE are flags, empty
/// for false. NAME is PATH's name in messages.
pub fn git() -> String {
LINKS.to_string()
+ GIT_PROBLEMS
+ r#"
path=$1 name=$2 url=$3 ref=$4 shallow=$5 fetch_always=$6 reclone=$7
shift 7
# Hooks and commands a repository's configuration names run with Git's
# commands, so populate's Git doesn't run any. The configuration gets
# checked as well, but Git also runs a hook it has itself.
git() {
command git -c core.hooksPath=/dev/null -c core.fsmonitor=false "$@"
}
# A target path that doesn't look like a Git work tree gets removed, e.g.
# when it had another type before. A work tree that a symlink points to,
# or whose .git is a symlink or a file naming a repository elsewhere, isn't
# populate's own: fetching, resetting, and cleaning would change what's
# elsewhere.
remove_link "$path" "$name"
if test -L "$path"/.git || ! test -d "$path"/.git; then
rm -fR "$path"
fi
# A .git directory can name places elsewhere too, which Git's commands
# change, or commands, which they run. Populate only uses a work tree
# whose .git directory has nothing like that, and else replaces it if
# reclone is set, or fails, telling why.
if test -e "$path"; then
if problems=$(git_problems "$path"); then :; else
exit 1
fi
if test -n "$problems"; then
if test -n "$reclone"; then
>&2 printf "replacing the Git work tree '%s', because %s...\n" \
"$name" "$problems"
rm -fR "$path"
else
>&2 printf 'error: populate doesn'"'"'t use the Git work tree %s, because %s. Fix that, remove the work tree, or set the source'"'"'s reclone to true to replace it by a fresh clone.\n' \
"$name" "$problems"
exit 1
fi
fi
fi
if ! test -e "$path"; then
if test -n "$shallow"; then
git init "$path"
else
git clone --recurse-submodules -- "$url" "$path"
fi
fi
cd "$path"
if ! origin=$(git config remote.origin.url); then
git remote add -- origin "$url"
elif test "$origin" != "$url"; then
git remote set-url -- origin "$url"
fi
# A commit, given by its hash or a prefix thereof, gets fetched only if the
# work tree lacks it, unless fetchAlways is set. After --, a ref can't be
# taken for an option. A branch or tag gets
# fetched on every run, so the work tree follows it: resetting to its name
# would reset to a local branch, which fetching doesn't move.
fetch() {
if test -n "$shallow"; then
git fetch --depth=1 origin -- "$1"
else
git fetch origin -- "$1"
fi
}
fetch_commit() {
if test -n "$shallow"; then
fetch "$hash"
else
git fetch origin
fi
}
if printf %s "$ref" | grep -Eqx '[0-9a-f]{4,40}'; then
hash=$ref
if ! test "$(git log --format=%H -1)" = "$hash"; then
if test -n "$fetch_always"; then
fetch_commit
elif ! git log -1 "$hash" >/dev/null 2>&1; then
fetch_commit
fi
fi
else
fetch "$ref"
hash=$(git rev-parse --verify 'FETCH_HEAD^{commit}')
fi
# Also if HEAD is there already: rewriting a lock file in place, e.g.,
# changes the work tree, and stages the lock file if Nix created it.
git reset --hard "$hash" >&2
git submodule update --init --recursive
n=$#
while test "$n" -gt 0; do
set -- "$@" -e "$1"
shift
n=$((n - 1))
done
git clean -dfx "$@"
"#
}
/// Shell code running on the deploying machine, in the source's working
/// directory.
pub mod local {
/// pass_info DIR NAME: prints what tells whether the pass source of the
/// passwords below NAME in the store DIR changed: the last commits of
/// NAME, and of what symlinks therein point to, whose changes the
/// commits of NAME miss. Prints nothing if the store isn't a Git
/// repository.
pub const PASS_INFO: &str = r#"
dir=$1 name=$2
if test -e "$dir"/.git; then
printf '%s %s\n' "$name" "$(
git -C "$dir" log -1 --format=%H -- "$name"
find "$dir"/"$name" -type l -exec realpath {} + |
sort |
xargs -r -n 1 git -C "$dir" log -1 --format=%H
)"
fi
"#;
/// pass DIR NAME INFO: decrypts the passwords below NAME in the store
/// DIR into the working directory, with the times of their last
/// commits, and INFO, unless empty, in .pass_info.
pub const PASS: &str = r#"
dir=$1 name=$2 info=$3
umask 0077
chmod 0700 "$PWD"
prefix=$dir/$name
find "$prefix" -type f -follow ! -name .gpg-id |
while read -r gpg_path; do
rel_name=${gpg_path#"$prefix"}
rel_name=${rel_name%.gpg}
pass_date=$(
if test -e "$dir"/.git; then
git -C "$dir" log -1 --format=%aI "$gpg_path"
fi
)
pass_name=$name/$rel_name
pass_out=$PWD/$rel_name
mkdir -p "$(dirname "$pass_out")"
PASSWORD_STORE_DIR=$dir pass show -- "$pass_name" > "$pass_out"
if [ -n "$pass_date" ]; then
touch -d "$pass_date" "$pass_out"
fi
done
if test -n "$info"; then
printf '%s\n' "$info" > "$PWD"/.pass_info
fi
"#;
/// passage HOST: decrypts the secrets in $PASSAGE_DIR into the working
/// directory, with the times of their files. HOST is the first label
/// of the target's host, for the command a failure prints.
pub const PASSAGE: &str = r#"
host=$1
umask 0077
chmod 0700 "$PWD"
find "$PASSAGE_DIR" -type f -name \*.age -follow |
while read -r age_path; do
pass_name=${age_path#"$PASSAGE_DIR"/}
pass_name=${pass_name%.age}
pass_out=$PWD/$pass_name
mkdir -p "$(dirname "$pass_out")"
if ! passage show "$pass_name" > "$pass_out" 2>&1; then
echo ++ secrets "$host" show "$pass_name" >&2
# What passage says, without age's progress lines.
grep '^[^[]' "$pass_out" >&2 || :
exit 1
fi
touch -r "$age_path" "$pass_out"
done
"#;
}
/// pass_info PATH: prints the .pass_info the pass source at PATH got last.
pub const PASS_INFO: &str = r#"
cat "$1"/.pass_info || :
"#;
/// Defines url_path PATH, which prints PATH as it goes into a URL: with
/// every byte percent-encoded that isn't a letter, a digit, or one of
/// -._~/. Nix decodes that, and otherwise takes, e.g., ? and # for
/// delimiters, and refuses blanks.
pub const URL_PATH: &str = r#"
url_path() {
printf %s "$1" | od -An -v -tx1 | tr -s ' ' '\n' |
while read -r url_byte; do
case $url_byte in
"") ;;
2[d-f]|3[0-9]|4[1-9a-f]|5[0-9a]|5f|6[1-9a-f]|7[0-9a]|7e)
printf "\\$(printf %o "0x$url_byte")"
;;
*)
printf '%%%s' "$url_byte"
;;
esac
done
}
"#;
/// Defines require_flakes WHAT, which enables flakes for the Nix commands
/// following it, and fails unless Nix supports them, i.e. is version 2.4 or
/// later. WHAT names what needs flakes, for the error message.
pub const REQUIRE_FLAKES: &str = r#"
require_flakes() {
NIX_CONFIG=$(printf '%s\n%s' "${NIX_CONFIG-}" \
'extra-experimental-features = nix-command flakes')
export NIX_CONFIG
if ! nix flake --help >/dev/null 2>&1; then
>&2 printf 'error: %s needs Nix 2.4 or later on %s, found: %s\n' \
"$1" "$(uname -n)" "$(nix --version 2>&1 || :)"
exit 1
fi
}
"#;
/// The definitions the shell code for flakes uses.
pub fn prelude() -> String {
[
&use_state(),
REMOVE_TREE,
LINKS,
REMOVE_OTHERS,
URL_PATH,
REQUIRE_FLAKES,
]
.concat()
}
/// Shell code for flake and flake-input sources on the target. Each gets
/// SENTINEL and NAME, the source's name, first, and the definitions of
/// prelude.
pub mod flake {
/// Defines remove_upload, which removes the directory receiving the
/// flake itself, and the directory of uploads once it's empty.
/// Sources get populated one after another, so no other source uses
/// that meanwhile.
pub const REMOVE_UPLOAD: &str = r#"
remove_upload() {
remove_tree "$sentinel"/flake-uploads/"$name"
if test -d "$sentinel"/flake-uploads &&
test -z "$(ls -A "$sentinel"/flake-uploads)"; then
rmdir "$sentinel"/flake-uploads
fi
}
"#;
/// prepare_upload SENTINEL NAME: makes the directory receiving the
/// flake itself, or the patches if PATCHES isn't empty.
pub const PREPARE: &str = r#"
sentinel=$1 name=$2 patches=$3
use_state "$sentinel" "$name"
if test -n "$patches"; then
mkdir -p "$sentinel"/patches
else
remove_upload
mkdir -p "$sentinel"/flake-uploads/"$name"
fi
"#;
/// valid PATH: succeeds if the store has PATH.
pub const VALID: &str = r#"
nix-store --check-validity "$1" 2>/dev/null
"#;
/// archive PATH SUBDIR REQUIRE NAME HINT OVERRIDDEN...: prints what
/// nix flake archive prints for the flake in SUBDIR of the store path
/// PATH, with an empty flake in place of each input at an OVERRIDDEN
/// path, so Nix doesn't fetch them, or nothing if the flake has no
/// lock file, since locking would change it. If its inputs can't be
/// archived, it fails if REQUIRE isn't empty, saying so for the source
/// NAME, with HINT appended, or else archives what Nix locks now, or
/// prints nothing, with a warning.
pub const ARCHIVE: &str = r#"
path=$1 subdir=$2 require=$3 name=$4 hint=$5
shift 5
dir=$path${subdir:+/$subdir}
if test -e "$dir"/flake.nix && test -e "$dir"/flake.lock; then
flake=path:$path${subdir:+?dir=$subdir}
empty=
if test $# -gt 0; then
empty=$(mktemp -d)
echo '{ outputs = _: {}; }' > "$empty"/flake.nix
n=$#
while test "$n" -gt 0; do
set -- "$@" --override-input "$1" "path:$empty"
shift
n=$((n - 1))
done
fi
if nix flake archive --json --no-update-lock-file "$@" "$flake"; then
:
elif test -n "$require"; then
>&2 printf 'error: source %s: the inputs its own lock file pins can'"'"'t be delivered%s\n' \
"$name" "$hint"
exit 1
# Without --no-update-lock-file, Nix adds what the lock file lacks.
elif nix flake archive --json --no-write-lock-file "$@" "$flake"; then
>&2 printf 'warning: source %s: its own lock file needs to be updated; delivering it with the inputs Nix locks now, which evaluating it may not use\n' \
"$name"
else
>&2 printf 'warning: source %s: the inputs its own lock file pins can'"'"'t be delivered; delivering it without them\n' \
"$name"
fi
if test -n "$empty"; then
rm -R "$empty"
fi
fi
"#;
/// link SENTINEL NAME LINK_PATH LINK_NAME DERIVED SUBDIR PATH ROOT...:
/// links the store path PATH, or SUBDIR therein, at LINK_PATH, and
/// registers it and the store paths ROOT as garbage collector roots of
/// the source NAME. Roots get added before obsolete ones get removed,
/// so the inputs stay rooted. LINK_NAME is LINK_PATH's name in
/// messages, and DERIVED isn't empty if LINK_PATH is in the source's
/// derived state.
pub const LINK: &str = r#"
sentinel=$1 name=$2 link_path=$3 link_name=$4 derived=$5 subdir=$6 path=$7
shift 7
roots_dir=$sentinel/flake-inputs/$name
use_state "$sentinel" "$name"
if test -n "$derived"; then
mkdir -p "$(dirname "$link_path")"
fi
make_room_for_link "$link_path" "$link_name"
nix-store --realise "$path" --add-root "$link_path" --indirect >/dev/null
# The garbage collector follows the link into the store path.
if test -n "$subdir"; then
ln -fns "$path/$subdir" "$link_path"
fi
# Left behind by an interrupted run, or not needed anymore.
remove_upload
mkdir -p "$roots_dir"
roots=
for path; do
root=$(basename "$path")
if test "$(readlink "$roots_dir"/"$root")" != "$path"; then
nix-store --realise "$path" --add-root "$roots_dir"/"$root" --indirect >/dev/null
fi
roots="$roots $root"
done
ls -A "$roots_dir" | while read -r root; do
case " $roots " in
*" $root "*) ;;
*) rm "$roots_dir"/"$root" ;;
esac
done
# An input without own inputs leaves the directory empty.
if test -z "$roots"; then
rmdir "$roots_dir"
if test -z "$(ls -A "$sentinel"/flake-inputs)"; then
rmdir "$sentinel"/flake-inputs
fi
fi
"#;
/// fetch SENTINEL NAME URL INPUT SELECTED SELF EXPECTED: prints the
/// store path of what the selection SELECTED, as JSON, names, and the
/// subdirectory therein, on separate lines. That's SELF, the flake
/// itself, which gets added from the upload directory unless the store
/// has it already, or EXPECTED, the store path the lock file pins it
/// to, if the store has it, or else what fetching it gets. Inputs
/// referring to local paths can only be fetched if the target has them
/// as well. URL and INPUT name the flake and the input in messages.
pub const FETCH: &str = r#"
sentinel=$1 name=$2 url=$3 input=$4 selected=$5 self=$6 expected=$7
upload=$sentinel/flake-uploads/$name
if test -n "$self" && ! nix-store --check-validity "$self" 2>/dev/null; then
store_name=$(basename "$self" | cut -c34-)
mv "$upload"/tree "$upload"/"$store_name"
added=$(nix-store --add-fixed --recursive sha256 "$upload"/"$store_name")
remove_upload
if test "$added" != "$self"; then
>&2 printf 'error: flake %s arrived as %s instead of %s\n' \
"$url" "$added" "$self"
exit 1
fi
fi
# Fetching contacts where the input comes from even if the store has what
# the lock file pins.
if test -n "$expected" && ! nix-store --check-validity "$expected" 2>/dev/null; then
expected=
fi
FLAKE=$url INPUT=$input SELECTED=$selected SELF=$self EXPECTED=$expected \
nix eval --impure --raw --expr '
let
selected = builtins.fromJSON (builtins.getEnv "SELECTED");
expected = builtins.getEnv "EXPECTED";
selectedPath =
if selected.self || expected != "" then null
else localPath selected.locked;
path =
if selected.self then builtins.getEnv "SELF"
else if expected != "" then expected
else (builtins.fetchTree selected.locked).outPath;
dir = path + (if selected.subdir == "" then "" else "/" + selected.subdir);
lock =
if builtins.pathExists (dir + "/flake.lock")
then builtins.fromJSON (builtins.readFile (dir + "/flake.lock"))
else { nodes = {}; };
localPath = locked:
if (locked.type or null) == "path" then locked.path
else let
match = builtins.match "file:(//)?(.*)" (locked.url or "");
in
if match == null then null else builtins.elemAt match 1;
missing = builtins.filter
(key: let
p = localPath (lock.nodes.${key}.locked or {});
in
p != null && builtins.substring 0 1 p == "/" && !builtins.pathExists p)
(builtins.attrNames lock.nodes);
key = builtins.head missing;
in
if selectedPath != null && builtins.substring 0 1 selectedPath == "/" && !builtins.pathExists selectedPath then
throw "flake ${builtins.getEnv "FLAKE"}: input ${builtins.getEnv "INPUT"} refers to ${selectedPath}, which does not exist on the target"
else if missing != [] then
throw "flake ${builtins.getEnv "FLAKE"}: input ${key} refers to ${localPath lock.nodes.${key}.locked}, which does not exist on the target"
else
path + "\n" + selected.subdir
'
"#;
}
/// Shell code for drawing inputs of flakes in sources from other sources,
/// and for deriving what flake and flake-input sources that get patched,
/// get their lock files rewritten, or are materialized deliver. Each gets
/// the definitions of prelude and DEFINITIONS. Populate decides what to
/// do, from what these send back.
pub mod draw {
/// Defines own_lock DIR, apply_patches PATCH_DIR NAME, and write_key
/// KEY_FILE KEY.
///
/// Nix rewrites lock files in place, which also changes what else is a
/// hard link to the lock file, e.g. in a backup, or what it is a
/// symlink to. So own_lock replaces the lock file of the flake in DIR
/// by a copy first. The copy goes to a new file, so nothing that's
/// there already gets written through.
///
/// apply_patches applies the patches in PATCH_DIR, in order, to the
/// working directory, failing, naming the patch and the source NAME, if
/// one doesn't apply.
///
/// write_key writes KEY to a new file, which then replaces KEY_FILE, so
/// nothing at its place gets written through, e.g. a symlink.
pub const DEFINITIONS: &str = r#"
own_lock() {
if test -e "$1"/flake.lock; then
own_lock_copy=$(mktemp "$1"/.flake.lock.XXXXXX)
cp -p "$1"/flake.lock "$own_lock_copy"
mv -f "$own_lock_copy" "$1"/flake.lock
fi
}
apply_patches() {
ls "$1" | while read -r patch; do
if ! patch -p1 -s --batch --no-backup-if-mismatch -i "$1"/"$patch" >&2; then
>&2 printf 'error: patch %s does not apply to source %s\n' \
"$(printf %s "$patch" | cut -c6-)" "$2"
exit 1
fi
done
}
write_key() {
key_file=$(mktemp "$1".XXXXXX)
printf '%s\n' "$2" > "$key_file"
mv -f "$key_file" "$1"
}
"#;
/// use_state SENTINEL NAME...
pub const USE_STATE: &str = r#"
use_state "$@"
"#;
/// resolve PATH...: prints, for each PATH, what's there and where it
/// resolves to, as two fields, each ending in a null byte. The first
/// holds d if PATH is a directory, g if it has a .git directory that
/// git_problems finds nothing wrong with, and that has no hooks, s if
/// it has .gitmodules, and f if it has flake.nix. Nix runs Git in a
/// git+file: flake, with the repository's configuration and hooks, so
/// it only gets such ones.
pub const RESOLVE: &str = r#"
for path; do
resolved=$(readlink -f "$path") || resolved=
flags=
if test -d "$path"; then flags=${flags}d; fi
if ! test -L "$path"/.git && test -d "$path"/.git; then
problems=$(git_problems "$path" 2>/dev/null) || problems=failed
if test -d "$path"/.git/hooks; then
hooks=$(find "$path"/.git/hooks -mindepth 1 ! -name '*.sample' | head -n 1)
else
hooks=
fi
if test -z "$problems" && test -z "$hooks"; then flags=${flags}g; fi
fi
if test -e "$path"/.gitmodules; then flags=${flags}s; fi
if test -e "$path"/flake.nix; then flags=${flags}f; fi
printf '%s\0%s\0' "$flags" "$resolved"
done
"#;
/// metadata URL NAME OVERRIDE...: prints what nix flake metadata prints
/// for the flake at URL, of the source NAME, as Nix locks it now, with
/// an empty flake in place of each input at an OVERRIDE path, which
/// gets drawn from a source and may not be reachable as the flake's
/// lock file pins it.
pub const METADATA: &str = r#"
url=$1 name=$2
shift 2
empty=$(mktemp -d)
echo '{ outputs = _: {}; }' > "$empty"/flake.nix
n=$#
while test "$n" -gt 0; do
set -- "$@" --override-input "$1" "path:$empty"
shift
n=$((n - 1))
done
if ! nix flake metadata --json --no-write-lock-file "$@" "$url"; then
rm -R "$empty"
>&2 printf 'error: source %s draws inputs from other sources, but its flake can'"'"'t be locked\n' \
"$name"
exit 1
fi
rm -R "$empty"
"#;
/// lock DIR URL ARG...: rewrites the lock file of the flake in DIR,
/// at URL, with nix flake lock's arguments ARG.
pub const LOCK: &str = r#"
dir=$1 url=$2
shift 2
own_lock "$dir"
nix flake lock "$@" "$url" >&2
"#;
/// derived_state DIR TARGET_PATH PATCH_DIR DRAWN...: removes what an
/// interrupted run left in the source's derived state DIR, and prints
/// what tells whether the copy there needs to be made again, as fields
/// ending in null bytes: where DIR/original resolves to, the key of the
/// copy, the store path DIR/result links to, if the store has it, link
/// or directory, if TARGET_PATH is one, a line of sha256sum for each
/// patch in PATCH_DIR, an empty field, and the hash of each DRAWN path,
/// or nothing if it resolves into the store.
pub const DERIVED_STATE: &str = r#"
dir=$1 target_path=$2 patch_dir=$3
shift 3
remove_tree "$dir"/work
printf '%s\0' "$(readlink -f "$dir"/original)"
if test -e "$dir"/key; then
printf '%s\0' "$(cat "$dir"/key)"
else
printf '\0'
fi
if result=$(readlink "$dir"/result) &&
nix-store --check-validity "$result" 2>/dev/null; then
printf '%s\0' "$result"
else
printf '\0'
fi
if test -L "$target_path"; then
printf 'link\0'
elif test -d "$target_path"; then
printf 'directory\0'
else
printf '\0'
fi
if test -n "$patch_dir"; then
ls "$patch_dir" | while read -r patch; do
printf '%s\0' "$(sha256sum "$patch_dir"/"$patch")"
done
fi
printf '\0'
for drawn; do
drawn=$(readlink -f "$drawn")
case $drawn in
/nix/store/*) printf '\0' ;;
*) printf '%s\0' "$(nix-hash --type sha256 "$drawn")" ;;
esac
done
"#;
/// materialize ORIGINAL WORK TARGET_PATH TARGET_NAME PATCH_DIR NAME
/// KEY_FILE KEY: puts a copy of ORIGINAL, made in WORK and patched with
/// the patches in PATCH_DIR, if that isn't empty, at TARGET_PATH, and
/// writes its key. TARGET_NAME is TARGET_PATH's name in messages, and
/// NAME the source's.
pub const MATERIALIZE: &str = r#"
original=$1 work=$2 target_path=$3 target_name=$4 patch_dir=$5 name=$6
key_file=$7 key=$8
cp -R "$original" "$work"
chmod -R u+w "$work"
if test -n "$patch_dir"; then
(
cd "$work"
apply_patches "$patch_dir" "$name"
)
fi
remove_foreign_link "$target_path" "$target_name"
rm -fR "$target_path"
mv "$work" "$target_path"
write_key "$key_file" "$key"
"#;
/// add DIR PATH SUBDIR TARGET_PATH TARGET_NAME RESULT PATCH_DIR NAME
/// KEY ARG...: links SUBDIR of the store path RESULT at TARGET_PATH.
/// If RESULT is empty, that's a copy of the store path PATH, made in
/// the source's derived state DIR, patched with the patches in
/// PATCH_DIR, if that isn't empty, with the lock file of its flake in
/// SUBDIR rewritten with nix flake lock's arguments ARG, if any, and
/// added to the store, rooted by DIR/result, with its key in DIR/key.
/// TARGET_NAME is TARGET_PATH's name in messages, and NAME the
/// source's.
pub const ADD: &str = r#"
dir=$1 path=$2 subdir=$3 target_path=$4 target_name=$5 result=$6
patch_dir=$7 name=$8 key=$9
shift 9
if test -z "$result"; then
# The copy keeps the name of the store path.
mkdir "$dir"/work
work=$(readlink -f "$dir"/work)/$(basename "$path" | cut -c34-)
cp -R "$path" "$work"
chmod -R u+w "$work"
if test -n "$patch_dir"; then
(
cd "$work"/"$subdir"
apply_patches "$patch_dir" "$name"
)
fi
if test $# -gt 0; then
# Not just the path, which Nix takes for the enclosing Git work tree,
# if any.
nix flake lock "$@" "path:$(url_path "$work")${subdir:+?dir=$subdir}" >&2
fi
result=$(nix-store --add-fixed --recursive sha256 "$work")
remove_tree "$dir"/work
nix-store --realise "$result" --add-root "$dir"/result --indirect >/dev/null
write_key "$dir"/key "$key"
fi
make_room_for_link "$target_path" "$target_name"
# The source's derived state roots the store path.
ln -fns "$result${subdir:+/$subdir}" "$target_path"
"#;
/// generate DIR TEXT: makes DIR a flake, with TEXT as its flake.nix,
/// and locks it.
pub const GENERATE: &str = r#"
dir=$1 text=$2
mkdir -p "$(dirname "$dir")"
rm -fR "$dir"
mkdir "$dir"
cd "$dir"
printf %s "$text" > flake.nix
# Not just ., which Nix takes for the enclosing Git work tree, if any.
nix flake lock "path:$(url_path "$(pwd -P)")" >&2
"#;
/// clean_up SENTINEL NAMES NOT_FLAKES GENERATED DERIVED PATCHED: removes
/// the state sources don't need anymore. Each argument after SENTINEL
/// is a list of source names, separated by blanks: of all sources, of
/// those that are neither flake nor flake-input sources, whose roots
/// for the garbage collector go, and of those that have a generated
/// flake, derived state, and patches.
pub const CLEAN_UP: &str = r#"
sentinel=$1
# The names are portable file names, so splitting them at blanks is fine.
# shellcheck disable=SC2086
use_state "$sentinel" $2
# shellcheck disable=SC2086
for name in $3; do
rm -fR "$sentinel"/flake-inputs/"$name"
done
if test -d "$sentinel"/flake-inputs &&
test -z "$(ls -A "$sentinel"/flake-inputs)"; then
rmdir "$sentinel"/flake-inputs
fi
remove_stale() {
stale_dir=$1
shift
if test -d "$stale_dir"; then
remove_others "$stale_dir" '' "$@"
if test -z "$(ls -A "$stale_dir")"; then
rmdir "$stale_dir"
fi
fi
}
# shellcheck disable=SC2086
remove_stale "$sentinel"/flakes $4
# shellcheck disable=SC2086
remove_stale "$sentinel"/derived $5
# shellcheck disable=SC2086
remove_stale "$sentinel"/patches $6
"#;
}
|