1use crate::engine::download;
24use anyhow::{anyhow, bail, Context, Result};
25use std::path::{Path, PathBuf};
26use tracing::{info, warn};
27
28const TRACE_TARGET: &str = "studio_worker::engine::sd_provision";
31
32const DEFAULT_RELEASE_TAG: &str = "master-669-2d40a8b";
36
37const RELEASE_ENV: &str = "STUDIO_WORKER_SDCPP_RELEASE";
39const URL_ENV: &str = "STUDIO_WORKER_SDCPP_URL";
41
42pub fn binary_name() -> &'static str {
44 if cfg!(target_os = "windows") {
45 "sd-cli.exe"
46 } else {
47 "sd-cli"
48 }
49}
50
51fn library_name() -> &'static str {
53 if cfg!(target_os = "windows") {
54 "stable-diffusion.dll"
55 } else if cfg!(target_os = "macos") {
56 "libstable-diffusion.dylib"
57 } else {
58 "libstable-diffusion.so"
59 }
60}
61
62fn release_tag() -> String {
64 std::env::var(RELEASE_ENV).unwrap_or_else(|_| DEFAULT_RELEASE_TAG.to_string())
65}
66
67fn sha_from_tag(tag: &str) -> Result<&str> {
70 match tag.rsplit_once('-') {
71 Some((_, sha)) if !sha.is_empty() => Ok(sha),
72 _ => Err(anyhow!("release tag {tag:?} has no '-<sha>' segment")),
73 }
74}
75
76fn asset_suffix(os: &str, arch: &str) -> Result<&'static str> {
80 match (os, arch) {
81 ("windows", "x86_64") => Ok("win-vulkan-x64"),
82 ("linux", "x86_64") => Ok("Linux-Ubuntu-24.04-x86_64-vulkan"),
83 ("macos", "aarch64") => Ok("Darwin-macOS-15.7.7-arm64"),
84 _ => bail!(
85 "no prebuilt stable-diffusion.cpp binary for {os}/{arch}; \
86 install sd-cli manually — see docs/operations/sd-cli-install.md"
87 ),
88 }
89}
90
91fn asset_name(tag: &str, os: &str, arch: &str) -> Result<String> {
93 let sha = sha_from_tag(tag)?;
94 let suffix = asset_suffix(os, arch)?;
95 Ok(format!("sd-master-{sha}-bin-{suffix}.zip"))
96}
97
98fn download_url(tag: &str, os: &str, arch: &str) -> Result<String> {
100 let asset = asset_name(tag, os, arch)?;
101 Ok(format!(
102 "https://github.com/leejet/stable-diffusion.cpp/releases/download/{tag}/{asset}"
103 ))
104}
105
106fn resolve_url() -> Result<String> {
110 if let Ok(url) = std::env::var(URL_ENV) {
111 if !url.is_empty() {
112 return Ok(url);
113 }
114 }
115 download_url(&release_tag(), std::env::consts::OS, std::env::consts::ARCH)
116}
117
118pub fn library_path_env(sd_cli: &Path) -> Option<(&'static str, PathBuf)> {
124 if cfg!(target_os = "windows") {
125 return None;
126 }
127 let dir = sd_cli.parent()?;
128 if dir.join(library_name()).is_file() {
129 let var = if cfg!(target_os = "macos") {
130 "DYLD_LIBRARY_PATH"
131 } else {
132 "LD_LIBRARY_PATH"
133 };
134 Some((var, dir.to_path_buf()))
135 } else {
136 None
137 }
138}
139
140#[cfg_attr(coverage_nightly, coverage(off))]
146fn extract_zip(zip_path: &Path, dest_dir: &Path) -> Result<usize> {
147 let file =
148 std::fs::File::open(zip_path).with_context(|| format!("opening {}", zip_path.display()))?;
149 let mut archive = zip::ZipArchive::new(file)
150 .with_context(|| format!("reading zip {}", zip_path.display()))?;
151 std::fs::create_dir_all(dest_dir)
152 .with_context(|| format!("creating {}", dest_dir.display()))?;
153 let mut written = 0usize;
154 for i in 0..archive.len() {
155 let mut entry = archive.by_index(i)?;
156 if entry.is_dir() {
157 continue;
158 }
159 let Some(file_name) = Path::new(entry.name()).file_name().map(|n| n.to_owned()) else {
160 warn!(
161 target: TRACE_TARGET,
162 op = "extract",
163 name = entry.name(),
164 "skipping zip entry with no file name"
165 );
166 continue;
167 };
168 let out = dest_dir.join(&file_name);
169 let mode = entry.unix_mode();
170 let mut writer =
171 std::fs::File::create(&out).with_context(|| format!("creating {}", out.display()))?;
172 std::io::copy(&mut entry, &mut writer)
173 .with_context(|| format!("writing {}", out.display()))?;
174 drop(writer);
175 apply_unix_mode(&out, mode)?;
176 written += 1;
177 }
178 Ok(written)
179}
180
181#[cfg(unix)]
183fn apply_unix_mode(path: &Path, mode: Option<u32>) -> Result<()> {
184 use std::os::unix::fs::PermissionsExt;
185 if let Some(mode) = mode {
186 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
187 .with_context(|| format!("chmod {}", path.display()))?;
188 }
189 Ok(())
190}
191
192#[cfg(not(unix))]
193fn apply_unix_mode(_path: &Path, _mode: Option<u32>) -> Result<()> {
194 Ok(())
195}
196
197#[cfg(unix)]
199fn make_executable(path: &Path) -> Result<()> {
200 use std::os::unix::fs::PermissionsExt;
201 let mut perms = std::fs::metadata(path)
202 .with_context(|| format!("stat {}", path.display()))?
203 .permissions();
204 perms.set_mode(perms.mode() | 0o755);
205 std::fs::set_permissions(path, perms).with_context(|| format!("chmod +x {}", path.display()))
206}
207
208#[cfg(not(unix))]
209fn make_executable(_path: &Path) -> Result<()> {
210 Ok(())
211}
212
213fn install_dir(staging: &Path, target: &Path) -> Result<usize> {
218 std::fs::create_dir_all(target).with_context(|| format!("creating {}", target.display()))?;
219 let mut moved = 0usize;
220 for entry in
221 std::fs::read_dir(staging).with_context(|| format!("reading {}", staging.display()))?
222 {
223 let entry = entry?;
224 if !entry.file_type()?.is_file() {
225 continue;
226 }
227 let from = entry.path();
228 let to = target.join(entry.file_name());
229 if to.exists() {
230 std::fs::remove_file(&to).with_context(|| format!("replacing {}", to.display()))?;
231 }
232 if std::fs::rename(&from, &to).is_err() {
233 std::fs::copy(&from, &to)
234 .with_context(|| format!("copying {} -> {}", from.display(), to.display()))?;
235 }
236 moved += 1;
237 }
238 Ok(moved)
239}
240
241#[cfg_attr(coverage_nightly, coverage(off))]
251pub fn provision(models_root: &Path) -> Result<PathBuf> {
252 let target_dir = models_root.join("bin");
253 let binary = target_dir.join(binary_name());
254 if binary.is_file() {
255 return Ok(binary);
256 }
257
258 let url = resolve_url()?;
259 info!(
260 target: TRACE_TARGET,
261 op = "provision",
262 url = %url,
263 dest = %target_dir.display(),
264 "sd-cli not found; provisioning stable-diffusion.cpp"
265 );
266
267 std::fs::create_dir_all(models_root)
268 .with_context(|| format!("creating {}", models_root.display()))?;
269 let stamp = format!("{}-{}", std::process::id(), now_nanos());
270 let zip_path = models_root.join(format!(".sd-cli-{stamp}.zip"));
271 let staging = models_root.join(format!(".sd-cli-staging-{stamp}"));
272
273 let result = (|| -> Result<PathBuf> {
274 download::download_file(&url, &zip_path)
275 .with_context(|| format!("downloading sd-cli zip from {url}"))?;
276 let count = extract_zip(&zip_path, &staging)?;
277 let staged_binary = staging.join(binary_name());
278 if !staged_binary.is_file() {
279 bail!(
280 "downloaded sd-cli zip from {url} did not contain {} (extracted {count} files)",
281 binary_name()
282 );
283 }
284 install_dir(&staging, &target_dir)?;
285 make_executable(&binary)?;
286 if !binary.is_file() {
287 bail!("sd-cli install left no binary at {}", binary.display());
288 }
289 Ok(binary.clone())
290 })();
291
292 let _ = std::fs::remove_file(&zip_path);
296 let _ = std::fs::remove_dir_all(&staging);
297
298 match &result {
299 Ok(path) => info!(
300 target: TRACE_TARGET,
301 op = "provision",
302 path = %path.display(),
303 "sd-cli provisioned"
304 ),
305 Err(e) => warn!(
306 target: TRACE_TARGET,
307 op = "provision",
308 error = %e,
309 "sd-cli provisioning failed"
310 ),
311 }
312 result
313}
314
315#[cfg_attr(coverage_nightly, coverage(off))]
316fn now_nanos() -> i64 {
317 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use std::io::Write;
324 use tempfile::tempdir;
325
326 #[test]
327 fn sha_from_tag_takes_trailing_segment() {
328 assert_eq!(sha_from_tag("master-669-2d40a8b").unwrap(), "2d40a8b");
329 assert_eq!(sha_from_tag("master-1-abc").unwrap(), "abc");
330 }
331
332 #[test]
333 fn sha_from_tag_rejects_a_tag_without_a_sha() {
334 assert!(sha_from_tag("master").is_err());
335 assert!(sha_from_tag("trailing-").is_err());
336 }
337
338 #[test]
339 fn asset_suffix_picks_vulkan_for_supported_targets() {
340 assert_eq!(asset_suffix("windows", "x86_64").unwrap(), "win-vulkan-x64");
341 assert_eq!(
342 asset_suffix("linux", "x86_64").unwrap(),
343 "Linux-Ubuntu-24.04-x86_64-vulkan"
344 );
345 assert_eq!(
346 asset_suffix("macos", "aarch64").unwrap(),
347 "Darwin-macOS-15.7.7-arm64"
348 );
349 }
350
351 #[test]
352 fn asset_suffix_rejects_unsupported_targets_with_guidance() {
353 let err = asset_suffix("linux", "aarch64").unwrap_err().to_string();
354 assert!(err.contains("no prebuilt"), "got: {err}");
355 assert!(
356 err.contains("sd-cli-install.md"),
357 "points to the doc: {err}"
358 );
359 assert!(asset_suffix("freebsd", "x86_64").is_err());
360 assert!(asset_suffix("macos", "x86_64").is_err());
361 }
362
363 #[test]
364 fn asset_name_embeds_sha_and_platform() {
365 assert_eq!(
366 asset_name("master-669-2d40a8b", "windows", "x86_64").unwrap(),
367 "sd-master-2d40a8b-bin-win-vulkan-x64.zip"
368 );
369 assert_eq!(
370 asset_name("master-669-2d40a8b", "linux", "x86_64").unwrap(),
371 "sd-master-2d40a8b-bin-Linux-Ubuntu-24.04-x86_64-vulkan.zip"
372 );
373 }
374
375 #[test]
376 fn download_url_targets_the_upstream_release() {
377 let url = download_url("master-669-2d40a8b", "windows", "x86_64").unwrap();
378 let expected = concat!(
379 "https://github.com/leejet/stable-diffusion.cpp/releases/download/",
380 "master-669-2d40a8b/sd-master-2d40a8b-bin-win-vulkan-x64.zip"
381 );
382 assert_eq!(url, expected);
383 }
384
385 #[test]
386 fn install_dir_moves_files_and_overwrites() {
387 let staging = tempdir().unwrap();
388 let target = tempdir().unwrap();
389 std::fs::write(staging.path().join("sd-cli"), b"new-binary").unwrap();
390 std::fs::write(staging.path().join("libstable-diffusion.so"), b"lib").unwrap();
391 std::fs::write(target.path().join("sd-cli"), b"old-binary").unwrap();
393
394 let moved = install_dir(staging.path(), target.path()).unwrap();
395 assert_eq!(moved, 2);
396 assert_eq!(
397 std::fs::read(target.path().join("sd-cli")).unwrap(),
398 b"new-binary"
399 );
400 assert_eq!(
401 std::fs::read(target.path().join("libstable-diffusion.so")).unwrap(),
402 b"lib"
403 );
404 assert!(!staging.path().join("sd-cli").exists());
406 }
407
408 #[test]
409 fn extract_zip_flattens_and_defuses_zip_slip() {
410 let dir = tempdir().unwrap();
411 let zip_path = dir.path().join("test.zip");
412 {
415 let file = std::fs::File::create(&zip_path).unwrap();
416 let mut zw = zip::ZipWriter::new(file);
417 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
418 .compression_method(zip::CompressionMethod::Deflated);
419 zw.start_file("sd-cli", opts).unwrap();
420 zw.write_all(b"binary").unwrap();
421 zw.start_file("nested/libstable-diffusion.so", opts)
422 .unwrap();
423 zw.write_all(b"lib").unwrap();
424 zw.start_file("../../escape.txt", opts).unwrap();
425 zw.write_all(b"evil").unwrap();
426 zw.finish().unwrap();
427 }
428 let dest = dir.path().join("out");
429 let count = extract_zip(&zip_path, &dest).unwrap();
430 assert_eq!(count, 3);
431 assert_eq!(std::fs::read(dest.join("sd-cli")).unwrap(), b"binary");
432 assert_eq!(
433 std::fs::read(dest.join("libstable-diffusion.so")).unwrap(),
434 b"lib"
435 );
436 assert!(dest.join("escape.txt").is_file());
439 assert!(!dir.path().join("escape.txt").exists());
440 }
441
442 #[cfg(unix)]
443 #[test]
444 fn extract_zip_preserves_exec_bit() {
445 use std::os::unix::fs::PermissionsExt;
446 let dir = tempdir().unwrap();
447 let zip_path = dir.path().join("exec.zip");
448 {
449 let file = std::fs::File::create(&zip_path).unwrap();
450 let mut zw = zip::ZipWriter::new(file);
451 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
452 .compression_method(zip::CompressionMethod::Deflated)
453 .unix_permissions(0o755);
454 zw.start_file("sd-cli", opts).unwrap();
455 zw.write_all(b"#!/bin/sh\n").unwrap();
456 zw.finish().unwrap();
457 }
458 let dest = dir.path().join("out");
459 extract_zip(&zip_path, &dest).unwrap();
460 let mode = std::fs::metadata(dest.join("sd-cli"))
461 .unwrap()
462 .permissions()
463 .mode();
464 assert!(mode & 0o111 != 0, "exec bit must survive: {mode:o}");
465 }
466
467 #[cfg(unix)]
468 #[test]
469 fn library_path_env_points_loader_at_sibling_lib() {
470 let dir = tempdir().unwrap();
471 let sd_cli = dir.path().join(binary_name());
472 std::fs::write(&sd_cli, b"bin").unwrap();
473 assert!(library_path_env(&sd_cli).is_none());
475 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
477 let (var, env_dir) = library_path_env(&sd_cli).expect("sibling lib resolved");
478 assert!(var == "LD_LIBRARY_PATH" || var == "DYLD_LIBRARY_PATH");
479 assert_eq!(env_dir, dir.path());
480 }
481
482 #[cfg(target_os = "windows")]
483 #[test]
484 fn library_path_env_is_none_on_windows() {
485 let dir = tempdir().unwrap();
486 let sd_cli = dir.path().join(binary_name());
487 std::fs::write(&sd_cli, b"bin").unwrap();
488 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
489 assert!(library_path_env(&sd_cli).is_none());
490 }
491}