1use crate::engine::download;
24use anyhow::{anyhow, bail, Context, Result};
25use std::path::{Path, PathBuf};
26use tracing::{debug, info, warn};
27
28const TRACE_TARGET: &str = "studio_worker::engine::sd_provision";
31
32const DEFAULT_RELEASE_TAG: &str = "master-920-2f88688";
42
43pub const RELEASE_MARKER: &str = ".sd-cli-release";
47
48fn needs_provision(binary_present: bool, marker: Option<&str>, wanted_url: &str) -> bool {
51 !binary_present || marker.map(str::trim) != Some(wanted_url)
52}
53
54const MIN_SHA_LEN: usize = 7;
56
57pub fn reported_commit(version_output: &str) -> Option<&str> {
60 let (_, rest) = version_output.rsplit_once("commit ")?;
61 let sha = rest.split_whitespace().next()?;
62 (!sha.is_empty() && sha.chars().all(|c| c.is_ascii_hexdigit())).then_some(sha)
63}
64
65pub fn matches_pin(reported: Option<&str>, pinned: Option<&str>) -> bool {
70 let Some(pinned) = pinned else { return true };
71 let Some(reported) = reported else {
72 return false;
73 };
74 let shared = reported.len().min(pinned.len());
75 shared >= MIN_SHA_LEN && reported[..shared].eq_ignore_ascii_case(&pinned[..shared])
76}
77
78pub fn pinned_commit_for(tag: &str, url_override: Option<&str>) -> Option<String> {
81 if url_override.is_some_and(|url| !url.trim().is_empty()) {
82 return None;
83 }
84 sha_from_tag(tag).ok().map(str::to_string)
85}
86
87#[cfg_attr(coverage_nightly, coverage(off))]
89pub fn pinned_commit() -> Option<String> {
90 pinned_commit_for(&release_tag(), std::env::var(URL_ENV).ok().as_deref())
91}
92
93#[cfg_attr(coverage_nightly, coverage(off))]
96pub fn probe_commit(sd_cli: &Path) -> Option<String> {
97 let mut command = std::process::Command::new(sd_cli);
98 command.arg("--version");
99 if let Some((var, dir)) = library_path_env(sd_cli) {
100 command.env(var, dir);
101 }
102 let output = command.output().ok()?;
103 let text = format!(
104 "{}{}",
105 String::from_utf8_lossy(&output.stdout),
106 String::from_utf8_lossy(&output.stderr)
107 );
108 reported_commit(&text).map(str::to_string)
109}
110
111const RELEASE_ENV: &str = "STUDIO_WORKER_SDCPP_RELEASE";
113const URL_ENV: &str = "STUDIO_WORKER_SDCPP_URL";
115
116pub fn binary_name() -> &'static str {
118 if cfg!(target_os = "windows") {
119 "sd-cli.exe"
120 } else {
121 "sd-cli"
122 }
123}
124
125fn library_name() -> &'static str {
127 if cfg!(target_os = "windows") {
128 "stable-diffusion.dll"
129 } else if cfg!(target_os = "macos") {
130 "libstable-diffusion.dylib"
131 } else {
132 "libstable-diffusion.so"
133 }
134}
135
136fn vulkan_loader_name() -> Option<&'static str> {
140 if cfg!(target_os = "windows") {
141 Some("vulkan-1.dll")
142 } else if cfg!(target_os = "macos") {
143 None
144 } else {
145 Some("libvulkan.so.1")
146 }
147}
148
149fn vulkan_remedy() -> &'static str {
153 if cfg!(target_os = "windows") {
154 "install/update your GPU driver (NVIDIA, AMD, or Intel) — it ships \
155 the Vulkan runtime (vulkan-1.dll)"
156 } else {
157 "install the Vulkan loader + a GPU driver, e.g. on Debian/Ubuntu \
158 `sudo apt install libvulkan1 mesa-vulkan-drivers` (plus the \
159 vendor driver for NVIDIA/AMD); verify with `vulkaninfo --summary`"
160 }
161}
162
163#[cfg_attr(coverage_nightly, coverage(off))]
169fn vulkan_loader_loads() -> bool {
170 match vulkan_loader_name() {
171 None => true,
172 Some(name) => unsafe { libloading::Library::new(name).is_ok() },
173 }
174}
175
176fn vulkan_runtime_status_with(loader_loads: bool) -> Result<()> {
182 let Some(loader) = vulkan_loader_name() else {
183 return Ok(()); };
185 if loader_loads {
186 return Ok(());
187 }
188 bail!(
189 "Vulkan runtime not available: the loader `{loader}` could not be \
190 loaded, so stable-diffusion.cpp cannot run on the GPU. We cannot \
191 auto-provision it — {}.",
192 vulkan_remedy()
193 )
194}
195
196#[cfg_attr(coverage_nightly, coverage(off))]
200pub fn vulkan_runtime_status() -> Result<()> {
201 vulkan_runtime_status_with(vulkan_loader_loads())
202}
203
204fn select_release_tag(override_tag: Option<String>) -> String {
211 match override_tag {
212 Some(tag) => {
213 info!(
214 target: TRACE_TARGET,
215 op = "resolve-url",
216 tag = %tag,
217 source = RELEASE_ENV,
218 "using sd-cli release-tag override"
219 );
220 tag
221 }
222 None => {
223 debug!(
224 target: TRACE_TARGET,
225 op = "resolve-url",
226 tag = DEFAULT_RELEASE_TAG,
227 "using pinned sd-cli release tag"
228 );
229 DEFAULT_RELEASE_TAG.to_string()
230 }
231 }
232}
233
234#[cfg_attr(coverage_nightly, coverage(off))]
239fn release_tag() -> String {
240 select_release_tag(std::env::var(RELEASE_ENV).ok())
241}
242
243fn sha_from_tag(tag: &str) -> Result<&str> {
246 match tag.rsplit_once('-') {
247 Some((_, sha)) if !sha.is_empty() => Ok(sha),
248 _ => Err(anyhow!("release tag {tag:?} has no '-<sha>' segment")),
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254enum AssetSource {
255 Upstream,
257 SelfHosted,
261}
262
263fn asset_plan(os: &str, arch: &str) -> Result<(AssetSource, &'static str)> {
268 use AssetSource::*;
269 match (os, arch) {
270 ("windows", "x86_64") => Ok((Upstream, "win-vulkan-x64")),
271 ("linux", "x86_64") => Ok((Upstream, "Linux-Ubuntu-24.04-x86_64-vulkan")),
272 ("macos", "aarch64") | ("macos", "x86_64") => Ok((Upstream, "Darwin-macOS-26.6.2-arm64")),
275 ("linux", "aarch64") => Ok((SelfHosted, "Linux-aarch64-vulkan")),
277 _ => bail!(
278 "no prebuilt stable-diffusion.cpp binary for {os}/{arch}; \
279 install sd-cli manually — see docs/operations/sd-cli-install.md"
280 ),
281 }
282}
283
284fn asset_name(sha: &str, suffix: &str) -> String {
287 format!("sd-master-{sha}-bin-{suffix}.zip")
288}
289
290fn self_hosted_tag(upstream_tag: &str) -> String {
292 format!("sdcpp-prebuilt-{upstream_tag}")
293}
294
295fn download_url(tag: &str, os: &str, arch: &str) -> Result<String> {
298 let sha = sha_from_tag(tag)?;
299 let (source, suffix) = asset_plan(os, arch)?;
300 let asset = asset_name(sha, suffix);
301 Ok(match source {
302 AssetSource::Upstream => format!(
303 "https://github.com/leejet/stable-diffusion.cpp/releases/download/{tag}/{asset}"
304 ),
305 AssetSource::SelfHosted => format!(
306 "https://github.com/webbertakken/studio-worker/releases/download/{}/{asset}",
307 self_hosted_tag(tag)
308 ),
309 })
310}
311
312fn select_url(
318 override_url: Option<String>,
319 default_url: impl FnOnce() -> Result<String>,
320) -> Result<String> {
321 if let Some(url) = override_url {
322 if !url.is_empty() {
323 info!(
324 target: TRACE_TARGET,
325 op = "resolve-url",
326 url = %url,
327 source = URL_ENV,
328 "using sd-cli zip-URL override"
329 );
330 return Ok(url);
331 }
332 warn!(
338 target: TRACE_TARGET,
339 op = "resolve-url",
340 source = URL_ENV,
341 "ignoring empty STUDIO_WORKER_SDCPP_URL override; using the default release URL"
342 );
343 }
344 default_url()
345}
346
347#[cfg_attr(coverage_nightly, coverage(off))]
352fn resolve_url() -> Result<String> {
353 select_url(std::env::var(URL_ENV).ok(), || {
354 download_url(&release_tag(), std::env::consts::OS, std::env::consts::ARCH)
355 })
356}
357
358pub fn library_path_env(sd_cli: &Path) -> Option<(&'static str, PathBuf)> {
364 if cfg!(target_os = "windows") {
365 return None;
366 }
367 let dir = sd_cli.parent()?;
368 if dir.join(library_name()).is_file() {
369 let var = if cfg!(target_os = "macos") {
370 "DYLD_LIBRARY_PATH"
371 } else {
372 "LD_LIBRARY_PATH"
373 };
374 Some((var, dir.to_path_buf()))
375 } else {
376 None
377 }
378}
379
380#[cfg_attr(coverage_nightly, coverage(off))]
386fn extract_zip(zip_path: &Path, dest_dir: &Path) -> Result<usize> {
387 let file =
388 std::fs::File::open(zip_path).with_context(|| format!("opening {}", zip_path.display()))?;
389 let mut archive = zip::ZipArchive::new(file)
390 .with_context(|| format!("reading zip {}", zip_path.display()))?;
391 std::fs::create_dir_all(dest_dir)
392 .with_context(|| format!("creating {}", dest_dir.display()))?;
393 let mut written = 0usize;
394 for i in 0..archive.len() {
395 let mut entry = archive.by_index(i)?;
396 if entry.is_dir() {
397 continue;
398 }
399 let Some(file_name) = Path::new(entry.name()).file_name().map(|n| n.to_owned()) else {
400 warn!(
401 target: TRACE_TARGET,
402 op = "extract",
403 name = entry.name(),
404 "skipping zip entry with no file name"
405 );
406 continue;
407 };
408 let out = dest_dir.join(&file_name);
409 let mode = entry.unix_mode();
410 let mut writer =
411 std::fs::File::create(&out).with_context(|| format!("creating {}", out.display()))?;
412 std::io::copy(&mut entry, &mut writer)
413 .with_context(|| format!("writing {}", out.display()))?;
414 drop(writer);
415 apply_unix_mode(&out, mode)?;
416 written += 1;
417 }
418 Ok(written)
419}
420
421#[cfg(unix)]
423fn apply_unix_mode(path: &Path, mode: Option<u32>) -> Result<()> {
424 use std::os::unix::fs::PermissionsExt;
425 if let Some(mode) = mode {
426 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
427 .with_context(|| format!("chmod {}", path.display()))?;
428 }
429 Ok(())
430}
431
432#[cfg(not(unix))]
433fn apply_unix_mode(_path: &Path, _mode: Option<u32>) -> Result<()> {
434 Ok(())
435}
436
437#[cfg(unix)]
439fn make_executable(path: &Path) -> Result<()> {
440 use std::os::unix::fs::PermissionsExt;
441 let mut perms = std::fs::metadata(path)
442 .with_context(|| format!("stat {}", path.display()))?
443 .permissions();
444 perms.set_mode(perms.mode() | 0o755);
445 std::fs::set_permissions(path, perms).with_context(|| format!("chmod +x {}", path.display()))
446}
447
448#[cfg(not(unix))]
449fn make_executable(_path: &Path) -> Result<()> {
450 Ok(())
451}
452
453fn install_dir(staging: &Path, target: &Path) -> Result<usize> {
458 std::fs::create_dir_all(target).with_context(|| format!("creating {}", target.display()))?;
459 let mut moved = 0usize;
460 for entry in
461 std::fs::read_dir(staging).with_context(|| format!("reading {}", staging.display()))?
462 {
463 let entry = entry?;
464 if !entry.file_type()?.is_file() {
465 continue;
466 }
467 let from = entry.path();
468 let to = target.join(entry.file_name());
469 if to.exists() {
470 std::fs::remove_file(&to).with_context(|| format!("replacing {}", to.display()))?;
471 }
472 if std::fs::rename(&from, &to).is_err() {
473 std::fs::copy(&from, &to)
474 .with_context(|| format!("copying {} -> {}", from.display(), to.display()))?;
475 }
476 moved += 1;
477 }
478 Ok(moved)
479}
480
481fn clean_scratch(zip_path: &Path, staging: &Path) {
488 if let Err(e) = std::fs::remove_file(zip_path) {
489 if e.kind() != std::io::ErrorKind::NotFound {
490 warn!(
491 target: TRACE_TARGET,
492 op = "cleanup",
493 path = %zip_path.display(),
494 error = %e,
495 "could not remove sd-cli scratch zip; it may fill the disk"
496 );
497 }
498 }
499 if let Err(e) = std::fs::remove_dir_all(staging) {
500 if e.kind() != std::io::ErrorKind::NotFound {
501 warn!(
502 target: TRACE_TARGET,
503 op = "cleanup",
504 path = %staging.display(),
505 error = %e,
506 "could not remove sd-cli staging dir; it may fill the disk"
507 );
508 }
509 }
510}
511
512#[cfg_attr(coverage_nightly, coverage(off))]
522pub fn provision(models_root: &Path) -> Result<PathBuf> {
523 let target_dir = models_root.join("bin");
524 let binary = target_dir.join(binary_name());
525 let marker_path = target_dir.join(RELEASE_MARKER);
526 let url = resolve_url()?;
527 let marker = std::fs::read_to_string(&marker_path).ok();
528 if !needs_provision(binary.is_file(), marker.as_deref(), &url) {
529 return Ok(binary);
530 }
531 info!(
532 target: TRACE_TARGET,
533 op = "provision",
534 url = %url,
535 dest = %target_dir.display(),
536 previous = marker.as_deref().map(str::trim).unwrap_or(if binary.is_file() { "unmarked" } else { "none" }),
537 "provisioning stable-diffusion.cpp"
538 );
539
540 std::fs::create_dir_all(models_root)
541 .with_context(|| format!("creating {}", models_root.display()))?;
542 let stamp = format!("{}-{}", std::process::id(), now_nanos());
543 let zip_path = models_root.join(format!(".sd-cli-{stamp}.zip"));
544 let staging = models_root.join(format!(".sd-cli-staging-{stamp}"));
545
546 let result = (|| -> Result<PathBuf> {
547 download::download_file(&url, &zip_path)
548 .with_context(|| format!("downloading sd-cli zip from {url}"))?;
549 let count = extract_zip(&zip_path, &staging)?;
550 let staged_binary = staging.join(binary_name());
551 if !staged_binary.is_file() {
552 bail!(
553 "downloaded sd-cli zip from {url} did not contain {} (extracted {count} files)",
554 binary_name()
555 );
556 }
557 install_dir(&staging, &target_dir)?;
558 make_executable(&binary)?;
559 if !binary.is_file() {
560 bail!("sd-cli install left no binary at {}", binary.display());
561 }
562 std::fs::write(&marker_path, format!("{url}\n"))
563 .with_context(|| format!("writing {}", marker_path.display()))?;
564 Ok(binary.clone())
565 })();
566
567 clean_scratch(&zip_path, &staging);
572
573 match &result {
574 Ok(path) => info!(
575 target: TRACE_TARGET,
576 op = "provision",
577 path = %path.display(),
578 "sd-cli provisioned"
579 ),
580 Err(e) => warn!(
581 target: TRACE_TARGET,
582 op = "provision",
583 error = %e,
584 "sd-cli provisioning failed"
585 ),
586 }
587 result
588}
589
590#[cfg_attr(coverage_nightly, coverage(off))]
591fn now_nanos() -> i64 {
592 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn reported_commit_reads_the_commit_from_version_output() {
601 assert_eq!(
602 reported_commit("stable-diffusion.cpp version master-920-2f88688, commit 2f88688\n"),
603 Some("2f88688")
604 );
605 assert_eq!(
606 reported_commit("stable-diffusion.cpp version unknown, commit 29ab511"),
607 Some("29ab511")
608 );
609 assert_eq!(reported_commit("usage: sd-cli [options]"), None);
610 assert_eq!(reported_commit("commit "), None);
611 }
612
613 #[test]
614 fn matches_pin_compares_short_shas_and_trusts_an_unknown_pin() {
615 assert!(matches_pin(Some("2f88688"), Some("2f88688")));
616 assert!(matches_pin(Some("2f886881a2b3"), Some("2f88688")));
617 assert!(matches_pin(Some("2f88688"), Some("2f886881a2b3")));
618 assert!(!matches_pin(Some("29ab511"), Some("2f88688")));
619 assert!(!matches_pin(None, Some("2f88688")));
621 assert!(matches_pin(Some("29ab511"), None));
623 assert!(matches_pin(None, None));
624 assert!(!matches_pin(Some("2f"), Some("2f88688")));
626 }
627
628 #[test]
629 fn pinned_commit_follows_the_release_tag() {
630 assert_eq!(
631 pinned_commit_for("master-920-2f88688", None),
632 Some("2f88688".to_string())
633 );
634 assert_eq!(
635 pinned_commit_for("master-920-2f88688", Some("https://mirror/sd.zip")),
636 None
637 );
638 assert_eq!(pinned_commit_for("master", None), None);
639 }
640
641 #[test]
642 fn needs_provision_when_missing_unmarked_or_from_another_release() {
643 assert!(needs_provision(false, None, "u"));
644 assert!(needs_provision(false, Some("u"), "u"));
645 assert!(needs_provision(true, None, "u"));
646 assert!(needs_provision(true, Some("old"), "u"));
647 assert!(!needs_provision(true, Some("u\n"), "u"));
648 }
649 use std::io::Write;
650 use tempfile::tempdir;
651
652 #[test]
653 fn sha_from_tag_takes_trailing_segment() {
654 assert_eq!(sha_from_tag("master-669-2d40a8b").unwrap(), "2d40a8b");
655 assert_eq!(sha_from_tag("master-1-abc").unwrap(), "abc");
656 }
657
658 #[test]
659 fn sha_from_tag_rejects_a_tag_without_a_sha() {
660 assert!(sha_from_tag("master").is_err());
661 assert!(sha_from_tag("trailing-").is_err());
662 }
663
664 #[test]
665 fn asset_plan_picks_vulkan_or_universal_for_supported_targets() {
666 use AssetSource::*;
667 assert_eq!(
668 asset_plan("windows", "x86_64").unwrap(),
669 (Upstream, "win-vulkan-x64")
670 );
671 assert_eq!(
672 asset_plan("linux", "x86_64").unwrap(),
673 (Upstream, "Linux-Ubuntu-24.04-x86_64-vulkan")
674 );
675 assert_eq!(
676 asset_plan("macos", "aarch64").unwrap(),
677 (Upstream, "Darwin-macOS-26.6.2-arm64")
678 );
679 }
680
681 #[test]
682 fn asset_plan_makes_intel_mac_and_arm_linux_first_class() {
683 use AssetSource::*;
684 assert_eq!(
686 asset_plan("macos", "x86_64").unwrap(),
687 (Upstream, "Darwin-macOS-26.6.2-arm64")
688 );
689 assert_eq!(
691 asset_plan("linux", "aarch64").unwrap(),
692 (SelfHosted, "Linux-aarch64-vulkan")
693 );
694 }
695
696 #[test]
697 fn asset_plan_rejects_unsupported_targets_with_guidance() {
698 let err = asset_plan("freebsd", "x86_64").unwrap_err().to_string();
699 assert!(err.contains("no prebuilt"), "got: {err}");
700 assert!(
701 err.contains("sd-cli-install.md"),
702 "points to the doc: {err}"
703 );
704 assert!(asset_plan("windows", "aarch64").is_err());
705 }
706
707 #[test]
708 fn asset_name_embeds_sha_and_platform() {
709 assert_eq!(
710 asset_name("2d40a8b", "win-vulkan-x64"),
711 "sd-master-2d40a8b-bin-win-vulkan-x64.zip"
712 );
713 assert_eq!(
714 asset_name("2d40a8b", "Linux-aarch64-vulkan"),
715 "sd-master-2d40a8b-bin-Linux-aarch64-vulkan.zip"
716 );
717 }
718
719 #[test]
720 fn download_url_targets_upstream_for_covered_platforms() {
721 let url = download_url("master-669-2d40a8b", "windows", "x86_64").unwrap();
722 let expected = concat!(
723 "https://github.com/leejet/stable-diffusion.cpp/releases/download/",
724 "master-669-2d40a8b/sd-master-2d40a8b-bin-win-vulkan-x64.zip"
725 );
726 assert_eq!(url, expected);
727 }
728
729 #[test]
730 fn download_url_targets_our_release_for_arm_linux() {
731 let url = download_url("master-669-2d40a8b", "linux", "aarch64").unwrap();
732 let expected = concat!(
733 "https://github.com/webbertakken/studio-worker/releases/download/",
734 "sdcpp-prebuilt-master-669-2d40a8b/",
735 "sd-master-2d40a8b-bin-Linux-aarch64-vulkan.zip"
736 );
737 assert_eq!(url, expected);
738 }
739
740 #[test]
741 fn download_url_uses_universal_darwin_asset_for_intel_mac() {
742 let arm = download_url("master-669-2d40a8b", "macos", "aarch64").unwrap();
743 let intel = download_url("master-669-2d40a8b", "macos", "x86_64").unwrap();
744 assert_eq!(arm, intel, "Intel Macs use the same universal2 asset");
745 assert!(intel.contains("Darwin-macOS-26.6.2-arm64"), "got: {intel}");
746 }
747
748 #[test]
749 fn select_release_tag_prefers_the_override() {
750 assert_eq!(
751 select_release_tag(Some("master-700-deadbee".into())),
752 "master-700-deadbee"
753 );
754 }
755
756 #[test]
757 fn select_release_tag_falls_back_to_the_pinned_default() {
758 assert_eq!(select_release_tag(None), DEFAULT_RELEASE_TAG);
759 }
760
761 #[test]
762 fn select_release_tag_logs_the_override_source() {
763 let logs = crate::test_support::capture(|| {
764 let _ = select_release_tag(Some("master-700-deadbee".into()));
765 });
766 assert!(
767 logs.contains("STUDIO_WORKER_SDCPP_RELEASE"),
768 "override log must name the env var: {logs}"
769 );
770 assert!(logs.contains("master-700-deadbee"), "got: {logs}");
771 assert!(logs.contains("override"), "got: {logs}");
772 }
773
774 #[test]
775 fn select_url_prefers_a_non_empty_override() {
776 let url = select_url(Some("https://mirror.example/sd.zip".into()), || {
777 panic!("default must not be consulted when an override is present")
778 })
779 .unwrap();
780 assert_eq!(url, "https://mirror.example/sd.zip");
781 }
782
783 #[test]
784 fn select_url_ignores_an_empty_override_and_falls_back() {
785 let url = select_url(Some(String::new()), || Ok("fallback".into())).unwrap();
786 assert_eq!(url, "fallback");
787 }
788
789 #[test]
790 fn select_url_falls_back_when_no_override_is_set() {
791 let url = select_url(None, || Ok("fallback".into())).unwrap();
792 assert_eq!(url, "fallback");
793 }
794
795 #[test]
796 fn select_url_propagates_a_default_resolution_error() {
797 let err = select_url(None, || bail!("no prebuilt for this platform"))
798 .unwrap_err()
799 .to_string();
800 assert!(err.contains("no prebuilt"), "got: {err}");
801 }
802
803 #[test]
804 fn select_url_logs_the_override_source() {
805 let logs = crate::test_support::capture(|| {
806 let _ = select_url(Some("https://mirror.example/sd.zip".into()), || {
807 Ok("unused".into())
808 });
809 });
810 assert!(
811 logs.contains("STUDIO_WORKER_SDCPP_URL"),
812 "override log must name the env var: {logs}"
813 );
814 assert!(
815 logs.contains("https://mirror.example/sd.zip"),
816 "got: {logs}"
817 );
818 }
819
820 #[test]
821 fn select_url_warns_when_the_override_is_present_but_empty() {
822 let logs = crate::test_support::capture(|| {
830 let url = select_url(Some(String::new()), || Ok("fallback".into())).unwrap();
831 assert_eq!(url, "fallback", "an empty override must still fall back");
832 });
833 assert!(
834 logs.contains("WARN"),
835 "expected a WARN breadcrumb, got: {logs}"
836 );
837 assert!(
838 logs.contains("STUDIO_WORKER_SDCPP_URL"),
839 "the warning must name the ignored env var: {logs}"
840 );
841 assert!(
842 logs.contains("op=\"resolve-url\""),
843 "expected the resolve-url op field: {logs}"
844 );
845 }
846
847 #[test]
848 fn install_dir_moves_files_and_overwrites() {
849 let staging = tempdir().unwrap();
850 let target = tempdir().unwrap();
851 std::fs::write(staging.path().join("sd-cli"), b"new-binary").unwrap();
852 std::fs::write(staging.path().join("libstable-diffusion.so"), b"lib").unwrap();
853 std::fs::write(target.path().join("sd-cli"), b"old-binary").unwrap();
855
856 let moved = install_dir(staging.path(), target.path()).unwrap();
857 assert_eq!(moved, 2);
858 assert_eq!(
859 std::fs::read(target.path().join("sd-cli")).unwrap(),
860 b"new-binary"
861 );
862 assert_eq!(
863 std::fs::read(target.path().join("libstable-diffusion.so")).unwrap(),
864 b"lib"
865 );
866 assert!(!staging.path().join("sd-cli").exists());
868 }
869
870 #[test]
871 fn install_dir_skips_subdirectories_and_counts_only_files() {
872 let staging = tempdir().unwrap();
878 let target = tempdir().unwrap();
879 std::fs::write(staging.path().join("sd-cli"), b"binary").unwrap();
880 std::fs::write(staging.path().join("libstable-diffusion.so"), b"lib").unwrap();
881 let nested = staging.path().join("nested");
882 std::fs::create_dir(&nested).unwrap();
883 std::fs::write(nested.join("buried"), b"should-not-publish").unwrap();
884
885 let moved = install_dir(staging.path(), target.path()).unwrap();
886
887 assert_eq!(moved, 2);
889 assert!(target.path().join("sd-cli").is_file());
890 assert!(target.path().join("libstable-diffusion.so").is_file());
891 assert!(
893 !target.path().join("nested").exists(),
894 "a staging subdirectory must not be published"
895 );
896 assert!(
897 !target.path().join("buried").exists(),
898 "a staging subdirectory's contents must not be flattened into the target"
899 );
900 }
901
902 #[test]
903 fn clean_scratch_removes_zip_and_staging_quietly() {
904 let dir = tempdir().unwrap();
905 let zip = dir.path().join("scratch.zip");
906 let staging = dir.path().join("staging");
907 std::fs::write(&zip, b"zip").unwrap();
908 std::fs::create_dir_all(&staging).unwrap();
909 std::fs::write(staging.join("sd-cli"), b"bin").unwrap();
910
911 let (zip_c, staging_c) = (zip.clone(), staging.clone());
912 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
913
914 assert!(!zip.exists(), "scratch zip must be removed");
915 assert!(!staging.exists(), "staging dir must be removed");
916 assert!(
917 !logs.contains("could not remove"),
918 "a clean removal must not warn: {logs}"
919 );
920 }
921
922 #[test]
923 fn clean_scratch_is_silent_when_paths_are_already_gone() {
924 let dir = tempdir().unwrap();
925 let zip = dir.path().join("missing.zip");
926 let staging = dir.path().join("missing-staging");
927
928 let (zip_c, staging_c) = (zip.clone(), staging.clone());
929 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
930
931 assert!(
933 !logs.contains("could not remove"),
934 "an already-clean slot must not warn: {logs}"
935 );
936 }
937
938 #[test]
939 fn clean_scratch_warns_when_removal_fails() {
940 let dir = tempdir().unwrap();
941 let zip = dir.path().join("zip-slot");
947 std::fs::create_dir_all(&zip).unwrap();
948 let staging = dir.path().join("staging-slot");
949 std::fs::write(&staging, b"not a dir").unwrap();
950
951 let (zip_c, staging_c) = (zip.clone(), staging.clone());
952 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
953
954 assert!(
955 logs.matches("could not remove").count() >= 2,
956 "both failed removals must warn: {logs}"
957 );
958 assert!(
959 logs.contains("fill the disk"),
960 "the warning must flag the disk-fill risk: {logs}"
961 );
962 }
963
964 #[test]
965 fn extract_zip_flattens_and_defuses_zip_slip() {
966 let dir = tempdir().unwrap();
967 let zip_path = dir.path().join("test.zip");
968 {
971 let file = std::fs::File::create(&zip_path).unwrap();
972 let mut zw = zip::ZipWriter::new(file);
973 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
974 .compression_method(zip::CompressionMethod::Deflated);
975 zw.start_file("sd-cli", opts).unwrap();
976 zw.write_all(b"binary").unwrap();
977 zw.start_file("nested/libstable-diffusion.so", opts)
978 .unwrap();
979 zw.write_all(b"lib").unwrap();
980 zw.start_file("../../escape.txt", opts).unwrap();
981 zw.write_all(b"evil").unwrap();
982 zw.finish().unwrap();
983 }
984 let dest = dir.path().join("out");
985 let count = extract_zip(&zip_path, &dest).unwrap();
986 assert_eq!(count, 3);
987 assert_eq!(std::fs::read(dest.join("sd-cli")).unwrap(), b"binary");
988 assert_eq!(
989 std::fs::read(dest.join("libstable-diffusion.so")).unwrap(),
990 b"lib"
991 );
992 assert!(dest.join("escape.txt").is_file());
995 assert!(!dir.path().join("escape.txt").exists());
996 }
997
998 #[test]
999 fn extract_zip_skips_directory_entries() {
1000 let dir = tempdir().unwrap();
1009 let zip_path = dir.path().join("with-dirs.zip");
1010 {
1011 let file = std::fs::File::create(&zip_path).unwrap();
1012 let mut zw = zip::ZipWriter::new(file);
1013 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
1014 .compression_method(zip::CompressionMethod::Deflated);
1015 zw.add_directory("build/", opts).unwrap();
1016 zw.start_file("sd-cli", opts).unwrap();
1017 zw.write_all(b"binary").unwrap();
1018 zw.add_directory("nested/empty/", opts).unwrap();
1019 zw.finish().unwrap();
1020 }
1021 let dest = dir.path().join("out");
1022 let count = extract_zip(&zip_path, &dest).unwrap();
1023 assert_eq!(
1025 count, 1,
1026 "directory entries must not count as written files"
1027 );
1028 assert_eq!(std::fs::read(dest.join("sd-cli")).unwrap(), b"binary");
1029 assert!(
1031 !dest.join("build").exists(),
1032 "a directory entry must not become a file in the flat output"
1033 );
1034 assert!(
1035 !dest.join("empty").exists(),
1036 "a nested directory entry must not become a file either"
1037 );
1038 }
1039
1040 #[cfg(unix)]
1041 #[test]
1042 fn extract_zip_preserves_exec_bit() {
1043 use std::os::unix::fs::PermissionsExt;
1044 let dir = tempdir().unwrap();
1045 let zip_path = dir.path().join("exec.zip");
1046 {
1047 let file = std::fs::File::create(&zip_path).unwrap();
1048 let mut zw = zip::ZipWriter::new(file);
1049 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
1050 .compression_method(zip::CompressionMethod::Deflated)
1051 .unix_permissions(0o755);
1052 zw.start_file("sd-cli", opts).unwrap();
1053 zw.write_all(b"#!/bin/sh\n").unwrap();
1054 zw.finish().unwrap();
1055 }
1056 let dest = dir.path().join("out");
1057 extract_zip(&zip_path, &dest).unwrap();
1058 let mode = std::fs::metadata(dest.join("sd-cli"))
1059 .unwrap()
1060 .permissions()
1061 .mode();
1062 assert!(mode & 0o111 != 0, "exec bit must survive: {mode:o}");
1063 }
1064
1065 #[cfg(unix)]
1066 #[test]
1067 fn library_path_env_points_loader_at_sibling_lib() {
1068 let dir = tempdir().unwrap();
1069 let sd_cli = dir.path().join(binary_name());
1070 std::fs::write(&sd_cli, b"bin").unwrap();
1071 assert!(library_path_env(&sd_cli).is_none());
1073 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
1075 let (var, env_dir) = library_path_env(&sd_cli).expect("sibling lib resolved");
1076 assert!(var == "LD_LIBRARY_PATH" || var == "DYLD_LIBRARY_PATH");
1077 assert_eq!(env_dir, dir.path());
1078 }
1079
1080 #[test]
1081 fn vulkan_status_ok_when_loader_loads() {
1082 assert!(vulkan_runtime_status_with(true).is_ok());
1085 }
1086
1087 #[test]
1088 fn vulkan_status_errors_with_actionable_remedy_when_missing() {
1089 let result = vulkan_runtime_status_with(false);
1090 if cfg!(target_os = "macos") {
1091 assert!(result.is_ok());
1093 } else {
1094 let err = result.unwrap_err().to_string();
1095 assert!(err.contains("Vulkan runtime"), "got: {err}");
1096 assert!(
1097 err.contains("auto-provision"),
1098 "must say we can't auto-provision it: {err}"
1099 );
1100 if cfg!(target_os = "windows") {
1102 assert!(err.contains("vulkan-1.dll"), "got: {err}");
1103 assert!(err.contains("GPU driver"), "got: {err}");
1104 } else {
1105 assert!(err.contains("libvulkan1"), "got: {err}");
1106 assert!(err.contains("vulkaninfo"), "got: {err}");
1107 }
1108 }
1109 }
1110
1111 #[cfg(target_os = "windows")]
1112 #[test]
1113 fn library_path_env_is_none_on_windows() {
1114 let dir = tempdir().unwrap();
1115 let sd_cli = dir.path().join(binary_name());
1116 std::fs::write(&sd_cli, b"bin").unwrap();
1117 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
1118 assert!(library_path_env(&sd_cli).is_none());
1119 }
1120}