1use std::path::{Path, PathBuf};
4
5use superfluid_adapter_kit::Pull;
6
7use crate::runtime_pick::RuntimeId;
8use crate::runtimes::{Catalog, Source};
9
10pub fn looks_like_id(token: &str) -> bool {
11 if token.is_empty() || Path::new(token).exists() {
12 return false;
13 }
14 let file = token.to_ascii_lowercase();
15 if token.starts_with(['.', '/', '~']) || file.ends_with(".base") || file.ends_with(".gguf") {
16 return false;
17 }
18 let id = token.split_once(':').map_or(token, |(id, _)| id);
19 id.contains('/')
20}
21
22pub fn served_name(id: &str) -> String {
23 id.split_once(':').map_or(id, |(id, _)| id).to_string()
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct PullFlags {
28 pub offline: bool,
29 pub options: Vec<(String, Option<String>)>,
30}
31
32impl PullFlags {
33 fn check(&self, runtime: RuntimeId, declared: &[(String, bool)]) -> Result<(), String> {
34 for (name, value) in &self.options {
35 let Some((_, takes)) = declared.iter().find(|(n, _)| n == name) else {
36 let all: Vec<String> = declared.iter().map(|(n, _)| format!("--pull-{n}")).collect();
37 return Err(format!(
38 "--pull-{name}: the {} runtime's pull takes no such option (it takes: {})",
39 runtime.base(),
40 if all.is_empty() { "none".to_string() } else { all.join(", ") }
41 ));
42 };
43 match (takes, value) {
44 (true, None) => return Err(format!("--pull-{name} needs a value")),
45 (false, Some(v)) => return Err(format!("--pull-{name} takes no value (got {v:?})")),
46 _ => {}
47 }
48 }
49 Ok(())
50 }
51}
52
53pub fn ensure_installed(catalog: &Catalog, runtime: RuntimeId, offline: bool) -> Result<crate::runtimes::Runtime, String> {
56 match catalog.require(runtime) {
57 Ok(rt) => Ok(rt),
58 Err(why) if !never_installed(catalog, runtime) => Err(why),
59 Err(why) if offline => Err(format!("{why} (offline: nothing was installed)")),
60 Err(why) => match named_by_the_operator(catalog, runtime) {
61 Some(by) => Err(format!("{why} ({by} names the runtime to load, so none was installed)")),
62 None => {
63 install(catalog, runtime).map_err(|e| format!("{why}; and installing it failed: {e}"))?;
64 catalog.require(runtime)
65 }
66 },
67 }
68}
69
70pub fn pull(catalog: &Catalog, runtime: RuntimeId, id: &str, flags: &PullFlags) -> Result<PathBuf, String> {
71 let rt = ensure_installed(catalog, runtime, flags.offline)?;
72 let Some(declared) = &rt.pull else {
73 return Err(format!("the {runtime} runtime pulls no model by id: give --model a path to one of its models"));
74 };
75 flags.check(runtime, declared)?;
76 let request = Pull { id: id.to_string(), offline: flags.offline, options: flags.options.clone() };
77 let _one_at_a_time = Lock::take(&catalog.home().join("pulls"), id, &format!("pull of {id}"))?;
78 eprintln!("superfluid: pulling {id} with the {runtime} runtime{}", if flags.offline { " (offline)" } else { "" });
79 let path = if rt.worker.source == Source::BuiltIn {
80 let linked = crate::linked::get(runtime.base()).ok_or_else(|| format!("this build does not link the {runtime} runtime"))?;
81 linked.pull(&request, &superfluid_adapter_kit::WorkerArgs::default())?
82 } else {
83 let mut cmd = std::process::Command::new(&rt.worker.bin);
84 cmd.arg("pull").args(request.to_args()).stdin(std::process::Stdio::null()).stderr(std::process::Stdio::inherit());
85 let out = crate::exec::when_not_busy(|| cmd.output()).map_err(|e| format!("{} could not start: {e}", rt.worker.bin.display()))?;
86 if !out.status.success() {
87 return Err(format!("pulling {id} with the {runtime} runtime failed (its reason is above)"));
88 }
89 let v: serde_json::Value =
90 serde_json::from_slice(&out.stdout).map_err(|e| format!("{} pull printed no path: {e}", rt.worker.bin.display()))?;
91 PathBuf::from(v["path"].as_str().ok_or_else(|| format!("{} pull printed no path", rt.worker.bin.display()))?)
92 };
93 if !path.exists() {
94 return Err(format!("pulling {id} gave {}, which is not there", path.display()));
95 }
96 eprintln!("superfluid: {id} is {}", path.display());
97 Ok(path)
98}
99
100fn never_installed(catalog: &Catalog, runtime: RuntimeId) -> bool {
101 if runtime.install().is_some() {
102 return false;
103 }
104 match catalog.locate(runtime) {
105 None => true,
106 Some(worker) if worker.source == Source::Env => false,
107 Some(_) => crate::installs::list(catalog, runtime).is_empty(),
108 }
109}
110
111fn named_by_the_operator(catalog: &Catalog, runtime: RuntimeId) -> Option<String> {
112 catalog.inspect(runtime).info.and_then(|info| info.named)
113}
114
115fn install(catalog: &Catalog, runtime: RuntimeId) -> Result<(), String> {
116 let id = runtime.base();
117 eprintln!("superfluid: the {id} runtime is not here: installing it (`superfluid runtime install {id}`)");
118 let installer = crate::installs::installer(catalog, id)?;
119 let plans = crate::installs::plan(&installer, &superfluid_adapter_kit::install::Request::default())?;
120 let done = crate::installs::install(catalog, id, &installer, &plans.plans)?;
121 eprintln!("superfluid: installed {id} {} ({})", done.install.name, done.version);
122 Ok(())
123}
124
125pub(crate) struct Lock(#[allow(dead_code)] std::fs::File);
126
127impl Lock {
128 pub(crate) fn take(dir: &Path, id: &str, what: &str) -> Result<Lock, String> {
129 use std::os::unix::io::AsRawFd;
130 std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
131 let name: String = id.chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') { c } else { '_' }).collect();
132 let path = dir.join(format!("{name}.lock"));
133 let file = std::fs::OpenOptions::new()
134 .create(true)
135 .truncate(false)
136 .write(true)
137 .open(&path)
138 .map_err(|e| format!("{}: {e}", path.display()))?;
139 unsafe {
142 if libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) != 0 {
143 eprintln!("superfluid: another {what} is running; waiting for it");
144 if libc::flock(file.as_raw_fd(), libc::LOCK_EX) != 0 {
145 return Err(format!("{}: {}", path.display(), std::io::Error::last_os_error()));
146 }
147 }
148 }
149 Ok(Lock(file))
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn ids_are_told_from_paths_as_basert_tells_them() {
159 for id in ["Qwen/Qwen3-0.6B", "unsloth/Qwen3-0.6B-GGUF:Q4_K_M", "mlx-community/Qwen3-4B-4bit:main"] {
160 assert!(looks_like_id(id), "{id}");
161 }
162 for path in ["", "./m.gguf", "/models/m.gguf", "~/m", "org/m.base", "model.gguf", "Qwen3:Q4", "models/qwen3.gguf", "models/Qwen3.GGUF"] {
163 assert!(!looks_like_id(path), "{path}");
164 }
165 let dir = std::env::temp_dir().join(format!("superfluid-pulls-{}", std::process::id()));
166 std::fs::create_dir_all(dir.join("org")).unwrap();
167 std::fs::write(dir.join("org/m"), b"").unwrap();
168 let on_disk = dir.join("org/m");
169 assert!(!looks_like_id(&on_disk.to_string_lossy()), "a path on disk is a path");
170 let _ = std::fs::remove_dir_all(&dir);
171 assert_eq!(served_name("unsloth/Qwen3-0.6B-GGUF:Q4_K_M"), "unsloth/Qwen3-0.6B-GGUF");
172 }
173
174 #[test]
175 fn pull_options_are_checked_against_what_the_runtime_declares() {
176 let declared = vec![("target".to_string(), true), ("force".to_string(), false)];
177 let rt = RuntimeId::new("basert");
178 let flags = |options: Vec<(&str, Option<&str>)>| PullFlags {
179 offline: false,
180 options: options.into_iter().map(|(n, v)| (n.to_string(), v.map(str::to_string))).collect(),
181 };
182 assert_eq!(flags(vec![("target", Some("base-q8")), ("force", None)]).check(rt, &declared), Ok(()));
183 assert!(flags(vec![("quant", Some("q8"))]).check(rt, &declared).unwrap_err().contains("takes no such option (it takes: --pull-target, --pull-force)"));
184 assert!(flags(vec![("target", None)]).check(rt, &declared).unwrap_err().contains("needs a value"));
185 assert!(flags(vec![("force", Some("yes"))]).check(rt, &declared).unwrap_err().contains("takes no value"));
186 }
187
188 fn toy_worker(dir: &Path, model: &Path) -> PathBuf {
189 use std::os::unix::fs::PermissionsExt;
190 let v = superfluid_agent::client::PROTO_VERSION;
191 let bin = dir.join("superfluid-worker-toyrt");
192 let script = format!(
193 r#"#!/bin/sh
194case "$1" in
195 check) echo '{{"runtime":{{"id":"toyrt","version":"1"}},"link_w":[{v}],"available":true,"formats":[{{"id":"toy","describe":"a toy file","file":{{"extensions":["toy"]}}}}],"tokenizer":{{"from":"huggingface"}},"pull":{{"options":[{{"name":"target","value":true,"about":"x"}}]}}}}' ;;
196 pull) shift; echo "$@" > "{args}"; echo '{{"path":"{model}"}}' ;;
197esac
198"#,
199 args = dir.join("pull-args").display(),
200 model = model.display()
201 );
202 std::fs::write(&bin, script).unwrap();
203 std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
204 bin
205 }
206
207 #[test]
208 fn a_model_by_id_is_pulled_by_its_runtimes_own_worker() {
209 let dir = std::env::temp_dir().join(format!("superfluid-pulls-route-{}", std::process::id()));
210 let _ = std::fs::remove_dir_all(&dir);
211 std::fs::create_dir_all(&dir).unwrap();
212 let model = dir.join("m.toy");
213 std::fs::write(&model, b"").unwrap();
214 toy_worker(&dir, &model);
215 let cat = Catalog::new(dir.join("home"), Some(dir.clone()), dir.join("superfluid-workerd"));
216 let toy = RuntimeId::new("toyrt");
217 let flags = PullFlags { offline: true, options: vec![("target".into(), Some("q8".into()))] };
218 assert_eq!(pull(&cat, toy, "org/m:q4", &flags).unwrap(), model);
219 assert_eq!(std::fs::read_to_string(dir.join("pull-args")).unwrap().trim(), "org/m:q4 --offline --target q8");
220 let bad = PullFlags { offline: false, options: vec![("force".into(), None)] };
221 assert!(pull(&cat, toy, "org/m", &bad).unwrap_err().contains("--pull-force: the toyrt runtime's pull takes no such option"));
222 let e = pull(&cat, RuntimeId::new("nowhere"), "org/m", &PullFlags { offline: true, options: Vec::new() }).unwrap_err();
223 assert!(e.contains("offline: nothing was installed"), "{e}");
224 let _ = std::fs::remove_dir_all(&dir);
225 }
226
227 fn adapter_without_its_runtime(dir: &Path, home: &Path, model: &Path) -> PathBuf {
228 use std::os::unix::fs::PermissionsExt;
229 let v = superfluid_agent::client::PROTO_VERSION;
230 let bin = dir.join("superfluid-worker-toyrt");
231 let script = format!(
232 r#"#!/bin/sh
233self=$(cd "$(dirname "$0")" && pwd)
234formats='"formats":[{{"id":"toy","describe":"a toy file","file":{{"extensions":["toy"]}}}}],"tokenizer":{{"from":"huggingface"}}'
235case "$1" in
236 check)
237 if [ -e "$self/named-build" ]; then
238 echo '{{"runtime":{{"id":"toyrt","version":null}},"link_w":[{v}],"available":"/named/build holds no toyrt","named":"TOYRT_LIB",'"$formats"'}}'
239 exit 1
240 fi
241 if [ -e "$self/../lib/payload" ] || [ -e "{home}/runtimes/toyrt/current/lib/payload" ]; then
242 echo '{{"runtime":{{"id":"toyrt","version":"b1"}},"link_w":[{v}],"available":true,'"$formats"',"devices":[{{"backend":"CPU","name":"x","memory":1}}],"pull":{{"options":[]}}}}'
243 else
244 echo '{{"runtime":{{"id":"toyrt","version":null}},"link_w":[{v}],"available":"toyrt not found: install it with `superfluid runtime install toyrt`",'"$formats"'}}'
245 exit 1
246 fi ;;
247 plan) echo '{{"describe":"a test machine","plans":[{{"id":"toyrt","version":"b1","backend":"cpu","install":"b1-cpu","assets":[],"tested":true,"why":"a test","extra":null}}]}}' ;;
248 install)
249 dir="$3"
250 mkdir -p "$dir/bin" "$dir/lib"
251 echo payload > "$dir/lib/payload"
252 cp "$0" "$dir/bin/superfluid-worker-toyrt"
253 echo '{{"id":"toyrt","install":"b1-cpu","version":"b1","backend":"cpu","link_w":[{v}],"platforms":[],"worker":"bin/superfluid-worker-toyrt","sources":[],"tested":true}}' > "$dir/runtime.json"
254 cat "$dir/runtime.json" ;;
255 pull) echo '{{"path":"{model}"}}' ;;
256esac
257"#,
258 home = home.display(),
259 model = model.display()
260 );
261 std::fs::write(&bin, script).unwrap();
262 std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
263 bin
264 }
265
266 #[test]
267 fn a_runtime_whose_adapter_is_here_is_installed_on_first_use() {
268 let dir = std::env::temp_dir().join(format!("superfluid-pulls-first-use-{}", std::process::id()));
269 let _ = std::fs::remove_dir_all(&dir);
270 let beside = dir.join("bin");
271 let home = dir.join("home");
272 std::fs::create_dir_all(&beside).unwrap();
273 let model = dir.join("m.toy");
274 std::fs::write(&model, b"").unwrap();
275 adapter_without_its_runtime(&beside, &home, &model);
276 let catalog = || Catalog::new(home.clone(), Some(beside.clone()), beside.join("superfluid-workerd"));
277 let toy = RuntimeId::new("toyrt");
278 let installed = |cat: &Catalog| crate::installs::list(cat, toy).into_iter().map(|i| i.name).collect::<Vec<_>>();
279 let online = PullFlags { offline: false, options: Vec::new() };
280
281 let cat = catalog();
282 let e = pull(&cat, toy, "org/m", &PullFlags { offline: true, options: Vec::new() }).unwrap_err();
283 assert!(e.contains("toyrt not found") && e.ends_with("(offline: nothing was installed)"), "{e}");
284 assert!(installed(&cat).is_empty());
285 let e = pull(&cat, RuntimeId::new("toyrt@b9-cpu"), "org/m", &online).unwrap_err();
286 assert!(e.contains("b9-cpu"), "{e}");
287 assert!(installed(&cat).is_empty());
288
289 assert_eq!(pull(&cat, toy, "org/m", &online).unwrap(), model);
290 assert_eq!(installed(&cat), ["b1-cpu"]);
291 assert!(cat.require(toy).is_ok());
292
293 std::fs::remove_file(home.join("runtimes/toyrt/b1-cpu/lib/payload")).unwrap();
294 let cat = catalog();
295 let e = pull(&cat, toy, "org/m", &online).unwrap_err();
296 assert!(e.contains("toyrt not found") && !e.contains("offline"), "{e}");
297 assert_eq!(installed(&cat), ["b1-cpu"]);
298 let _ = std::fs::remove_dir_all(&dir);
299 }
300
301 #[test]
302 fn a_runtime_the_operator_named_a_build_of_is_not_installed_on_first_use() {
303 let dir = std::env::temp_dir().join(format!("superfluid-pulls-named-{}", std::process::id()));
304 let _ = std::fs::remove_dir_all(&dir);
305 let (beside, home) = (dir.join("bin"), dir.join("home"));
306 std::fs::create_dir_all(&beside).unwrap();
307 let model = dir.join("m.toy");
308 std::fs::write(&model, b"").unwrap();
309 adapter_without_its_runtime(&beside, &home, &model);
310 std::fs::write(beside.join("named-build"), b"").unwrap();
311 let catalog = || Catalog::new(home.clone(), Some(beside.clone()), beside.join("superfluid-workerd"));
312 let toy = RuntimeId::new("toyrt");
313 let online = PullFlags { offline: false, options: Vec::new() };
314
315 let cat = catalog();
316 let e = pull(&cat, toy, "org/m", &online).unwrap_err();
317 assert!(e.contains("cannot run here: /named/build holds no toyrt"), "{e}");
318 assert!(e.ends_with("(TOYRT_LIB names the runtime to load, so none was installed)"), "{e}");
319 assert!(crate::installs::list(&cat, toy).is_empty(), "nothing was installed");
320
321 std::fs::remove_file(beside.join("named-build")).unwrap();
322 let cat = catalog();
323 assert_eq!(pull(&cat, toy, "org/m", &online).unwrap(), model);
324 assert_eq!(crate::installs::list(&cat, toy).len(), 1);
325 let _ = std::fs::remove_dir_all(&dir);
326 }
327
328 #[test]
329 fn a_release_installs_a_runtime_it_ships_the_adapter_for_on_first_use() {
330 let dir = std::env::temp_dir().join(format!("superfluid-pulls-shipped-{}", std::process::id()));
331 let _ = std::fs::remove_dir_all(&dir);
332 let (beside, libexec, home) = (dir.join("bin"), dir.join("libexec").join("superfluid"), dir.join("home"));
333 std::fs::create_dir_all(&beside).unwrap();
334 std::fs::create_dir_all(&libexec).unwrap();
335 let model = dir.join("m.toy");
336 std::fs::write(&model, b"").unwrap();
337 adapter_without_its_runtime(&libexec, &home, &model);
338 let cat = Catalog::new(home.clone(), Some(beside.clone()), beside.join("superfluid-workerd"));
339 let toy = RuntimeId::new("toyrt");
340 assert_eq!(cat.locate(toy).unwrap().source, Source::Shipped);
341
342 let e = pull(&cat, toy, "org/m", &PullFlags { offline: true, options: Vec::new() }).unwrap_err();
343 assert!(e.starts_with("the toyrt runtime is not installed: toyrt not found"), "{e}");
344 assert!(e.ends_with("(offline: nothing was installed)"), "{e}");
345 assert!(crate::installs::list(&cat, toy).is_empty());
346
347 assert_eq!(pull(&cat, toy, "org/m", &PullFlags { offline: false, options: Vec::new() }).unwrap(), model);
348 assert_eq!(crate::installs::list(&cat, toy).into_iter().map(|i| i.name).collect::<Vec<_>>(), ["b1-cpu"]);
349 assert_eq!(cat.require(toy).unwrap().worker.source, Source::Installed);
350 let _ = std::fs::remove_dir_all(&dir);
351 }
352
353 #[test]
354 fn a_second_pull_of_one_model_waits_for_the_first() {
355 let dir = std::env::temp_dir().join(format!("superfluid-pull-lock-{}", std::process::id()));
356 let first = Lock::take(&dir, "org/m:q4", "pull of org/m:q4").unwrap();
357 let (tx, rx) = std::sync::mpsc::channel();
358 let d = dir.clone();
359 let waiter = std::thread::spawn(move || {
360 let _second = Lock::take(&d, "org/m:q4", "pull of org/m:q4").unwrap();
361 tx.send(()).unwrap();
362 });
363 assert!(rx.recv_timeout(std::time::Duration::from_millis(200)).is_err(), "the second waits");
364 drop(first);
365 assert!(rx.recv_timeout(std::time::Duration::from_secs(5)).is_ok(), "and goes once the first is done");
366 waiter.join().unwrap();
367 let _ = std::fs::remove_dir_all(&dir);
368 }
369}