Skip to main content

superfluid_daemon/
installs.rs

1//! Runtime installs.
2
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5
6use superfluid_adapter_kit::install::{backend_rank, unchecked, version_key, Manifest, Plan, Request};
7use serde_json::Value;
8
9use crate::runtime_pick::RuntimeId;
10use crate::runtimes::{Catalog, Source, Worker};
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct Install {
14    pub name: String,
15    pub dir: PathBuf,
16    pub manifest: Manifest,
17}
18
19pub fn list(catalog: &Catalog, id: RuntimeId) -> Vec<Install> {
20    let root = catalog.package_dir(id.base());
21    let mut all: Vec<Install> = std::fs::read_dir(&root)
22        .map(|rd| {
23            rd.flatten()
24                .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
25                .filter_map(|e| {
26                    let name = e.file_name().to_str()?.to_string();
27                    if name.starts_with('.') || name == "current" {
28                        return None;
29                    }
30                    let manifest = Manifest::read(&e.path()).ok()?;
31                    Some(Install { name, dir: e.path(), manifest })
32                })
33                .collect()
34        })
35        .unwrap_or_default();
36    all.sort_by(|a, b| {
37        backend_rank(&a.manifest.backend)
38            .cmp(&backend_rank(&b.manifest.backend))
39            .then_with(|| version_key(&b.manifest.version).cmp(&version_key(&a.manifest.version)))
40            .then_with(|| a.name.cmp(&b.name))
41    });
42    all
43}
44
45pub fn pinned(catalog: &Catalog, id: RuntimeId) -> Option<String> {
46    let name = std::fs::read_to_string(catalog.package_dir(id.base()).join("pinned")).ok()?.trim().to_string();
47    list(catalog, id).into_iter().any(|i| i.name == name).then_some(name)
48}
49
50pub fn default(catalog: &Catalog, id: RuntimeId) -> Option<Install> {
51    let all = list(catalog, id);
52    let pin = pinned(catalog, id);
53    all.iter().find(|i| Some(&i.name) == pin.as_ref()).or(all.first()).cloned()
54}
55
56pub fn refresh_current(catalog: &Catalog, id: RuntimeId) -> Result<Option<String>, String> {
57    let root = catalog.package_dir(id.base());
58    let link = root.join("current");
59    match default(catalog, id) {
60        Some(d) => {
61            let tmp = root.join(format!(".current-new-{}", unguessable()?));
62            std::os::unix::fs::symlink(&d.name, &tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
63            std::fs::rename(&tmp, &link).map_err(|e| {
64                let _ = std::fs::remove_file(&tmp);
65                format!("switching current: {e}")
66            })?;
67            Ok(Some(d.name))
68        }
69        None => {
70            let _ = std::fs::remove_file(&link);
71            let _ = std::fs::remove_file(root.join("pinned"));
72            Ok(None)
73        }
74    }
75}
76
77/// A name nobody else sharing the directory can claim first.
78fn unguessable() -> Result<u64, String> {
79    use std::io::Read;
80    let mut buf = [0u8; 8];
81    std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut buf)).map_err(|e| format!("/dev/urandom: {e}"))?;
82    Ok(u64::from_le_bytes(buf))
83}
84
85pub fn pin(catalog: &Catalog, id: RuntimeId, name: &str) -> Result<(), String> {
86    let all = list(catalog, id);
87    if !all.iter().any(|i| i.name == name) {
88        return Err(format!("{} has no install {name} (installed: {})", id.base(), names(&all)));
89    }
90    let path = catalog.package_dir(id.base()).join("pinned");
91    std::fs::write(&path, format!("{name}\n")).map_err(|e| format!("{}: {e}", path.display()))?;
92    refresh_current(catalog, id).map(|_| ())
93}
94
95pub fn unpin(catalog: &Catalog, id: RuntimeId) -> Result<Option<String>, String> {
96    let _ = std::fs::remove_file(catalog.package_dir(id.base()).join("pinned"));
97    refresh_current(catalog, id)
98}
99
100fn names(all: &[Install]) -> String {
101    if all.is_empty() {
102        "none".into()
103    } else {
104        all.iter().map(|i| i.name.as_str()).collect::<Vec<_>>().join(", ")
105    }
106}
107
108pub fn installer(catalog: &Catalog, id: RuntimeId) -> Result<PathBuf, String> {
109    let id = id.base();
110    let name = crate::runtimes::worker_name(id);
111    if let Some(p) = std::env::var_os(crate::runtimes::worker_env(id)) {
112        return Ok(PathBuf::from(p));
113    }
114    let mut places: Vec<PathBuf> = Vec::new();
115    places.extend(catalog.beside().map(|dir| dir.join(&name)));
116    places.extend(catalog.shipped(id));
117    if let Some(d) = default(catalog, id) {
118        places.push(d.dir.join(&d.manifest.worker));
119    }
120    places.into_iter().find(|p| p.is_file()).ok_or_else(|| {
121        let shipped = catalog.shipped_ids();
122        if !shipped.is_empty() {
123            return format!(
124                "this superfluid ships no adapter to install the {id} runtime with (it ships: {})",
125                shipped.iter().map(|i| i.name()).collect::<Vec<_>>().join(", ")
126            );
127        }
128        format!(
129            "no {name} to install the {id} runtime with (looked beside superfluid, in a release's libexec/superfluid and in its \
130             installs; a source build makes it with `cargo build -p superfluid-adapter-{id}`)"
131        )
132    })
133}
134
135#[derive(Debug, Clone)]
136pub struct Plans {
137    pub host: String,
138    pub plans: Vec<Plan>,
139}
140
141pub fn plan(installer: &Path, req: &Request) -> Result<Plans, String> {
142    let mut cmd = Command::new(installer);
143    crate::exec::without_secrets(&mut cmd).arg("plan").args(req.to_args()).stdin(Stdio::null());
144    let out = crate::exec::when_not_busy(|| cmd.output()).map_err(|e| format!("{} could not start: {e}", installer.display()))?;
145    if !out.status.success() {
146        return Err(last_lines(&out.stderr).unwrap_or_else(|| format!("{} plan failed ({})", installer.display(), out.status)));
147    }
148    let v: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("{} plan printed no plan: {e}", installer.display()))?;
149    let plans = v["plans"].as_array().map(|a| a.iter().map(Plan::from_json).collect::<Result<Vec<_>, _>>()).transpose()?.unwrap_or_default();
150    if plans.is_empty() {
151        return Err(format!("{} has nothing to install here", installer.display()));
152    }
153    Ok(Plans { host: v["describe"].as_str().unwrap_or_default().to_string(), plans })
154}
155
156#[derive(Debug, Clone)]
157pub struct Installed {
158    pub install: Install,
159    pub version: String,
160    pub device: Option<String>,
161    pub fell_back: Vec<(String, String)>,
162    pub replaced: bool,
163    pub default: Option<String>,
164}
165
166pub fn install(catalog: &Catalog, id: RuntimeId, installer: &Path, plans: &[Plan]) -> Result<Installed, String> {
167    let id = id.base();
168    let root = catalog.package_dir(id);
169    std::fs::create_dir_all(&root).map_err(|e| format!("{}: {e}", root.display()))?;
170    if let Some(plan) = plans.iter().find(|p| !crate::runtime_pick::valid_install(&p.install)) {
171        return Err(format!(
172            "installing {id}: '{}' is not a name an install can have (letters, digits, '.', '-' and '_', not starting with '.')",
173            plan.install
174        ));
175    }
176    if let Some(why) = plans.iter().flat_map(|p| &p.assets).find_map(unchecked) {
177        return Err(format!("installing {id}: {why}"));
178    }
179    let _one_at_a_time = crate::pulls::Lock::take(&root, ".install", &format!("install of {id}"))?;
180    let mut left: Vec<PathBuf> = std::fs::read_dir(&root)
181        .map(|entries| entries.flatten().map(|e| e.path()).collect())
182        .unwrap_or_default();
183    left.retain(|p| p.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.starts_with(".staging-") || n.starts_with(".old-")));
184    left.sort();
185    let mut put_back = false;
186    for stale in left {
187        match set_aside_unreplaced(id, &root, &stale) {
188            Some(dest) => {
189                std::fs::rename(&stale, &dest).map_err(|e| {
190                    format!(
191                        "{}: an interrupted install set this copy of {} aside and it cannot be put back: {e}",
192                        stale.display(),
193                        dest.display()
194                    )
195                })?;
196                put_back = true;
197            }
198            None => {
199                let _ = std::fs::remove_dir_all(&stale);
200            }
201        }
202    }
203    if put_back {
204        refresh_current(catalog, id)?;
205    }
206    let mut fell_back = Vec::new();
207    for plan in plans {
208        let staging = root.join(format!(".staging-{}", unguessable()?));
209        std::fs::create_dir(&staging).map_err(|e| format!("{}: {e}", staging.display()))?;
210        let staged = stage(id, installer, plan, &staging);
211        let checked = staged.and_then(|manifest| verify(id, plan, &staging).map(|ok| (manifest, ok)));
212        match checked {
213            Ok((manifest, Ok((version, device)))) => {
214                let dest = root.join(&plan.install);
215                let replaced = dest.exists();
216                let aside = root.join(format!(".old-{}-{}", plan.install, unguessable()?));
217                if replaced {
218                    if let Err(e) = std::fs::rename(&dest, &aside) {
219                        let _ = std::fs::remove_dir_all(&staging);
220                        return Err(format!("{}: {e}", dest.display()));
221                    }
222                }
223                if let Err(e) = std::fs::rename(&staging, &dest) {
224                    if replaced {
225                        let _ = std::fs::rename(&aside, &dest);
226                    }
227                    let _ = std::fs::remove_dir_all(&staging);
228                    return Err(format!("{}: {e}", dest.display()));
229                }
230                if replaced {
231                    let _ = std::fs::remove_dir_all(&aside);
232                }
233                let default = refresh_current(catalog, id)?;
234                return Ok(Installed {
235                    install: Install { name: plan.install.clone(), dir: dest, manifest },
236                    version,
237                    device,
238                    fell_back,
239                    replaced,
240                    default,
241                });
242            }
243            Ok((_, Err(why))) => {
244                let _ = std::fs::remove_dir_all(&staging);
245                fell_back.push((plan.install.clone(), why));
246            }
247            Err(e) => {
248                let _ = std::fs::remove_dir_all(&staging);
249                return Err(format!("installing {id} {}: {e}", plan.install));
250            }
251        }
252    }
253    Err(format!(
254        "no {id} build runs here: {}",
255        fell_back.iter().map(|(p, why)| format!("{p}: {why}")).collect::<Vec<_>>().join("; ")
256    ))
257}
258
259fn set_aside_unreplaced(id: RuntimeId, root: &Path, aside: &Path) -> Option<PathBuf> {
260    let name = aside.file_name()?.to_str()?.strip_prefix(".old-")?;
261    let (install, pid) = name.rsplit_once('-')?;
262    if pid.is_empty() || !pid.bytes().all(|b| b.is_ascii_digit()) || !crate::runtime_pick::valid_install(install) {
263        return None;
264    }
265    let manifest = Manifest::read(aside).ok()?;
266    if manifest.id != id.name() || manifest.install != install {
267        return None;
268    }
269    let dest = root.join(install);
270    dest.symlink_metadata().is_err().then_some(dest)
271}
272
273fn stage(id: RuntimeId, installer: &Path, plan: &Plan, staging: &Path) -> Result<Manifest, String> {
274    let mut cmd = Command::new(installer);
275    crate::exec::without_secrets(&mut cmd).arg("install").arg("--into").arg(staging).arg("--plan").arg(plan.to_json().to_string()).stdin(Stdio::null()).stderr(Stdio::inherit());
276    let out = crate::exec::when_not_busy(|| cmd.output()).map_err(|e| format!("{} could not start: {e}", installer.display()))?;
277    if !out.status.success() {
278        return Err(format!("{} install failed ({})", installer.display(), out.status));
279    }
280    let manifest = Manifest::read(staging)?;
281    if manifest.id != id.name() || manifest.install != plan.install {
282        return Err(format!("the install made {} {}, the plan says {id} {}", manifest.id, manifest.install, plan.install));
283    }
284    Ok(manifest)
285}
286
287fn verify(id: RuntimeId, plan: &Plan, staging: &Path) -> Result<Result<(String, Option<String>), String>, String> {
288    let worker = Worker { bin: staging.join("bin").join(crate::runtimes::worker_name(id)), prefix: Vec::new(), source: Source::Installed };
289    if !worker.bin.is_file() {
290        return Err(format!("the install has no {}", worker.bin.display()));
291    }
292    let runtime = match crate::runtimes::check_worker(id, &worker) {
293        Ok(r) => r,
294        Err(why) => return Ok(Err(why)),
295    };
296    let family = plan.backend.split(['-', '_']).next().unwrap_or(&plan.backend).to_ascii_lowercase();
297    let device = runtime.devices.iter().find(|d| family == "cpu" || d.backend.eq_ignore_ascii_case(&family));
298    match device {
299        Some(d) => Ok(Ok((runtime.version.unwrap_or_default(), Some(d.summary())))),
300        None => Ok(Err(format!(
301            "its check finds no {} device here (it sees: {})",
302            plan.backend,
303            if runtime.devices.is_empty() {
304                "none".to_string()
305            } else {
306                runtime.devices.iter().map(|d| d.backend.clone()).collect::<Vec<_>>().join(", ")
307            }
308        ))),
309    }
310}
311
312pub fn not_an_update(current: &str, planned: &str) -> Option<String> {
313    if current == "custom" {
314        return Some("it is a build of your own, not one of the adapter's".to_string());
315    }
316    (version_key(current) > version_key(planned))
317        .then(|| format!("it is {current}, newer than {planned}, the newest build this adapter is tested with"))
318}
319
320/// Removes the install `name`, or the one `id` names after its '@'; every install of the runtime
321/// only when neither names one.
322pub fn remove(catalog: &Catalog, id: RuntimeId, name: Option<&str>) -> Result<Vec<String>, String> {
323    let name = match (id.install(), name) {
324        (Some(at), Some(n)) if at != n => return Err(format!("{id} names the install {at}, not {n}")),
325        (at, n) => n.or(at),
326    };
327    let root = catalog.package_dir(id.base());
328    let all = list(catalog, id);
329    match name {
330        Some(n) => {
331            let Some(i) = all.iter().find(|i| i.name == n) else {
332                return Err(format!("{} has no install {n} (installed: {})", id.base(), names(&all)));
333            };
334            std::fs::remove_dir_all(&i.dir).map_err(|e| format!("{}: {e}", i.dir.display()))?;
335            if pinned(catalog, id).is_none() {
336                let _ = std::fs::remove_file(root.join("pinned"));
337            }
338            refresh_current(catalog, id)?;
339            Ok(vec![n.to_string()])
340        }
341        None => {
342            if all.is_empty() {
343                return Err(format!("{} is not installed", id.base()));
344            }
345            std::fs::remove_dir_all(&root).map_err(|e| format!("{}: {e}", root.display()))?;
346            Ok(all.into_iter().map(|i| i.name).collect())
347        }
348    }
349}
350
351fn last_lines(stderr: &[u8]) -> Option<String> {
352    let text = String::from_utf8_lossy(stderr);
353    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).rev().take(3).collect();
354    (!lines.is_empty()).then(|| lines.into_iter().rev().collect::<Vec<_>>().join(" / "))
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use std::os::unix::fs::PermissionsExt;
361
362    const V: u16 = superfluid_agent::client::PROTO_VERSION;
363
364    fn scratch(tag: &str) -> PathBuf {
365        let d = std::env::temp_dir().join(format!("superfluid-installs-{tag}-{}", std::process::id()));
366        let _ = std::fs::remove_dir_all(&d);
367        std::fs::create_dir_all(&d).unwrap();
368        d
369    }
370
371    fn plan_for(id: &str, version: &str, backend: &str) -> Plan {
372        Plan {
373            id: id.into(),
374            version: version.into(),
375            backend: backend.into(),
376            install: format!("{version}-{backend}"),
377            assets: Vec::new(),
378            tested: true,
379            why: "a test".into(),
380            extra: Value::Null,
381        }
382    }
383
384    fn installer(dir: &Path, id: &str, devices: &str, broken: &str) -> PathBuf {
385        let p = dir.join(format!("superfluid-worker-{id}"));
386        let script = format!(
387            r#"#!/bin/sh
388# plan: nothing to say; install --into DIR --plan JSON
389[ "$1" = install ] || exit 2
390dir="$3"; plan="$5"
391echo "$dir" >> "$0.staged"
392backend=$(printf '%s' "$plan" | sed -E 's/.*"backend":"([^"]*)".*/\1/')
393install=$(printf '%s' "$plan" | sed -E 's/.*"install":"([^"]*)".*/\1/')
394version=$(printf '%s' "$plan" | sed -E 's/.*"version":"([^"]*)".*/\1/')
395case " {broken} " in *" $backend "*) echo "cannot fetch $backend" >&2; exit 1;; esac
396devices=$(printf '%s' '{devices}' | sed -E "s/.*\"$backend\":\[([^]]*)\].*/\1/")
397mkdir -p "$dir/bin"
398cat > "$dir/bin/superfluid-worker-{id}" <<EOF
399#!/bin/sh
400echo '{{"runtime":{{"id":"{id}","version":"$version"}},"link_w":[{V}],"available":true,"devices":[$devices]}}'
401EOF
402chmod +x "$dir/bin/superfluid-worker-{id}"
403echo '{{"id":"{id}","install":"'$install'","version":"'$version'","backend":"'$backend'","link_w":[{V}],"platforms":[],"worker":"bin/superfluid-worker-{id}","sources":[],"tested":true}}' > "$dir/runtime.json"
404cat "$dir/runtime.json"
405"#
406        );
407        std::fs::write(&p, script).unwrap();
408        std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
409        p
410    }
411
412    fn device(backend: &str) -> String {
413        format!(r#"{{"backend":"{backend}","name":"x","memory":1}}"#)
414    }
415
416    #[test]
417    fn installs_live_side_by_side_and_the_best_ranked_is_the_default_until_one_is_pinned() {
418        let dir = scratch("side");
419        let home = dir.join("home");
420        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
421        let id = RuntimeId::new("toyrt");
422        let devices = format!(r#"{{"cpu":[{}],"vulkan":[{},{}]}}"#, device("CPU"), device("Vulkan"), device("CPU"));
423        let w = installer(&dir, "toyrt", &devices, "");
424        let cpu = install(&cat, id, &w, &[plan_for("toyrt", "b2", "cpu")]).unwrap();
425        assert_eq!((cpu.install.name.as_str(), cpu.default.as_deref(), cpu.replaced), ("b2-cpu", Some("b2-cpu"), false));
426        let vk = install(&cat, id, &w, &[plan_for("toyrt", "b1", "vulkan")]).unwrap();
427        assert_eq!(vk.default.as_deref(), Some("b1-vulkan"), "a GPU backend outranks the CPU, older or not");
428        assert_eq!(std::fs::read_link(home.join("runtimes/toyrt/current")).unwrap(), Path::new("b1-vulkan"));
429        assert_eq!(list(&cat, id).iter().map(|i| i.name.as_str()).collect::<Vec<_>>(), ["b1-vulkan", "b2-cpu"]);
430
431        pin(&cat, id, "b2-cpu").unwrap();
432        assert_eq!(default(&cat, id).unwrap().name, "b2-cpu");
433        assert_eq!(install(&cat, id, &w, &[plan_for("toyrt", "b3", "vulkan")]).unwrap().default.as_deref(), Some("b2-cpu"), "a pin holds");
434        assert_eq!(unpin(&cat, id).unwrap().as_deref(), Some("b3-vulkan"));
435        assert!(pin(&cat, id, "b9-cuda").unwrap_err().contains("has no install b9-cuda"));
436
437        assert_eq!(cat.require(id).unwrap().version.as_deref(), Some("b3"));
438        let at = RuntimeId::new("toyrt@b2-cpu");
439        assert_eq!(cat.require(at).unwrap().version.as_deref(), Some("b2"));
440
441        assert_eq!(remove(&cat, id, Some("b3-vulkan")).unwrap(), ["b3-vulkan"]);
442        assert_eq!(default(&cat, id).unwrap().name, "b1-vulkan");
443        assert_eq!(remove(&cat, at, Some("b1-vulkan")).unwrap_err(), "toyrt@b2-cpu names the install b2-cpu, not b1-vulkan");
444        let typo = RuntimeId::new("toyrt@b2-cpuu");
445        assert_eq!(remove(&cat, typo, None).unwrap_err(), "toyrt has no install b2-cpuu (installed: b1-vulkan, b2-cpu)");
446        assert_eq!(list(&cat, id).len(), 2, "a mistyped install removes nothing");
447        assert_eq!(remove(&cat, at, None).unwrap(), ["b2-cpu"], "an id with an install removes that one only");
448        assert_eq!(remove(&cat, id, None).unwrap(), ["b1-vulkan"]);
449        assert!(!home.join("runtimes/toyrt").exists());
450        let _ = std::fs::remove_dir_all(&dir);
451    }
452
453    #[test]
454    fn a_build_that_finds_no_device_falls_back_and_a_failed_fetch_stops() {
455        let dir = scratch("fallback");
456        let home = dir.join("home");
457        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
458        let id = RuntimeId::new("toyrt");
459        let devices = format!(r#"{{"cuda-13.4":[{}],"vulkan":[{}],"cpu":[{}]}}"#, device("CPU"), device("Vulkan"), device("CPU"));
460        let w = installer(&dir, "toyrt", &devices, "");
461        let plans = [plan_for("toyrt", "b1", "cuda-13.4"), plan_for("toyrt", "b1", "vulkan"), plan_for("toyrt", "b1", "cpu")];
462        let got = install(&cat, id, &w, &plans).unwrap();
463        assert_eq!(got.install.name, "b1-vulkan");
464        assert_eq!(got.fell_back.len(), 1);
465        assert!(got.fell_back[0].1.contains("finds no cuda-13.4 device here (it sees: CPU)"), "{:?}", got.fell_back);
466
467        let broken = installer(&dir, "toyrt", &devices, "cuda-13.4");
468        let e = install(&cat, id, &broken, &plans).unwrap_err();
469        assert!(e.contains("installing toyrt b1-cuda-13.4") && e.contains("install failed"), "{e}");
470        assert_eq!(list(&cat, id).len(), 1, "what was installed stays");
471        let left: Vec<String> =
472            std::fs::read_dir(home.join("runtimes/toyrt")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
473        assert!(left.iter().all(|n| !n.starts_with(".staging")), "no staging left behind: {left:?}");
474        let _ = std::fs::remove_dir_all(&dir);
475    }
476    #[test]
477    fn an_install_past_the_tested_builds_is_not_updated_back() {
478        assert_eq!(not_an_update("b11284", "b11300"), None);
479        assert_eq!(not_an_update("0.2.6", "0.3.0"), None);
480        assert!(not_an_update("b12000", "b11284").unwrap().contains("newer than b11284"));
481        assert!(not_an_update("custom", "b11284").unwrap().contains("a build of your own"));
482        assert_eq!(not_an_update("b11284", "b11284"), None);
483    }
484
485    #[test]
486    fn an_install_is_named_by_a_name_an_install_can_have() {
487        let dir = scratch("names");
488        let home = dir.join("home");
489        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
490        let id = RuntimeId::new("toyrt");
491        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
492        for bad in ["../x-cpu", "a/b", ".hidden", "b1 cpu", ""] {
493            let mut plan = plan_for("toyrt", "b1", "cpu");
494            plan.install = bad.to_string();
495            let e = install(&cat, id, &w, &[plan]).unwrap_err();
496            assert!(e.contains("is not a name an install can have"), "{bad}: {e}");
497        }
498        assert!(!home.join("runtimes/x-cpu").exists());
499        assert!(list(&cat, id).is_empty());
500        let _ = std::fs::remove_dir_all(&dir);
501    }
502
503    #[test]
504    fn an_install_clears_what_an_interrupted_one_left() {
505        let dir = scratch("stale");
506        let home = dir.join("home");
507        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
508        let id = RuntimeId::new("toyrt");
509        let root = home.join("runtimes/toyrt");
510        for stale in [".staging-4242-0", ".old-b1-cpu-4242"] {
511            std::fs::create_dir_all(root.join(stale).join("lib")).unwrap();
512        }
513        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
514        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
515        let left: Vec<String> = std::fs::read_dir(&root)
516            .unwrap()
517            .flatten()
518            .map(|e| e.file_name().to_string_lossy().into_owned())
519            .filter(|n| n.starts_with(".staging-") || n.starts_with(".old-"))
520            .collect();
521        assert!(left.is_empty(), "{left:?}");
522        let again = install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
523        assert!(again.replaced);
524        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file());
525        assert!(!std::fs::read_dir(&root).unwrap().flatten().any(|e| e.file_name().to_string_lossy().starts_with(".old-")));
526        let _ = std::fs::remove_dir_all(&dir);
527    }
528
529    #[test]
530    fn an_install_set_aside_by_an_interrupted_replacement_is_put_back() {
531        let dir = scratch("aside");
532        let home = dir.join("home");
533        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
534        let id = RuntimeId::new("toyrt");
535        let root = home.join("runtimes/toyrt");
536        let devices = format!(r#"{{"cpu":[{}]}}"#, device("CPU"));
537        let plans = [plan_for("toyrt", "b1", "cpu")];
538        let w = installer(&dir, "toyrt", &devices, "");
539        install(&cat, id, &w, &plans).unwrap();
540        std::fs::rename(root.join("b1-cpu"), root.join(".old-b1-cpu-4242")).unwrap();
541        assert!(list(&cat, id).is_empty());
542        assert_eq!(refresh_current(&cat, id).unwrap(), None);
543        let broken = installer(&dir, "toyrt", &devices, "cpu");
544        let e = install(&cat, id, &broken, &plans).unwrap_err();
545        assert!(e.contains("installing toyrt b1-cpu") && e.contains("install failed"), "{e}");
546        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file(), "the install that was there is there again");
547        assert_eq!(list(&cat, id).iter().map(|i| i.name.as_str()).collect::<Vec<_>>(), ["b1-cpu"]);
548        assert_eq!(std::fs::read_link(root.join("current")).unwrap(), Path::new("b1-cpu"), "and it is the default again");
549        assert!(!root.join(".old-b1-cpu-4242").exists());
550
551        let copied = Command::new("cp").arg("-R").arg(root.join("b1-cpu")).arg(root.join(".old-b1-cpu-4243")).status().unwrap();
552        assert!(copied.success());
553        install(&cat, id, &broken, &plans).unwrap_err();
554        assert!(!root.join(".old-b1-cpu-4243").exists());
555        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file());
556        std::fs::remove_dir_all(root.join("b1-cpu")).unwrap();
557        std::fs::create_dir_all(root.join(".old-b1-cpu-4244/lib")).unwrap();
558        install(&cat, id, &broken, &plans).unwrap_err();
559        assert!(!root.join("b1-cpu").exists() && !root.join(".old-b1-cpu-4244").exists());
560        let _ = std::fs::remove_dir_all(&dir);
561    }
562
563    #[test]
564    fn an_archive_from_elsewhere_with_no_digest_is_not_installed() {
565        let dir = scratch("unchecked");
566        let home = dir.join("home");
567        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
568        let id = RuntimeId::new("toyrt");
569        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
570        let asset = |url: &str, sha256: Option<&str>| superfluid_adapter_kit::install::Asset {
571            name: "a.tar.gz".into(),
572            url: url.into(),
573            sha256: sha256.map(str::to_string),
574            size: None,
575        };
576        let mut plan = plan_for("toyrt", "b1", "cpu");
577        plan.assets = vec![asset("https://mirror.example/a.tar.gz", None)];
578        let e = install(&cat, id, &w, &[plan.clone()]).unwrap_err();
579        assert!(e.contains("installing toyrt: https://mirror.example/a.tar.gz: no SHA-256"), "{e}");
580        assert!(!w.with_extension("staged").exists() && list(&cat, id).is_empty(), "nothing was staged");
581        plan.assets = vec![asset("https://mirror.example/a.tar.gz", Some("ab")), asset("/local/a.tar.gz", None)];
582        install(&cat, id, &w, &[plan]).unwrap();
583        let _ = std::fs::remove_dir_all(&dir);
584    }
585
586    #[test]
587    fn staging_names_are_not_predictable_and_a_planted_one_is_not_reused() {
588        let dir = scratch("names-unguessable");
589        let home = dir.join("home");
590        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
591        let id = RuntimeId::new("toyrt");
592        let root = home.join("runtimes/toyrt");
593        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
594        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
595        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
596        let staged: Vec<String> = std::fs::read_to_string(w.with_extension("staged")).unwrap().lines().map(str::to_string).collect();
597        assert_eq!(staged.len(), 2);
598        assert_ne!(staged[0], staged[1], "each install stages somewhere new");
599        let by_pid = root.join(format!(".staging-{}-0", std::process::id())).display().to_string();
600        assert!(staged.iter().all(|s| *s != by_pid && s.starts_with(&root.join(".staging-").display().to_string())), "{staged:?}");
601
602        std::fs::create_dir_all(root.join(".current-new/x")).unwrap();
603        assert_eq!(refresh_current(&cat, id).unwrap().as_deref(), Some("b1-cpu"), "a name someone else made is left alone");
604        assert_eq!(std::fs::read_link(root.join("current")).unwrap(), Path::new("b1-cpu"));
605        assert!(root.join(".current-new/x").is_dir());
606        let _ = std::fs::remove_dir_all(&dir);
607    }
608}
609