1use 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
77fn 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