···9595 if builtins.isBool v && hasEnableOption opt
9696 then {enable = v;}
9797 else resolveValue opt v;
9898+9999+ # dependencies go into a devshell so we can make use of stdenv setup hooks
100100+ # (e.g. for pkg-config and such)
101101+ dependencies = userConfig.dependencies or [];
102102+ spindleDevShell = pkgs.mkShellNoCC {
103103+ name = "spindle-deps";
104104+ packages = map resolvePackage dependencies;
105105+ };
98106in {
99107 nix.registry = builtins.mapAttrs (name: _:
100108 lib.mkForce {
···104112 };
105113 })
106114 registry;
107107- environment.systemPackages = map resolvePackage (userConfig.dependencies or []);
115115+ # put the devshell into the resulting image env.
116116+ # we do this instead of using a `.nix` file because it lets us skip eval time.
117117+ environment.etc = lib.mkIf (dependencies != []) {
118118+ "spindle/devshell.drv".source = spindleDevShell.drvPath;
119119+ "spindle/devshell-inputs".source = spindleDevShell.inputDerivation;
120120+ };
108121 services = builtins.mapAttrs (normalize (options.services or {})) (userConfig.services or {});
109122 virtualisation = builtins.mapAttrs (normalize (options.virtualisation or {})) (userConfig.virtualisation or {});
110123}
···1212 sed -E "s/${esc}\[[0-9;]*[a-zA-Z]//g; s/${esc}\([a-zA-Z]//g" "$@"
1313}
14141515+# check_needles OUT NEEDLES...
1616+check_needles() {
1717+ local out="$1"
1818+ shift
1919+ local clean
2020+ clean=$(echo "$out" | strip_ansi)
2121+2222+ local needle missing=0
2323+ for needle in "$@"; do
2424+ if ! echo "$clean" | grep -qE "$needle"; then
2525+ echo "error: output missing '$needle'" >&2
2626+ missing=1
2727+ fi
2828+ done
2929+3030+ if [ "$missing" -ne 0 ]; then
3131+ echo "$clean" >&2
3232+ return 1
3333+ fi
3434+}
3535+1536declare -a TEST_NAMES=()
1637declare -a TEST_STATUSES=()
1738declare -a TEST_TIMES=()
···446467/run/current-system/sw/bin/nix-store --realise "$store_path" >/dev/null
447468' bash "$test_store_path") || return 1
448469449449- if ! echo "$out" | strip_ansi | grep -q -E "^http_version=2(\\.0)?$"; then
450450- echo "error: cache proxy did not report HTTP/2" >&2
451451- echo "$out" | strip_ansi >&2
452452- return 1
453453- fi
470470+ check_needles "$out" "^http_version=2(\\.0)?$" || return 1
454471 echo "success: store path realized from cache and cache proxy accepted cleartext HTTP/2"
455472}
456473···536553 local out
537554 out=$(run_vm --name "nixpkgs-hello" --timeout "180s" --upload -- /run/current-system/sw/bin/nix-store --realise "$hello_path") || return 1
538555539539- # Check that it was substituted from our proxy
540540- if ! echo "$out" | strip_ansi | grep -q -E "copying path.*hello"; then
541541- echo "error: hello package was not substituted (or output mismatch)" >&2
542542- echo "$out" | strip_ansi >&2
543543- return 1
544544- fi
545545-546546- # Check that nothing was uploaded to the cache
547547- if ! echo "$out" | strip_ansi | grep -q "cache uploaded: 0"; then
548548- echo "error: hello package substitution triggered cache upload" >&2
549549- echo "$out" | strip_ansi >&2
550550- return 1
551551- fi
552552-556556+ # substituted from our proxy, and nothing uploaded back to the cache.
557557+ check_needles "$out" "copying path.*hello" "cache uploaded: 0" || return 1
553558 echo "success: hello package substituted from upstream cache and was not uploaded"
554559}
555560···570575 }'
571576 local out
572577 out=$(run_vm --name "activation-services" --timeout "300s" --activate "$config" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1
573573- if ! echo "$out" | strip_ansi | grep -q "^active$"; then
574574- echo "error: sshd not active after activation" >&2
575575- echo "$out" | strip_ansi >&2
576576- return 1
577577- fi
578578+ check_needles "$out" "^active$" || return 1
578579 echo "success: openssh service active after activation"
579580}
580581581582test_activation_dependencies() {
582582- # cowsay as a bare dependency (resolved via the pinned nixpkgs registry);
583583- # hello via the github flakeref and (separately) the my-nixpkgs alias.
583583+ # pkg-config + openssl as bare dependencies (resolved via the pinned nixpkgs
584584+ # registry); their job is to prove the activation devshell exports build env
585585+ # vars like PKG_CONFIG_PATH, not just PATH. hello comes in via the github
586586+ # flakeref and (separately) the my-nixpkgs alias.
584587 local config='{
585588 '"$ACTIVATION_REGISTRY"',
586589 "dependencies": [
587587- "cowsay",
590590+ "pkg-config",
591591+ "openssl",
588592 "github:nixos/nixpkgs#hello",
589593 "my-nixpkgs#hello"
590594 ]
591595 }'
596596+ # dependencies live in the activation devshell now, not systemPackages, so a
597597+ # step picks them up by sourcing the materialised env (this is what the
598598+ # engine's RunStep does automatically; the CLI runner does not, so we do it
599599+ # here).
592600 local out
593601 out=$(run_vm --name "activation-dependencies" --timeout "600s" --activate "$config" -- /run/current-system/sw/bin/bash -l -c '
602602+env_file=/run/spindle/devshell-env.sh
603603+[ -f "$env_file" ] && echo "env_file=present" || echo "env_file=missing"
604604+605605+# mirror RunStep
606606+. "$env_file"
607607+export PATH="$PATH:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin"
608608+594609set -euo pipefail
595595-cowsay "registry pin ok" >/dev/null && echo "cowsay=ran"
610610+# bare deps: pkg-config must be on PATH, and it must locate openssl through the
611611+# devshell-provided PKG_CONFIG_PATH (openssls headers live in a separate `dev`
612612+# output, so this also proves that output got realised into the devshell).
613613+echo "pkgconfig=$(pkg-config --version)"
614614+pkg-config --exists openssl && echo "openssl_found=yes"
615615+echo "openssl_version=$(pkg-config --modversion openssl)"
616616+inc=$(pkg-config --variable=includedir openssl)
617617+echo "includedir=$inc"
618618+[ -f "$inc/openssl/ssl.h" ] && echo "dev_headers=found"
619619+620620+# flakeref + aliased deps
596621echo "hello=$(hello)"
597622') || return 1
598623599599- local clean
600600- clean=$(echo "$out" | strip_ansi)
601601- if ! echo "$clean" | grep -qF "cowsay=ran"; then
602602- echo "error: bare dependency 'cowsay' (resolved via the pinned nixpkgs registry) did not run" >&2
603603- echo "$clean" >&2
604604- return 1
605605- fi
606606- if ! echo "$clean" | grep -qF "hello=Hello, world!"; then
607607- echo "error: hello dependency (github flakeref + my-nixpkgs alias) did not run" >&2
608608- echo "$clean" >&2
609609- return 1
610610- fi
611611- echo "success: bare, flakeref, and aliased dependencies all resolved and ran"
624624+ check_needles "$out" \
625625+ "env_file=present" "pkgconfig=[0-9]" "openssl_found=yes" \
626626+ "openssl_version=[0-9]" "dev_headers=found" "hello=Hello, world!" || return 1
627627+ echo "success: bare (pkg-config + openssl, dev headers found via PKG_CONFIG_PATH), flakeref, and aliased deps all resolved"
612628}
613629614630test_activation_registry_pin() {
···629645echo "nix_path=$(nix eval --raw --impure --expr "toString <nixpkgs>")"
630646') || return 1
631647632632- local clean
648648+ check_needles "$out" "lib_version=[0-9]" || return 1
649649+ local clean guest_nixpath
633650 clean=$(echo "$out" | strip_ansi)
634634- if ! echo "$clean" | grep -qE "lib_version=[0-9]"; then
635635- echo "error: guest could not resolve nixpkgs#lib.version from the user registry (locked-ref failure?)" >&2
636636- echo "$clean" >&2
637637- return 1
638638- fi
639639- local guest_nixpath
640651 guest_nixpath=$(echo "$clean" | sed -n 's/^nix_path=//p' | head -n1)
641652 if [ -z "$guest_nixpath" ] || [ "$guest_nixpath" = "$base_nixpkgs" ]; then
642653 echo "error: guest <nixpkgs> nixPath did not resolve to the registry override (got '$guest_nixpath', base '$base_nixpkgs')" >&2
···664675echo "substituted=$(cat "$store_path")"
665676' bash "$test_store_path") || return 1
666677667667- if ! echo "$out" | strip_ansi | grep -qF "substituted=hello from the workflow cache"; then
668668- echo "error: unique path was not substituted from the configured cache" >&2
669669- echo "$out" | strip_ansi >&2
670670- return 1
671671- fi
678678+ check_needles "$out" "substituted=hello from the workflow cache" || return 1
672679 echo "success: unique path substituted from the workflow cache through the read proxy"
673680}
674681···693700docker run --rm alpine echo container-ran-ok
694701') || return 1
695702696696- local clean
697697- clean=$(echo "$out" | strip_ansi)
698698- if ! echo "$clean" | grep -q "^docker_unit=active$"; then
699699- echo "error: docker service not active after activation" >&2
700700- echo "$clean" >&2
701701- return 1
702702- fi
703703- if ! echo "$clean" | grep -qE "storage_driver=overlay(2|fs)"; then
704704- echo "error: docker is not using an overlay storage driver (overlay.ko missing?)" >&2
705705- return 1
706706- fi
707707- if ! echo "$clean" | grep -qE "alpine_release=[0-9]+\."; then
708708- echo "error: failed to pull and read the alpine image" >&2
709709- return 1
710710- fi
711711- if ! echo "$clean" | grep -q "container-ran-ok"; then
712712- echo "error: command did not run inside the alpine container" >&2
713713- return 1
714714- fi
703703+ check_needles "$out" \
704704+ "^docker_unit=active$" "storage_driver=overlay(2|fs)" \
705705+ "alpine_release=[0-9]+\." "container-ran-ok" || return 1
715706 echo "success: docker service active, pulled and ran an alpine container on the overlay storage driver"
716707}
717708···730721 # the db. nothing cached yet, so this builds from scratch.
731722 local out
732723 out=$(run_vm --name "activation-cached-first" --timeout "600s" --activate "$config" --db "$db_path" --upload -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1
733733- if ! echo "$out" | strip_ansi | grep -q "^active$"; then
734734- echo "error: sshd not active after first activation" >&2
735735- echo "$out" | strip_ansi >&2
736736- return 1
737737- fi
724724+ check_needles "$out" "^active$" || return 1
738725739726 # second run: same config + db, no upload. must realize the recorded toplevel
740727 # from the cache instead of rebuilding, and the cached system must come up.
741728 out=$(run_vm --name "activation-cached-second" --timeout "300s" --activate "$config" --db "$db_path" -- /run/current-system/sw/bin/systemctl is-active sshd) || return 1
742729743743- local clean
744744- clean=$(echo "$out" | strip_ansi)
745745- if ! echo "$clean" | grep -q "realizing cached NixOS config"; then
746746- echo "error: second run did not realize cached configuration" >&2
747747- echo "$clean" >&2
748748- return 1
749749- fi
750750- if ! echo "$clean" | grep -q "^active$"; then
751751- echo "error: sshd not active after cached config activation" >&2
752752- echo "$clean" >&2
753753- return 1
754754- fi
730730+ check_needles "$out" "realizing cached NixOS config" "^active$" || return 1
755731 echo "success: second run realized the cached NixOS config from the cache and sshd came up"
756732}
757733···779755echo "ran=$("$hello_path/bin/hello")"
780756' sh "$hello_path") || return 1
781757782782- echo "$out" | strip_ansi >&2
783783- for needle in "release=" "user=spindle-workflow" "git version" "bash=" "workspace writable" "git over https ok" "apk ok" "ran=Hello, world!"; do
784784- if ! echo "$out" | strip_ansi | grep -q "$needle"; then
785785- echo "error: alpine guest output missing $needle" >&2
786786- return 1
787787- fi
788788- done
758758+ check_needles "$out" \
759759+ "release=" "user=spindle-workflow" "git version" "bash=" \
760760+ "workspace writable" "git over https ok" "apk ok" "ran=Hello, world!" || return 1
789761 echo "success: alpine guest booted, ran as workflow user, wrote workspace, cloned + installed over the network, and substituted+ran a package from cache.nixos.org over HTTPS"
790762}
791763···863835echo "old_path=$old_path"
864836' sh "$test_store_path") || return 1
865837866866- echo "$out" | strip_ansi >&2
867867- local clean
838838+ check_needles "$out" \
839839+ "daemon=ok" "substituted=hello from cache to alpine" "path_info=ok" \
840840+ "requisites=[1-9][0-9]*" "new_content=via-nix-build-with-dep" || return 1
841841+842842+ local clean new_path old_path
868843 clean=$(echo "$out" | strip_ansi)
869869-870870- local needle
871871- for needle in "daemon=ok" "substituted=hello from cache to alpine" "path_info=ok"; do
872872- if ! echo "$clean" | grep -q "$needle"; then
873873- echo "error: alpine nix output missing '$needle'" >&2
874874- return 1
875875- fi
876876- done
877877- if ! echo "$clean" | grep -qE "requisites=[1-9][0-9]*"; then
878878- echo "error: store db query returned no requisites for the substituted path" >&2
879879- return 1
880880- fi
881881- if ! echo "$clean" | grep -q "new_content=via-nix-build-with-dep"; then
882882- echo "error: 'nix build' (new CLI) did not realise its substituted dependency and build" >&2
883883- return 1
884884- fi
885885-886886- local new_path old_path
887844 new_path=$(echo "$clean" | grep -o 'new_path=/nix/store/[a-z0-9]*-alpine-nix-build-new' | cut -d= -f2)
888845 old_path=$(echo "$clean" | grep -o 'old_path=/nix/store/[a-z0-9]*-alpine-nix-build-old' | cut -d= -f2)
889846 if [ -z "$new_path" ] || [ -z "$old_path" ]; then