···1515pub const SPINDLE_RUN_DIR: &str = "/run/spindle";
1616pub const SPINDLE_NIX_CONFIG: &str = "/run/spindle/nix.conf";
1717pub const SPINDLE_CACHE_CONFIG: &str = "/run/spindle/cache.json";
1818-pub const NIX_EXECUTABLE: &str = "/run/current-system/sw/bin/nix";
1918pub const NIX_BUILD_EXECUTABLE: &str = "/run/current-system/sw/bin/nix-build";
2019pub const NIX_STORE_EXECUTABLE: &str = "/run/current-system/sw/bin/nix-store";
2120pub const SYSTEMCTL_EXECUTABLE: &str = "/run/current-system/sw/bin/systemctl";
2121+2222+// nix lives in different places depending on the guest OS (NixOS system
2323+// profile vs. plain /usr/local on e.g. alpine)
2424+pub fn nix_executable() -> &'static str {
2525+ static NIX: once_cell::sync::Lazy<&'static str> = once_cell::sync::Lazy::new(|| {
2626+ let paths = [
2727+ "/run/current-system/sw/bin/nix",
2828+ "/usr/local/bin/nix",
2929+ "/usr/bin/nix",
3030+ ];
3131+ for candidate in paths {
3232+ if Path::new(candidate).exists() {
3333+ return candidate;
3434+ }
3535+ }
3636+ "/run/current-system/sw/bin/nix"
3737+ });
3838+ &NIX
3939+}
22402341#[derive(Clone, Debug, Default, Deserialize, Serialize)]
2442pub struct RuntimeCacheConfig {
···102120}
103121104122pub async fn nix_version() -> String {
105105- let spec = Spec::new(NIX_EXECUTABLE)
123123+ let spec = Spec::new(nix_executable())
106124 .arg("--version")
107125 .timeout(Duration::from_secs(1));
108126···142160 // todo: ...and also here, the installed file?
143161}
144162163163+const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket";
164164+145165async fn restart_nix_daemon() {
146146- if !Path::new(SYSTEMCTL_EXECUTABLE).exists() {
147147- info!("no systemctl in guest, skipping nix-daemon restart");
148148- return;
149149- }
150150- let spec = Spec::new(SYSTEMCTL_EXECUTABLE)
151151- .args(["try-restart", "nix-daemon.service"])
152152- .timeout(Duration::from_secs(5));
166166+ let systemd = Path::new(SYSTEMCTL_EXECUTABLE).exists();
167167+ let spec = if systemd {
168168+ Spec::new(SYSTEMCTL_EXECUTABLE)
169169+ .args(["try-restart", "nix-daemon.service"])
170170+ .timeout(Duration::from_secs(5))
171171+ } else {
172172+ // on non-systemd we can just kill the daemon and it should restart
173173+ Spec::new("pkill")
174174+ .args(["-f", "nix-daemon"])
175175+ .timeout(Duration::from_secs(5))
176176+ };
153177154178 match command::run_capture(spec).await {
155155- Ok(output) if output.success() => {}
179179+ Ok(output) if output.success() => {
180180+ if !systemd {
181181+ // init has to respawn the daemon before any step needs it
182182+ wait_for_nix_daemon_socket(Duration::from_secs(5)).await;
183183+ }
184184+ }
185185+ // pkill exits 1 when nothing matched, ie. no daemon to restart
186186+ Ok(output) if !systemd && output.exit.exit_code == 1 => {
187187+ info!("no nix-daemon running, skipping restart")
188188+ }
156189 Ok(output) => warn!(
157190 exit_code = output.exit.exit_code,
158191 error = ?output.exit.error,
159192 output = %output.combined_lossy(),
160160- "nix-daemon try-restart failed"
193193+ "nix-daemon restart failed"
161194 ),
162162- Err(error) => warn!(%error, "nix-daemon try-restart failed"),
195195+ Err(error) => warn!(%error, "nix-daemon restart failed"),
196196+ }
197197+}
198198+199199+async fn wait_for_nix_daemon_socket(timeout: Duration) {
200200+ let deadline = tokio::time::Instant::now() + timeout;
201201+ loop {
202202+ if tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET)
203203+ .await
204204+ .is_ok()
205205+ {
206206+ return;
207207+ }
208208+ if tokio::time::Instant::now() >= deadline {
209209+ warn!(
210210+ socket = NIX_DAEMON_SOCKET,
211211+ "nix-daemon did not come back after restart"
212212+ );
213213+ return;
214214+ }
215215+ tokio::time::sleep(Duration::from_millis(100)).await;
163216 }
164217}
165218
+1-1
shuttle/src/session.rs
···207207208208 tokio::spawn(async move {
209209 tokio::time::sleep(Duration::from_millis(100)).await;
210210-210210+211211 // prefer a clean shutdown through the init system when one is around
212212 // (systemd on NixOS, busybox/openrc elsewhere), then fall back to the
213213 // raw reboot(2) syscall on minimal guests
+40
spindle/engines/microvm/test-spindle-microvm.sh
···469469 echo "success: alpine guest booted, ran as workflow user, wrote workspace, cloned over https"
470470}
471471472472+test_alpine_nix() {
473473+ local test_store_path
474474+ test_store_path=$(nix-build -E 'with import <nixpkgs> {}; writeText "alpine-nix-test" "hello from cache to alpine"' --no-out-link)
475475+ nix copy --to "$CACHE_UPLOAD_URL?secret-key=$CACHE_SECRET_KEY_PATH" "$test_store_path"
476476+477477+ local out
478478+ out=$(run_vm --spec "$ALPINE_IMAGE_SPEC_JSON" --name "alpine-nix" --timeout "180s" --upload -- /bin/sh -lc '
479479+set -eu
480480+export HOME=/workspace
481481+store_path=$1
482482+nix-store --realise "$store_path" >/dev/null
483483+echo "substituted=$(cat "$store_path")"
484484+built=$(nix-build -E "derivation { name = \"alpine-built\"; system = \"x86_64-linux\"; builder = \"/bin/sh\"; args = [ \"-c\" \"echo built-on-alpine > \$out\" ]; }" --no-out-link)
485485+echo "built=$built"
486486+' sh "$test_store_path") || return 1
487487+488488+ if ! echo "$out" | strip_ansi | grep -q "substituted=hello from cache to alpine"; then
489489+ echo "error: alpine guest did not substitute the test path from the cache" >&2
490490+ echo "$out" | strip_ansi >&2
491491+ return 1
492492+ fi
493493+494494+ local built_path
495495+ built_path=$(echo "$out" | strip_ansi | grep -o 'built=/nix/store/[a-z0-9]*-alpine-built' | cut -d= -f2)
496496+ if [ -z "$built_path" ]; then
497497+ echo "error: could not find built store path in alpine guest output" >&2
498498+ echo "$out" | strip_ansi >&2
499499+ return 1
500500+ fi
501501+502502+ local hash
503503+ hash=$(basename "$built_path" | cut -d'-' -f1)
504504+ if ! curl -s -f "http://127.0.0.1:8501/${hash}.narinfo" > /dev/null; then
505505+ echo "error: alpine-built store path was not uploaded to the binary cache" >&2
506506+ return 1
507507+ fi
508508+ echo "success: alpine guest substituted from cache, built a derivation, and uploaded it"
509509+}
510510+472511test_case "alpine" test_alpine
512512+test_case "alpine-nix" test_alpine_nix
473513test_case "realize" test_realize
474514test_case "build-upload" test_build_upload
475515test_case "networking" test_networking