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-669-2d40a8b";
40
41const RELEASE_ENV: &str = "STUDIO_WORKER_SDCPP_RELEASE";
43const URL_ENV: &str = "STUDIO_WORKER_SDCPP_URL";
45
46pub fn binary_name() -> &'static str {
48 if cfg!(target_os = "windows") {
49 "sd-cli.exe"
50 } else {
51 "sd-cli"
52 }
53}
54
55fn library_name() -> &'static str {
57 if cfg!(target_os = "windows") {
58 "stable-diffusion.dll"
59 } else if cfg!(target_os = "macos") {
60 "libstable-diffusion.dylib"
61 } else {
62 "libstable-diffusion.so"
63 }
64}
65
66fn vulkan_loader_name() -> Option<&'static str> {
70 if cfg!(target_os = "windows") {
71 Some("vulkan-1.dll")
72 } else if cfg!(target_os = "macos") {
73 None
74 } else {
75 Some("libvulkan.so.1")
76 }
77}
78
79fn vulkan_remedy() -> &'static str {
83 if cfg!(target_os = "windows") {
84 "install/update your GPU driver (NVIDIA, AMD, or Intel) — it ships \
85 the Vulkan runtime (vulkan-1.dll)"
86 } else {
87 "install the Vulkan loader + a GPU driver, e.g. on Debian/Ubuntu \
88 `sudo apt install libvulkan1 mesa-vulkan-drivers` (plus the \
89 vendor driver for NVIDIA/AMD); verify with `vulkaninfo --summary`"
90 }
91}
92
93#[cfg_attr(coverage_nightly, coverage(off))]
99fn vulkan_loader_loads() -> bool {
100 match vulkan_loader_name() {
101 None => true,
102 Some(name) => unsafe { libloading::Library::new(name).is_ok() },
103 }
104}
105
106fn vulkan_runtime_status_with(loader_loads: bool) -> Result<()> {
112 let Some(loader) = vulkan_loader_name() else {
113 return Ok(()); };
115 if loader_loads {
116 return Ok(());
117 }
118 bail!(
119 "Vulkan runtime not available: the loader `{loader}` could not be \
120 loaded, so stable-diffusion.cpp cannot run on the GPU. We cannot \
121 auto-provision it — {}.",
122 vulkan_remedy()
123 )
124}
125
126#[cfg_attr(coverage_nightly, coverage(off))]
130pub fn vulkan_runtime_status() -> Result<()> {
131 vulkan_runtime_status_with(vulkan_loader_loads())
132}
133
134fn select_release_tag(override_tag: Option<String>) -> String {
141 match override_tag {
142 Some(tag) => {
143 info!(
144 target: TRACE_TARGET,
145 op = "resolve-url",
146 tag = %tag,
147 source = RELEASE_ENV,
148 "using sd-cli release-tag override"
149 );
150 tag
151 }
152 None => {
153 debug!(
154 target: TRACE_TARGET,
155 op = "resolve-url",
156 tag = DEFAULT_RELEASE_TAG,
157 "using pinned sd-cli release tag"
158 );
159 DEFAULT_RELEASE_TAG.to_string()
160 }
161 }
162}
163
164#[cfg_attr(coverage_nightly, coverage(off))]
169fn release_tag() -> String {
170 select_release_tag(std::env::var(RELEASE_ENV).ok())
171}
172
173fn sha_from_tag(tag: &str) -> Result<&str> {
176 match tag.rsplit_once('-') {
177 Some((_, sha)) if !sha.is_empty() => Ok(sha),
178 _ => Err(anyhow!("release tag {tag:?} has no '-<sha>' segment")),
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184enum AssetSource {
185 Upstream,
187 SelfHosted,
191}
192
193fn asset_plan(os: &str, arch: &str) -> Result<(AssetSource, &'static str)> {
198 use AssetSource::*;
199 match (os, arch) {
200 ("windows", "x86_64") => Ok((Upstream, "win-vulkan-x64")),
201 ("linux", "x86_64") => Ok((Upstream, "Linux-Ubuntu-24.04-x86_64-vulkan")),
202 ("macos", "aarch64") | ("macos", "x86_64") => Ok((Upstream, "Darwin-macOS-15.7.7-arm64")),
205 ("linux", "aarch64") => Ok((SelfHosted, "Linux-aarch64-vulkan")),
207 _ => bail!(
208 "no prebuilt stable-diffusion.cpp binary for {os}/{arch}; \
209 install sd-cli manually — see docs/operations/sd-cli-install.md"
210 ),
211 }
212}
213
214fn asset_name(sha: &str, suffix: &str) -> String {
217 format!("sd-master-{sha}-bin-{suffix}.zip")
218}
219
220fn self_hosted_tag(upstream_tag: &str) -> String {
222 format!("sdcpp-prebuilt-{upstream_tag}")
223}
224
225fn download_url(tag: &str, os: &str, arch: &str) -> Result<String> {
228 let sha = sha_from_tag(tag)?;
229 let (source, suffix) = asset_plan(os, arch)?;
230 let asset = asset_name(sha, suffix);
231 Ok(match source {
232 AssetSource::Upstream => format!(
233 "https://github.com/leejet/stable-diffusion.cpp/releases/download/{tag}/{asset}"
234 ),
235 AssetSource::SelfHosted => format!(
236 "https://github.com/webbertakken/studio-worker/releases/download/{}/{asset}",
237 self_hosted_tag(tag)
238 ),
239 })
240}
241
242fn select_url(
248 override_url: Option<String>,
249 default_url: impl FnOnce() -> Result<String>,
250) -> Result<String> {
251 if let Some(url) = override_url {
252 if !url.is_empty() {
253 info!(
254 target: TRACE_TARGET,
255 op = "resolve-url",
256 url = %url,
257 source = URL_ENV,
258 "using sd-cli zip-URL override"
259 );
260 return Ok(url);
261 }
262 warn!(
268 target: TRACE_TARGET,
269 op = "resolve-url",
270 source = URL_ENV,
271 "ignoring empty STUDIO_WORKER_SDCPP_URL override; using the default release URL"
272 );
273 }
274 default_url()
275}
276
277#[cfg_attr(coverage_nightly, coverage(off))]
282fn resolve_url() -> Result<String> {
283 select_url(std::env::var(URL_ENV).ok(), || {
284 download_url(&release_tag(), std::env::consts::OS, std::env::consts::ARCH)
285 })
286}
287
288pub fn library_path_env(sd_cli: &Path) -> Option<(&'static str, PathBuf)> {
294 if cfg!(target_os = "windows") {
295 return None;
296 }
297 let dir = sd_cli.parent()?;
298 if dir.join(library_name()).is_file() {
299 let var = if cfg!(target_os = "macos") {
300 "DYLD_LIBRARY_PATH"
301 } else {
302 "LD_LIBRARY_PATH"
303 };
304 Some((var, dir.to_path_buf()))
305 } else {
306 None
307 }
308}
309
310#[cfg_attr(coverage_nightly, coverage(off))]
316fn extract_zip(zip_path: &Path, dest_dir: &Path) -> Result<usize> {
317 let file =
318 std::fs::File::open(zip_path).with_context(|| format!("opening {}", zip_path.display()))?;
319 let mut archive = zip::ZipArchive::new(file)
320 .with_context(|| format!("reading zip {}", zip_path.display()))?;
321 std::fs::create_dir_all(dest_dir)
322 .with_context(|| format!("creating {}", dest_dir.display()))?;
323 let mut written = 0usize;
324 for i in 0..archive.len() {
325 let mut entry = archive.by_index(i)?;
326 if entry.is_dir() {
327 continue;
328 }
329 let Some(file_name) = Path::new(entry.name()).file_name().map(|n| n.to_owned()) else {
330 warn!(
331 target: TRACE_TARGET,
332 op = "extract",
333 name = entry.name(),
334 "skipping zip entry with no file name"
335 );
336 continue;
337 };
338 let out = dest_dir.join(&file_name);
339 let mode = entry.unix_mode();
340 let mut writer =
341 std::fs::File::create(&out).with_context(|| format!("creating {}", out.display()))?;
342 std::io::copy(&mut entry, &mut writer)
343 .with_context(|| format!("writing {}", out.display()))?;
344 drop(writer);
345 apply_unix_mode(&out, mode)?;
346 written += 1;
347 }
348 Ok(written)
349}
350
351#[cfg(unix)]
353fn apply_unix_mode(path: &Path, mode: Option<u32>) -> Result<()> {
354 use std::os::unix::fs::PermissionsExt;
355 if let Some(mode) = mode {
356 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
357 .with_context(|| format!("chmod {}", path.display()))?;
358 }
359 Ok(())
360}
361
362#[cfg(not(unix))]
363fn apply_unix_mode(_path: &Path, _mode: Option<u32>) -> Result<()> {
364 Ok(())
365}
366
367#[cfg(unix)]
369fn make_executable(path: &Path) -> Result<()> {
370 use std::os::unix::fs::PermissionsExt;
371 let mut perms = std::fs::metadata(path)
372 .with_context(|| format!("stat {}", path.display()))?
373 .permissions();
374 perms.set_mode(perms.mode() | 0o755);
375 std::fs::set_permissions(path, perms).with_context(|| format!("chmod +x {}", path.display()))
376}
377
378#[cfg(not(unix))]
379fn make_executable(_path: &Path) -> Result<()> {
380 Ok(())
381}
382
383fn install_dir(staging: &Path, target: &Path) -> Result<usize> {
388 std::fs::create_dir_all(target).with_context(|| format!("creating {}", target.display()))?;
389 let mut moved = 0usize;
390 for entry in
391 std::fs::read_dir(staging).with_context(|| format!("reading {}", staging.display()))?
392 {
393 let entry = entry?;
394 if !entry.file_type()?.is_file() {
395 continue;
396 }
397 let from = entry.path();
398 let to = target.join(entry.file_name());
399 if to.exists() {
400 std::fs::remove_file(&to).with_context(|| format!("replacing {}", to.display()))?;
401 }
402 if std::fs::rename(&from, &to).is_err() {
403 std::fs::copy(&from, &to)
404 .with_context(|| format!("copying {} -> {}", from.display(), to.display()))?;
405 }
406 moved += 1;
407 }
408 Ok(moved)
409}
410
411fn clean_scratch(zip_path: &Path, staging: &Path) {
418 if let Err(e) = std::fs::remove_file(zip_path) {
419 if e.kind() != std::io::ErrorKind::NotFound {
420 warn!(
421 target: TRACE_TARGET,
422 op = "cleanup",
423 path = %zip_path.display(),
424 error = %e,
425 "could not remove sd-cli scratch zip; it may fill the disk"
426 );
427 }
428 }
429 if let Err(e) = std::fs::remove_dir_all(staging) {
430 if e.kind() != std::io::ErrorKind::NotFound {
431 warn!(
432 target: TRACE_TARGET,
433 op = "cleanup",
434 path = %staging.display(),
435 error = %e,
436 "could not remove sd-cli staging dir; it may fill the disk"
437 );
438 }
439 }
440}
441
442#[cfg_attr(coverage_nightly, coverage(off))]
452pub fn provision(models_root: &Path) -> Result<PathBuf> {
453 let target_dir = models_root.join("bin");
454 let binary = target_dir.join(binary_name());
455 if binary.is_file() {
456 return Ok(binary);
457 }
458
459 let url = resolve_url()?;
460 info!(
461 target: TRACE_TARGET,
462 op = "provision",
463 url = %url,
464 dest = %target_dir.display(),
465 "sd-cli not found; provisioning stable-diffusion.cpp"
466 );
467
468 std::fs::create_dir_all(models_root)
469 .with_context(|| format!("creating {}", models_root.display()))?;
470 let stamp = format!("{}-{}", std::process::id(), now_nanos());
471 let zip_path = models_root.join(format!(".sd-cli-{stamp}.zip"));
472 let staging = models_root.join(format!(".sd-cli-staging-{stamp}"));
473
474 let result = (|| -> Result<PathBuf> {
475 download::download_file(&url, &zip_path)
476 .with_context(|| format!("downloading sd-cli zip from {url}"))?;
477 let count = extract_zip(&zip_path, &staging)?;
478 let staged_binary = staging.join(binary_name());
479 if !staged_binary.is_file() {
480 bail!(
481 "downloaded sd-cli zip from {url} did not contain {} (extracted {count} files)",
482 binary_name()
483 );
484 }
485 install_dir(&staging, &target_dir)?;
486 make_executable(&binary)?;
487 if !binary.is_file() {
488 bail!("sd-cli install left no binary at {}", binary.display());
489 }
490 Ok(binary.clone())
491 })();
492
493 clean_scratch(&zip_path, &staging);
498
499 match &result {
500 Ok(path) => info!(
501 target: TRACE_TARGET,
502 op = "provision",
503 path = %path.display(),
504 "sd-cli provisioned"
505 ),
506 Err(e) => warn!(
507 target: TRACE_TARGET,
508 op = "provision",
509 error = %e,
510 "sd-cli provisioning failed"
511 ),
512 }
513 result
514}
515
516#[cfg_attr(coverage_nightly, coverage(off))]
517fn now_nanos() -> i64 {
518 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use std::io::Write;
525 use tempfile::tempdir;
526
527 #[test]
528 fn sha_from_tag_takes_trailing_segment() {
529 assert_eq!(sha_from_tag("master-669-2d40a8b").unwrap(), "2d40a8b");
530 assert_eq!(sha_from_tag("master-1-abc").unwrap(), "abc");
531 }
532
533 #[test]
534 fn sha_from_tag_rejects_a_tag_without_a_sha() {
535 assert!(sha_from_tag("master").is_err());
536 assert!(sha_from_tag("trailing-").is_err());
537 }
538
539 #[test]
540 fn asset_plan_picks_vulkan_or_universal_for_supported_targets() {
541 use AssetSource::*;
542 assert_eq!(
543 asset_plan("windows", "x86_64").unwrap(),
544 (Upstream, "win-vulkan-x64")
545 );
546 assert_eq!(
547 asset_plan("linux", "x86_64").unwrap(),
548 (Upstream, "Linux-Ubuntu-24.04-x86_64-vulkan")
549 );
550 assert_eq!(
551 asset_plan("macos", "aarch64").unwrap(),
552 (Upstream, "Darwin-macOS-15.7.7-arm64")
553 );
554 }
555
556 #[test]
557 fn asset_plan_makes_intel_mac_and_arm_linux_first_class() {
558 use AssetSource::*;
559 assert_eq!(
561 asset_plan("macos", "x86_64").unwrap(),
562 (Upstream, "Darwin-macOS-15.7.7-arm64")
563 );
564 assert_eq!(
566 asset_plan("linux", "aarch64").unwrap(),
567 (SelfHosted, "Linux-aarch64-vulkan")
568 );
569 }
570
571 #[test]
572 fn asset_plan_rejects_unsupported_targets_with_guidance() {
573 let err = asset_plan("freebsd", "x86_64").unwrap_err().to_string();
574 assert!(err.contains("no prebuilt"), "got: {err}");
575 assert!(
576 err.contains("sd-cli-install.md"),
577 "points to the doc: {err}"
578 );
579 assert!(asset_plan("windows", "aarch64").is_err());
580 }
581
582 #[test]
583 fn asset_name_embeds_sha_and_platform() {
584 assert_eq!(
585 asset_name("2d40a8b", "win-vulkan-x64"),
586 "sd-master-2d40a8b-bin-win-vulkan-x64.zip"
587 );
588 assert_eq!(
589 asset_name("2d40a8b", "Linux-aarch64-vulkan"),
590 "sd-master-2d40a8b-bin-Linux-aarch64-vulkan.zip"
591 );
592 }
593
594 #[test]
595 fn download_url_targets_upstream_for_covered_platforms() {
596 let url = download_url("master-669-2d40a8b", "windows", "x86_64").unwrap();
597 let expected = concat!(
598 "https://github.com/leejet/stable-diffusion.cpp/releases/download/",
599 "master-669-2d40a8b/sd-master-2d40a8b-bin-win-vulkan-x64.zip"
600 );
601 assert_eq!(url, expected);
602 }
603
604 #[test]
605 fn download_url_targets_our_release_for_arm_linux() {
606 let url = download_url("master-669-2d40a8b", "linux", "aarch64").unwrap();
607 let expected = concat!(
608 "https://github.com/webbertakken/studio-worker/releases/download/",
609 "sdcpp-prebuilt-master-669-2d40a8b/",
610 "sd-master-2d40a8b-bin-Linux-aarch64-vulkan.zip"
611 );
612 assert_eq!(url, expected);
613 }
614
615 #[test]
616 fn download_url_uses_universal_darwin_asset_for_intel_mac() {
617 let arm = download_url("master-669-2d40a8b", "macos", "aarch64").unwrap();
618 let intel = download_url("master-669-2d40a8b", "macos", "x86_64").unwrap();
619 assert_eq!(arm, intel, "Intel Macs use the same universal2 asset");
620 assert!(intel.contains("Darwin-macOS-15.7.7-arm64"), "got: {intel}");
621 }
622
623 #[test]
624 fn select_release_tag_prefers_the_override() {
625 assert_eq!(
626 select_release_tag(Some("master-700-deadbee".into())),
627 "master-700-deadbee"
628 );
629 }
630
631 #[test]
632 fn select_release_tag_falls_back_to_the_pinned_default() {
633 assert_eq!(select_release_tag(None), DEFAULT_RELEASE_TAG);
634 }
635
636 #[test]
637 fn select_release_tag_logs_the_override_source() {
638 let logs = crate::test_support::capture(|| {
639 let _ = select_release_tag(Some("master-700-deadbee".into()));
640 });
641 assert!(
642 logs.contains("STUDIO_WORKER_SDCPP_RELEASE"),
643 "override log must name the env var: {logs}"
644 );
645 assert!(logs.contains("master-700-deadbee"), "got: {logs}");
646 assert!(logs.contains("override"), "got: {logs}");
647 }
648
649 #[test]
650 fn select_url_prefers_a_non_empty_override() {
651 let url = select_url(Some("https://mirror.example/sd.zip".into()), || {
652 panic!("default must not be consulted when an override is present")
653 })
654 .unwrap();
655 assert_eq!(url, "https://mirror.example/sd.zip");
656 }
657
658 #[test]
659 fn select_url_ignores_an_empty_override_and_falls_back() {
660 let url = select_url(Some(String::new()), || Ok("fallback".into())).unwrap();
661 assert_eq!(url, "fallback");
662 }
663
664 #[test]
665 fn select_url_falls_back_when_no_override_is_set() {
666 let url = select_url(None, || Ok("fallback".into())).unwrap();
667 assert_eq!(url, "fallback");
668 }
669
670 #[test]
671 fn select_url_propagates_a_default_resolution_error() {
672 let err = select_url(None, || bail!("no prebuilt for this platform"))
673 .unwrap_err()
674 .to_string();
675 assert!(err.contains("no prebuilt"), "got: {err}");
676 }
677
678 #[test]
679 fn select_url_logs_the_override_source() {
680 let logs = crate::test_support::capture(|| {
681 let _ = select_url(Some("https://mirror.example/sd.zip".into()), || {
682 Ok("unused".into())
683 });
684 });
685 assert!(
686 logs.contains("STUDIO_WORKER_SDCPP_URL"),
687 "override log must name the env var: {logs}"
688 );
689 assert!(
690 logs.contains("https://mirror.example/sd.zip"),
691 "got: {logs}"
692 );
693 }
694
695 #[test]
696 fn select_url_warns_when_the_override_is_present_but_empty() {
697 let logs = crate::test_support::capture(|| {
705 let url = select_url(Some(String::new()), || Ok("fallback".into())).unwrap();
706 assert_eq!(url, "fallback", "an empty override must still fall back");
707 });
708 assert!(
709 logs.contains("WARN"),
710 "expected a WARN breadcrumb, got: {logs}"
711 );
712 assert!(
713 logs.contains("STUDIO_WORKER_SDCPP_URL"),
714 "the warning must name the ignored env var: {logs}"
715 );
716 assert!(
717 logs.contains("op=\"resolve-url\""),
718 "expected the resolve-url op field: {logs}"
719 );
720 }
721
722 #[test]
723 fn install_dir_moves_files_and_overwrites() {
724 let staging = tempdir().unwrap();
725 let target = tempdir().unwrap();
726 std::fs::write(staging.path().join("sd-cli"), b"new-binary").unwrap();
727 std::fs::write(staging.path().join("libstable-diffusion.so"), b"lib").unwrap();
728 std::fs::write(target.path().join("sd-cli"), b"old-binary").unwrap();
730
731 let moved = install_dir(staging.path(), target.path()).unwrap();
732 assert_eq!(moved, 2);
733 assert_eq!(
734 std::fs::read(target.path().join("sd-cli")).unwrap(),
735 b"new-binary"
736 );
737 assert_eq!(
738 std::fs::read(target.path().join("libstable-diffusion.so")).unwrap(),
739 b"lib"
740 );
741 assert!(!staging.path().join("sd-cli").exists());
743 }
744
745 #[test]
746 fn install_dir_skips_subdirectories_and_counts_only_files() {
747 let staging = tempdir().unwrap();
753 let target = tempdir().unwrap();
754 std::fs::write(staging.path().join("sd-cli"), b"binary").unwrap();
755 std::fs::write(staging.path().join("libstable-diffusion.so"), b"lib").unwrap();
756 let nested = staging.path().join("nested");
757 std::fs::create_dir(&nested).unwrap();
758 std::fs::write(nested.join("buried"), b"should-not-publish").unwrap();
759
760 let moved = install_dir(staging.path(), target.path()).unwrap();
761
762 assert_eq!(moved, 2);
764 assert!(target.path().join("sd-cli").is_file());
765 assert!(target.path().join("libstable-diffusion.so").is_file());
766 assert!(
768 !target.path().join("nested").exists(),
769 "a staging subdirectory must not be published"
770 );
771 assert!(
772 !target.path().join("buried").exists(),
773 "a staging subdirectory's contents must not be flattened into the target"
774 );
775 }
776
777 #[test]
778 fn clean_scratch_removes_zip_and_staging_quietly() {
779 let dir = tempdir().unwrap();
780 let zip = dir.path().join("scratch.zip");
781 let staging = dir.path().join("staging");
782 std::fs::write(&zip, b"zip").unwrap();
783 std::fs::create_dir_all(&staging).unwrap();
784 std::fs::write(staging.join("sd-cli"), b"bin").unwrap();
785
786 let (zip_c, staging_c) = (zip.clone(), staging.clone());
787 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
788
789 assert!(!zip.exists(), "scratch zip must be removed");
790 assert!(!staging.exists(), "staging dir must be removed");
791 assert!(
792 !logs.contains("could not remove"),
793 "a clean removal must not warn: {logs}"
794 );
795 }
796
797 #[test]
798 fn clean_scratch_is_silent_when_paths_are_already_gone() {
799 let dir = tempdir().unwrap();
800 let zip = dir.path().join("missing.zip");
801 let staging = dir.path().join("missing-staging");
802
803 let (zip_c, staging_c) = (zip.clone(), staging.clone());
804 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
805
806 assert!(
808 !logs.contains("could not remove"),
809 "an already-clean slot must not warn: {logs}"
810 );
811 }
812
813 #[test]
814 fn clean_scratch_warns_when_removal_fails() {
815 let dir = tempdir().unwrap();
816 let zip = dir.path().join("zip-slot");
822 std::fs::create_dir_all(&zip).unwrap();
823 let staging = dir.path().join("staging-slot");
824 std::fs::write(&staging, b"not a dir").unwrap();
825
826 let (zip_c, staging_c) = (zip.clone(), staging.clone());
827 let logs = crate::test_support::capture(move || clean_scratch(&zip_c, &staging_c));
828
829 assert!(
830 logs.matches("could not remove").count() >= 2,
831 "both failed removals must warn: {logs}"
832 );
833 assert!(
834 logs.contains("fill the disk"),
835 "the warning must flag the disk-fill risk: {logs}"
836 );
837 }
838
839 #[test]
840 fn extract_zip_flattens_and_defuses_zip_slip() {
841 let dir = tempdir().unwrap();
842 let zip_path = dir.path().join("test.zip");
843 {
846 let file = std::fs::File::create(&zip_path).unwrap();
847 let mut zw = zip::ZipWriter::new(file);
848 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
849 .compression_method(zip::CompressionMethod::Deflated);
850 zw.start_file("sd-cli", opts).unwrap();
851 zw.write_all(b"binary").unwrap();
852 zw.start_file("nested/libstable-diffusion.so", opts)
853 .unwrap();
854 zw.write_all(b"lib").unwrap();
855 zw.start_file("../../escape.txt", opts).unwrap();
856 zw.write_all(b"evil").unwrap();
857 zw.finish().unwrap();
858 }
859 let dest = dir.path().join("out");
860 let count = extract_zip(&zip_path, &dest).unwrap();
861 assert_eq!(count, 3);
862 assert_eq!(std::fs::read(dest.join("sd-cli")).unwrap(), b"binary");
863 assert_eq!(
864 std::fs::read(dest.join("libstable-diffusion.so")).unwrap(),
865 b"lib"
866 );
867 assert!(dest.join("escape.txt").is_file());
870 assert!(!dir.path().join("escape.txt").exists());
871 }
872
873 #[test]
874 fn extract_zip_skips_directory_entries() {
875 let dir = tempdir().unwrap();
884 let zip_path = dir.path().join("with-dirs.zip");
885 {
886 let file = std::fs::File::create(&zip_path).unwrap();
887 let mut zw = zip::ZipWriter::new(file);
888 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
889 .compression_method(zip::CompressionMethod::Deflated);
890 zw.add_directory("build/", opts).unwrap();
891 zw.start_file("sd-cli", opts).unwrap();
892 zw.write_all(b"binary").unwrap();
893 zw.add_directory("nested/empty/", opts).unwrap();
894 zw.finish().unwrap();
895 }
896 let dest = dir.path().join("out");
897 let count = extract_zip(&zip_path, &dest).unwrap();
898 assert_eq!(
900 count, 1,
901 "directory entries must not count as written files"
902 );
903 assert_eq!(std::fs::read(dest.join("sd-cli")).unwrap(), b"binary");
904 assert!(
906 !dest.join("build").exists(),
907 "a directory entry must not become a file in the flat output"
908 );
909 assert!(
910 !dest.join("empty").exists(),
911 "a nested directory entry must not become a file either"
912 );
913 }
914
915 #[cfg(unix)]
916 #[test]
917 fn extract_zip_preserves_exec_bit() {
918 use std::os::unix::fs::PermissionsExt;
919 let dir = tempdir().unwrap();
920 let zip_path = dir.path().join("exec.zip");
921 {
922 let file = std::fs::File::create(&zip_path).unwrap();
923 let mut zw = zip::ZipWriter::new(file);
924 let opts: zip::write::FileOptions<()> = zip::write::FileOptions::default()
925 .compression_method(zip::CompressionMethod::Deflated)
926 .unix_permissions(0o755);
927 zw.start_file("sd-cli", opts).unwrap();
928 zw.write_all(b"#!/bin/sh\n").unwrap();
929 zw.finish().unwrap();
930 }
931 let dest = dir.path().join("out");
932 extract_zip(&zip_path, &dest).unwrap();
933 let mode = std::fs::metadata(dest.join("sd-cli"))
934 .unwrap()
935 .permissions()
936 .mode();
937 assert!(mode & 0o111 != 0, "exec bit must survive: {mode:o}");
938 }
939
940 #[cfg(unix)]
941 #[test]
942 fn library_path_env_points_loader_at_sibling_lib() {
943 let dir = tempdir().unwrap();
944 let sd_cli = dir.path().join(binary_name());
945 std::fs::write(&sd_cli, b"bin").unwrap();
946 assert!(library_path_env(&sd_cli).is_none());
948 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
950 let (var, env_dir) = library_path_env(&sd_cli).expect("sibling lib resolved");
951 assert!(var == "LD_LIBRARY_PATH" || var == "DYLD_LIBRARY_PATH");
952 assert_eq!(env_dir, dir.path());
953 }
954
955 #[test]
956 fn vulkan_status_ok_when_loader_loads() {
957 assert!(vulkan_runtime_status_with(true).is_ok());
960 }
961
962 #[test]
963 fn vulkan_status_errors_with_actionable_remedy_when_missing() {
964 let result = vulkan_runtime_status_with(false);
965 if cfg!(target_os = "macos") {
966 assert!(result.is_ok());
968 } else {
969 let err = result.unwrap_err().to_string();
970 assert!(err.contains("Vulkan runtime"), "got: {err}");
971 assert!(
972 err.contains("auto-provision"),
973 "must say we can't auto-provision it: {err}"
974 );
975 if cfg!(target_os = "windows") {
977 assert!(err.contains("vulkan-1.dll"), "got: {err}");
978 assert!(err.contains("GPU driver"), "got: {err}");
979 } else {
980 assert!(err.contains("libvulkan1"), "got: {err}");
981 assert!(err.contains("vulkaninfo"), "got: {err}");
982 }
983 }
984 }
985
986 #[cfg(target_os = "windows")]
987 #[test]
988 fn library_path_env_is_none_on_windows() {
989 let dir = tempdir().unwrap();
990 let sd_cli = dir.path().join(binary_name());
991 std::fs::write(&sd_cli, b"bin").unwrap();
992 std::fs::write(dir.path().join(library_name()), b"lib").unwrap();
993 assert!(library_path_env(&sd_cli).is_none());
994 }
995}