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> {
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