Skip to main content

superfluid_daemon/
pulls.rs

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