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
320pub fn remove(catalog: &Catalog, id: RuntimeId, name: Option<&str>) -> Result<Vec<String>, String> {
321    let root = catalog.package_dir(id.base());
322    let all = list(catalog, id);
323    match name {
324        Some(n) => {
325            let Some(i) = all.iter().find(|i| i.name == n) else {
326                return Err(format!("{} has no install {n} (installed: {})", id.base(), names(&all)));
327            };
328            std::fs::remove_dir_all(&i.dir).map_err(|e| format!("{}: {e}", i.dir.display()))?;
329            if pinned(catalog, id).is_none() {
330                let _ = std::fs::remove_file(root.join("pinned"));
331            }
332            refresh_current(catalog, id)?;
333            Ok(vec![n.to_string()])
334        }
335        None => {
336            if all.is_empty() {
337                return Err(format!("{} is not installed", id.base()));
338            }
339            std::fs::remove_dir_all(&root).map_err(|e| format!("{}: {e}", root.display()))?;
340            Ok(all.into_iter().map(|i| i.name).collect())
341        }
342    }
343}
344
345fn last_lines(stderr: &[u8]) -> Option<String> {
346    let text = String::from_utf8_lossy(stderr);
347    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).rev().take(3).collect();
348    (!lines.is_empty()).then(|| lines.into_iter().rev().collect::<Vec<_>>().join(" / "))
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use std::os::unix::fs::PermissionsExt;
355
356    const V: u16 = superfluid_agent::client::PROTO_VERSION;
357
358    fn scratch(tag: &str) -> PathBuf {
359        let d = std::env::temp_dir().join(format!("superfluid-installs-{tag}-{}", std::process::id()));
360        let _ = std::fs::remove_dir_all(&d);
361        std::fs::create_dir_all(&d).unwrap();
362        d
363    }
364
365    fn plan_for(id: &str, version: &str, backend: &str) -> Plan {
366        Plan {
367            id: id.into(),
368            version: version.into(),
369            backend: backend.into(),
370            install: format!("{version}-{backend}"),
371            assets: Vec::new(),
372            tested: true,
373            why: "a test".into(),
374            extra: Value::Null,
375        }
376    }
377
378    fn installer(dir: &Path, id: &str, devices: &str, broken: &str) -> PathBuf {
379        let p = dir.join(format!("superfluid-worker-{id}"));
380        let script = format!(
381            r#"#!/bin/sh
382# plan: nothing to say; install --into DIR --plan JSON
383[ "$1" = install ] || exit 2
384dir="$3"; plan="$5"
385echo "$dir" >> "$0.staged"
386backend=$(printf '%s' "$plan" | sed -E 's/.*"backend":"([^"]*)".*/\1/')
387install=$(printf '%s' "$plan" | sed -E 's/.*"install":"([^"]*)".*/\1/')
388version=$(printf '%s' "$plan" | sed -E 's/.*"version":"([^"]*)".*/\1/')
389case " {broken} " in *" $backend "*) echo "cannot fetch $backend" >&2; exit 1;; esac
390devices=$(printf '%s' '{devices}' | sed -E "s/.*\"$backend\":\[([^]]*)\].*/\1/")
391mkdir -p "$dir/bin"
392cat > "$dir/bin/superfluid-worker-{id}" <<EOF
393#!/bin/sh
394echo '{{"runtime":{{"id":"{id}","version":"$version"}},"link_w":[{V}],"available":true,"devices":[$devices]}}'
395EOF
396chmod +x "$dir/bin/superfluid-worker-{id}"
397echo '{{"id":"{id}","install":"'$install'","version":"'$version'","backend":"'$backend'","link_w":[{V}],"platforms":[],"worker":"bin/superfluid-worker-{id}","sources":[],"tested":true}}' > "$dir/runtime.json"
398cat "$dir/runtime.json"
399"#
400        );
401        std::fs::write(&p, script).unwrap();
402        std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap();
403        p
404    }
405
406    fn device(backend: &str) -> String {
407        format!(r#"{{"backend":"{backend}","name":"x","memory":1}}"#)
408    }
409
410    #[test]
411    fn installs_live_side_by_side_and_the_best_ranked_is_the_default_until_one_is_pinned() {
412        let dir = scratch("side");
413        let home = dir.join("home");
414        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
415        let id = RuntimeId::new("toyrt");
416        let devices = format!(r#"{{"cpu":[{}],"vulkan":[{},{}]}}"#, device("CPU"), device("Vulkan"), device("CPU"));
417        let w = installer(&dir, "toyrt", &devices, "");
418        let cpu = install(&cat, id, &w, &[plan_for("toyrt", "b2", "cpu")]).unwrap();
419        assert_eq!((cpu.install.name.as_str(), cpu.default.as_deref(), cpu.replaced), ("b2-cpu", Some("b2-cpu"), false));
420        let vk = install(&cat, id, &w, &[plan_for("toyrt", "b1", "vulkan")]).unwrap();
421        assert_eq!(vk.default.as_deref(), Some("b1-vulkan"), "a GPU backend outranks the CPU, older or not");
422        assert_eq!(std::fs::read_link(home.join("runtimes/toyrt/current")).unwrap(), Path::new("b1-vulkan"));
423        assert_eq!(list(&cat, id).iter().map(|i| i.name.as_str()).collect::<Vec<_>>(), ["b1-vulkan", "b2-cpu"]);
424
425        pin(&cat, id, "b2-cpu").unwrap();
426        assert_eq!(default(&cat, id).unwrap().name, "b2-cpu");
427        assert_eq!(install(&cat, id, &w, &[plan_for("toyrt", "b3", "vulkan")]).unwrap().default.as_deref(), Some("b2-cpu"), "a pin holds");
428        assert_eq!(unpin(&cat, id).unwrap().as_deref(), Some("b3-vulkan"));
429        assert!(pin(&cat, id, "b9-cuda").unwrap_err().contains("has no install b9-cuda"));
430
431        assert_eq!(cat.require(id).unwrap().version.as_deref(), Some("b3"));
432        let at = RuntimeId::new("toyrt@b2-cpu");
433        assert_eq!(cat.require(at).unwrap().version.as_deref(), Some("b2"));
434
435        assert_eq!(remove(&cat, id, Some("b3-vulkan")).unwrap(), ["b3-vulkan"]);
436        assert_eq!(default(&cat, id).unwrap().name, "b1-vulkan");
437        assert_eq!(remove(&cat, id, None).unwrap().len(), 2);
438        assert!(!home.join("runtimes/toyrt").exists());
439        let _ = std::fs::remove_dir_all(&dir);
440    }
441
442    #[test]
443    fn a_build_that_finds_no_device_falls_back_and_a_failed_fetch_stops() {
444        let dir = scratch("fallback");
445        let home = dir.join("home");
446        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
447        let id = RuntimeId::new("toyrt");
448        let devices = format!(r#"{{"cuda-13.4":[{}],"vulkan":[{}],"cpu":[{}]}}"#, device("CPU"), device("Vulkan"), device("CPU"));
449        let w = installer(&dir, "toyrt", &devices, "");
450        let plans = [plan_for("toyrt", "b1", "cuda-13.4"), plan_for("toyrt", "b1", "vulkan"), plan_for("toyrt", "b1", "cpu")];
451        let got = install(&cat, id, &w, &plans).unwrap();
452        assert_eq!(got.install.name, "b1-vulkan");
453        assert_eq!(got.fell_back.len(), 1);
454        assert!(got.fell_back[0].1.contains("finds no cuda-13.4 device here (it sees: CPU)"), "{:?}", got.fell_back);
455
456        let broken = installer(&dir, "toyrt", &devices, "cuda-13.4");
457        let e = install(&cat, id, &broken, &plans).unwrap_err();
458        assert!(e.contains("installing toyrt b1-cuda-13.4") && e.contains("install failed"), "{e}");
459        assert_eq!(list(&cat, id).len(), 1, "what was installed stays");
460        let left: Vec<String> =
461            std::fs::read_dir(home.join("runtimes/toyrt")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
462        assert!(left.iter().all(|n| !n.starts_with(".staging")), "no staging left behind: {left:?}");
463        let _ = std::fs::remove_dir_all(&dir);
464    }
465    #[test]
466    fn an_install_past_the_tested_builds_is_not_updated_back() {
467        assert_eq!(not_an_update("b11284", "b11300"), None);
468        assert_eq!(not_an_update("0.2.6", "0.3.0"), None);
469        assert!(not_an_update("b12000", "b11284").unwrap().contains("newer than b11284"));
470        assert!(not_an_update("custom", "b11284").unwrap().contains("a build of your own"));
471        assert_eq!(not_an_update("b11284", "b11284"), None);
472    }
473
474    #[test]
475    fn an_install_is_named_by_a_name_an_install_can_have() {
476        let dir = scratch("names");
477        let home = dir.join("home");
478        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
479        let id = RuntimeId::new("toyrt");
480        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
481        for bad in ["../x-cpu", "a/b", ".hidden", "b1 cpu", ""] {
482            let mut plan = plan_for("toyrt", "b1", "cpu");
483            plan.install = bad.to_string();
484            let e = install(&cat, id, &w, &[plan]).unwrap_err();
485            assert!(e.contains("is not a name an install can have"), "{bad}: {e}");
486        }
487        assert!(!home.join("runtimes/x-cpu").exists());
488        assert!(list(&cat, id).is_empty());
489        let _ = std::fs::remove_dir_all(&dir);
490    }
491
492    #[test]
493    fn an_install_clears_what_an_interrupted_one_left() {
494        let dir = scratch("stale");
495        let home = dir.join("home");
496        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
497        let id = RuntimeId::new("toyrt");
498        let root = home.join("runtimes/toyrt");
499        for stale in [".staging-4242-0", ".old-b1-cpu-4242"] {
500            std::fs::create_dir_all(root.join(stale).join("lib")).unwrap();
501        }
502        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
503        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
504        let left: Vec<String> = std::fs::read_dir(&root)
505            .unwrap()
506            .flatten()
507            .map(|e| e.file_name().to_string_lossy().into_owned())
508            .filter(|n| n.starts_with(".staging-") || n.starts_with(".old-"))
509            .collect();
510        assert!(left.is_empty(), "{left:?}");
511        let again = install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
512        assert!(again.replaced);
513        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file());
514        assert!(!std::fs::read_dir(&root).unwrap().flatten().any(|e| e.file_name().to_string_lossy().starts_with(".old-")));
515        let _ = std::fs::remove_dir_all(&dir);
516    }
517
518    #[test]
519    fn an_install_set_aside_by_an_interrupted_replacement_is_put_back() {
520        let dir = scratch("aside");
521        let home = dir.join("home");
522        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
523        let id = RuntimeId::new("toyrt");
524        let root = home.join("runtimes/toyrt");
525        let devices = format!(r#"{{"cpu":[{}]}}"#, device("CPU"));
526        let plans = [plan_for("toyrt", "b1", "cpu")];
527        let w = installer(&dir, "toyrt", &devices, "");
528        install(&cat, id, &w, &plans).unwrap();
529        std::fs::rename(root.join("b1-cpu"), root.join(".old-b1-cpu-4242")).unwrap();
530        assert!(list(&cat, id).is_empty());
531        assert_eq!(refresh_current(&cat, id).unwrap(), None);
532        let broken = installer(&dir, "toyrt", &devices, "cpu");
533        let e = install(&cat, id, &broken, &plans).unwrap_err();
534        assert!(e.contains("installing toyrt b1-cpu") && e.contains("install failed"), "{e}");
535        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file(), "the install that was there is there again");
536        assert_eq!(list(&cat, id).iter().map(|i| i.name.as_str()).collect::<Vec<_>>(), ["b1-cpu"]);
537        assert_eq!(std::fs::read_link(root.join("current")).unwrap(), Path::new("b1-cpu"), "and it is the default again");
538        assert!(!root.join(".old-b1-cpu-4242").exists());
539
540        let copied = Command::new("cp").arg("-R").arg(root.join("b1-cpu")).arg(root.join(".old-b1-cpu-4243")).status().unwrap();
541        assert!(copied.success());
542        install(&cat, id, &broken, &plans).unwrap_err();
543        assert!(!root.join(".old-b1-cpu-4243").exists());
544        assert!(root.join("b1-cpu/bin/superfluid-worker-toyrt").is_file());
545        std::fs::remove_dir_all(root.join("b1-cpu")).unwrap();
546        std::fs::create_dir_all(root.join(".old-b1-cpu-4244/lib")).unwrap();
547        install(&cat, id, &broken, &plans).unwrap_err();
548        assert!(!root.join("b1-cpu").exists() && !root.join(".old-b1-cpu-4244").exists());
549        let _ = std::fs::remove_dir_all(&dir);
550    }
551
552    #[test]
553    fn an_archive_from_elsewhere_with_no_digest_is_not_installed() {
554        let dir = scratch("unchecked");
555        let home = dir.join("home");
556        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
557        let id = RuntimeId::new("toyrt");
558        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
559        let asset = |url: &str, sha256: Option<&str>| superfluid_adapter_kit::install::Asset {
560            name: "a.tar.gz".into(),
561            url: url.into(),
562            sha256: sha256.map(str::to_string),
563            size: None,
564        };
565        let mut plan = plan_for("toyrt", "b1", "cpu");
566        plan.assets = vec![asset("https://mirror.example/a.tar.gz", None)];
567        let e = install(&cat, id, &w, &[plan.clone()]).unwrap_err();
568        assert!(e.contains("installing toyrt: https://mirror.example/a.tar.gz: no SHA-256"), "{e}");
569        assert!(!w.with_extension("staged").exists() && list(&cat, id).is_empty(), "nothing was staged");
570        plan.assets = vec![asset("https://mirror.example/a.tar.gz", Some("ab")), asset("/local/a.tar.gz", None)];
571        install(&cat, id, &w, &[plan]).unwrap();
572        let _ = std::fs::remove_dir_all(&dir);
573    }
574
575    #[test]
576    fn staging_names_are_not_predictable_and_a_planted_one_is_not_reused() {
577        let dir = scratch("names-unguessable");
578        let home = dir.join("home");
579        let cat = Catalog::new(home.clone(), None, home.join("superfluid-workerd"));
580        let id = RuntimeId::new("toyrt");
581        let root = home.join("runtimes/toyrt");
582        let w = installer(&dir, "toyrt", &format!(r#"{{"cpu":[{}]}}"#, device("CPU")), "");
583        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
584        install(&cat, id, &w, &[plan_for("toyrt", "b1", "cpu")]).unwrap();
585        let staged: Vec<String> = std::fs::read_to_string(w.with_extension("staged")).unwrap().lines().map(str::to_string).collect();
586        assert_eq!(staged.len(), 2);
587        assert_ne!(staged[0], staged[1], "each install stages somewhere new");
588        let by_pid = root.join(format!(".staging-{}-0", std::process::id())).display().to_string();
589        assert!(staged.iter().all(|s| *s != by_pid && s.starts_with(&root.join(".staging-").display().to_string())), "{staged:?}");
590
591        std::fs::create_dir_all(root.join(".current-new/x")).unwrap();
592        assert_eq!(refresh_current(&cat, id).unwrap().as_deref(), Some("b1-cpu"), "a name someone else made is left alone");
593        assert_eq!(std::fs::read_link(root.join("current")).unwrap(), Path::new("b1-cpu"));
594        assert!(root.join(".current-new/x").is_dir());
595        let _ = std::fs::remove_dir_all(&dir);
596    }
597}
598