use apple_bundles::MacOsApplicationBundleBuilder; use apple_codesign::{SigningSettings, UnifiedSigner}; use flate2::{Compression, write::GzEncoder}; use icns::{IconFamily, Image}; use plist::{Dictionary, Value}; use sha2::{Digest, Sha256}; use simple_file_manifest::FileEntry; use std::{ error::Error, ffi::OsStr, fs::{self, File}, io::{self, BufReader, Read, Seek}, path::{Path, PathBuf}, }; use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions}; const APP_NAME: &str = "Blogging Desktop Server"; const APP_EXECUTABLE: &str = "bds-ui"; const BUNDLE_ID: &str = "de.rfc1437.ruds"; const MIB: u64 = 1024 * 1024; const LC_LOAD_DYLIB: u32 = 0x0c; const LC_ID_DYLIB: u32 = 0x0d; const LC_LOAD_WEAK_DYLIB: u32 = 0x8000_0018; const LC_REEXPORT_DYLIB: u32 = 0x8000_001f; const LC_LAZY_LOAD_DYLIB: u32 = 0x20; const LC_LOAD_UPWARD_DYLIB: u32 = 0x8000_0023; const DIRECTML_VERSION: &str = "1.15.4"; const DIRECTML_SHA256: &str = "4e7cb7ddce8cf837a7a75dc029209b520ca0101470fcdf275c1f49736a3615b9"; const MACOS_X64_ORT_VERSION: &str = "1.23.2"; const MACOS_X64_ORT_SHA256: &str = "d10359e16347b57d9959f7e80a225a5b4a66ed7d7e007274a15cae86836485a6"; pub(crate) fn prepare_macos_linker_alias(directory: &Path) -> Result<(), Box> { fs::create_dir_all(directory)?; fs::write(directory.join("libclang_rt.osx.a"), b"!\n")?; Ok(()) } pub(crate) fn prepare_macos_x64_runtime(directory: &Path) -> Result<(), Box> { fs::create_dir_all(directory)?; let archive_path = directory.join(format!( "onnxruntime-osx-x86_64-{MACOS_X64_ORT_VERSION}.tgz" )); let result = (|| { let url = format!( "https://github.com/microsoft/onnxruntime/releases/download/v{MACOS_X64_ORT_VERSION}/onnxruntime-osx-x86_64-{MACOS_X64_ORT_VERSION}.tgz" ); let mut response = reqwest::blocking::get(url)?.error_for_status()?; let mut archive = File::create(&archive_path)?; io::copy(&mut response, &mut archive)?; drop(archive); let mut archive = BufReader::new(File::open(&archive_path)?); let mut digest = Sha256::new(); let mut buffer = [0_u8; 64 * 1024]; loop { let read = archive.read(&mut buffer)?; if read == 0 { break; } digest.update(&buffer[..read]); } if format!("{:x}", digest.finalize()) != MACOS_X64_ORT_SHA256 { return Err("macOS x64 ONNX Runtime archive checksum mismatch".into()); } prepare_macos_x64_runtime_from_archive(File::open(&archive_path)?, directory) })(); if archive_path.try_exists()? { fs::remove_file(archive_path)?; } result } fn prepare_macos_x64_runtime_from_archive( archive: impl Read, directory: &Path, ) -> Result<(), Box> { fs::create_dir_all(directory)?; let decoder = flate2::read::GzDecoder::new(archive); let mut archive = tar::Archive::new(decoder); for entry in archive.entries()? { let mut entry = entry?; let path = entry.path()?; let destination = if path.ends_with(format!("lib/libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib")) { Some(format!("libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib")) } else if path.ends_with("LICENSE") { Some("ONNXRuntime-LICENSE.txt".to_owned()) } else { None }; if let Some(destination) = destination { io::copy(&mut entry, &mut File::create(directory.join(destination))?)?; } } let versioned = directory.join(format!("libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib")); require_file(&versioned)?; require_file(&directory.join("ONNXRuntime-LICENSE.txt"))?; fs::copy(versioned, directory.join("libonnxruntime.dylib"))?; Ok(()) } pub(crate) fn prepare_windows_runtime( directory: &Path, sdk_libraries: &Path, ) -> Result<(), Box> { fs::create_dir_all(directory)?; let package_path = directory.join(format!("microsoft.ai.directml.{DIRECTML_VERSION}.nupkg")); let result = (|| { let url = format!( "https://api.nuget.org/v3-flatcontainer/microsoft.ai.directml/{DIRECTML_VERSION}/microsoft.ai.directml.{DIRECTML_VERSION}.nupkg" ); let mut response = reqwest::blocking::get(url)?.error_for_status()?; let mut package = File::create(&package_path)?; io::copy(&mut response, &mut package)?; drop(package); let mut package = BufReader::new(File::open(&package_path)?); let mut digest = Sha256::new(); let mut buffer = [0_u8; 64 * 1024]; loop { let read = package.read(&mut buffer)?; if read == 0 { break; } digest.update(&buffer[..read]); } let digest = digest.finalize(); if format!("{digest:x}") != DIRECTML_SHA256 { return Err("DirectML package checksum mismatch".into()); } prepare_windows_runtime_from_archive(File::open(&package_path)?, directory)?; prepare_windows_sdk_linker_aliases(sdk_libraries, directory) })(); if package_path.try_exists()? { fs::remove_file(package_path)?; } result } fn prepare_windows_sdk_linker_aliases( sdk_libraries: &Path, directory: &Path, ) -> Result<(), Box> { let source = fs::read_dir(sdk_libraries)? .filter_map(Result::ok) .find(|entry| { entry .file_name() .to_str() .is_some_and(|name| name.eq_ignore_ascii_case("pathcch.lib")) }) .ok_or_else(|| { format!( "PathCch.lib is missing from Windows SDK directory: {}", sdk_libraries.display() ) })? .path(); fs::copy(source, directory.join("PathCch.lib"))?; Ok(()) } fn prepare_windows_runtime_from_archive( archive: impl Read + Seek, directory: &Path, ) -> Result<(), Box> { fs::create_dir_all(directory)?; let mut archive = zip::ZipArchive::new(archive)?; for (source, destination) in [ ("bin/x64-win/DirectML.lib", "DirectML.lib"), ("bin/x64-win/DirectML.dll", "DirectML.dll"), ("LICENSE.txt", "DirectML-LICENSE.txt"), ] { let mut source = archive.by_name(source)?; io::copy(&mut source, &mut File::create(directory.join(destination))?)?; } Ok(()) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Platform { MacOs, Linux, Windows, } impl Platform { fn from_label(label: &str) -> Result> { if label.starts_with("darwin-") { Ok(Self::MacOs) } else if label.starts_with("linux-") { Ok(Self::Linux) } else if label.starts_with("windows-") { Ok(Self::Windows) } else { Err(format!("unsupported release platform: {label}").into()) } } fn executable(self, name: &str) -> String { if self == Self::Windows { format!("{name}.exe") } else { name.to_owned() } } fn shared_libraries(self) -> [&'static str; 2] { match self { Self::MacOs => ["libbds_core.dylib", "libbds_server.dylib"], Self::Linux => ["libbds_core.so", "libbds_server.so"], Self::Windows => ["bds_core.dll", "bds_server.dll"], } } fn is_dynamic_std(self, path: &Path) -> bool { let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default(); match self { Self::MacOs => name.starts_with("libstd-") && name.ends_with(".dylib"), Self::Linux => name.starts_with("libstd-") && name.ends_with(".so"), Self::Windows => name.starts_with("std-") && name.ends_with(".dll"), } } } pub(crate) fn package( release_tag: &str, release_dir: &Path, rust_lib_dir: &Path, platform_label: &str, dist_dir: &Path, ) -> Result, Box> { let platform = Platform::from_label(platform_label)?; fs::create_dir_all(dist_dir)?; let version = safe_tag(release_tag); let runtime = runtime_files(platform, release_dir, rust_lib_dir)?; let cli_files = executable_files(platform, release_dir, &["bds-cli", "bds-mcp"])?; let cli_name = format!("bds-cli-{version}-{platform_label}"); let desktop_name = format!("bds-desktop-{version}-{platform_label}"); let cli_asset = match platform { Platform::Windows => { let path = dist_dir.join(format!("{cli_name}.zip")); zip_archive(&path, &cli_name, cli_files.iter().chain(&runtime))?; path } Platform::MacOs | Platform::Linux => { let path = dist_dir.join(format!("{cli_name}.tar.gz")); tar_gz_archive(&path, &cli_name, cli_files.iter().chain(&runtime))?; path } }; let desktop_files = executable_files( platform, release_dir, &[APP_EXECUTABLE, "bds-cli", "bds-mcp"], )?; let desktop_asset = match platform { Platform::MacOs => { let path = dist_dir.join(format!("{desktop_name}.dmg")); macos_dmg(release_tag, &path, desktop_files.iter().chain(&runtime))?; path } Platform::Linux => { let path = dist_dir.join(format!("{desktop_name}.tar.gz")); tar_gz_archive(&path, &desktop_name, desktop_files.iter().chain(&runtime))?; path } Platform::Windows => { let path = dist_dir.join(format!("{desktop_name}.zip")); zip_archive(&path, &desktop_name, desktop_files.iter().chain(&runtime))?; path } }; println!("created {}", cli_asset.display()); println!("created {}", desktop_asset.display()); Ok(vec![cli_asset, desktop_asset]) } fn executable_files( platform: Platform, release_dir: &Path, names: &[&str], ) -> Result, Box> { names .iter() .map(|name| { let file_name = platform.executable(name); let path = release_dir.join(&file_name); require_file(&path)?; Ok((path, file_name)) }) .collect() } fn runtime_files( platform: Platform, release_dir: &Path, rust_lib_dir: &Path, ) -> Result, Box> { let mut files = platform .shared_libraries() .into_iter() .map(|name| { let path = release_dir.join(name); require_file(&path)?; Ok((path, name.to_owned())) }) .collect::, Box>>()?; let mut std_libraries = fs::read_dir(rust_lib_dir)? .filter_map(Result::ok) .map(|entry| entry.path()) .filter(|path| platform.is_dynamic_std(path)) .collect::>(); std_libraries.sort(); for path in std_libraries { let name = path .file_name() .and_then(OsStr::to_str) .ok_or_else(|| format!("invalid runtime filename: {}", path.display()))? .to_owned(); files.push((path, name)); } let mut external_names = match platform { Platform::MacOs => { let mut names = fs::read_dir(release_dir)? .filter_map(Result::ok) .map(|entry| entry.file_name()) .filter_map(|name| name.into_string().ok()) .filter(|name| name.starts_with("libonnxruntime") && name.contains(".dylib")) .collect::>(); let has_versioned = names.iter().any(|name| name != "libonnxruntime.dylib"); if has_versioned { names.retain(|name| name != "libonnxruntime.dylib"); } if release_dir.join("ONNXRuntime-LICENSE.txt").is_file() { names.push("ONNXRuntime-LICENSE.txt".to_owned()); } names } Platform::Linux => fs::read_dir(release_dir)? .filter_map(Result::ok) .map(|entry| entry.file_name()) .filter_map(|name| name.into_string().ok()) .filter(|name| name.starts_with("libonnxruntime.so")) .collect::>(), Platform::Windows => ["DirectML.dll", "DirectML-LICENSE.txt"] .into_iter() .map(str::to_owned) .collect(), }; external_names.sort(); for name in external_names { let path = release_dir.join(&name); require_file(&path)?; files.push((path, name)); } Ok(files) } fn require_file(path: &Path) -> Result<(), Box> { if path.is_file() { Ok(()) } else { Err(format!("release file is missing: {}", path.display()).into()) } } fn ensure_new(path: &Path) -> Result<(), Box> { if path.try_exists()? { Err(format!("output already exists: {}", path.display()).into()) } else { Ok(()) } } fn tar_gz_archive<'a>( output: &Path, root: &str, files: impl Iterator, ) -> Result<(), Box> { ensure_new(output)?; let encoder = GzEncoder::new(File::create(output)?, Compression::best()); let mut archive = tar::Builder::new(encoder); for (source, name) in files { archive.append_path_with_name(source, Path::new(root).join(name))?; } archive.into_inner()?.finish()?; Ok(()) } fn zip_archive<'a>( output: &Path, root: &str, files: impl Iterator, ) -> Result<(), Box> { ensure_new(output)?; let mut archive = ZipWriter::new(File::create(output)?); let options = SimpleFileOptions::default() .compression_method(CompressionMethod::Deflated) .unix_permissions(0o755); for (source, name) in files { archive.start_file(format!("{root}/{name}"), options)?; io::copy(&mut File::open(source)?, &mut archive)?; } archive.finish()?; Ok(()) } fn macos_dmg<'a>( release_tag: &str, output: &Path, files: impl Iterator, ) -> Result<(), Box> { let files = files.collect::>(); ensure_new(output)?; let version = bundle_version(release_tag)?; let output_dir = output.parent().unwrap_or_else(|| Path::new(".")); let bundle_path = output_dir.join(format!("{APP_NAME}.app")); ensure_new(&bundle_path)?; let mut bundle = MacOsApplicationBundleBuilder::new(APP_NAME)?; bundle.set_info_plist_required_keys(APP_NAME, BUNDLE_ID, version, "RuDS", APP_EXECUTABLE)?; bundle.set_info_plist_key("CFBundleShortVersionString", version)?; bundle.set_info_plist_key("CFBundleIconFile", format!("{APP_NAME}.icns"))?; bundle.set_info_plist_key("CFBundlePackageType", "APPL")?; bundle.set_info_plist_key( "LSApplicationCategoryType", "public.app-category.productivity", )?; bundle.set_info_plist_key("LSMinimumSystemVersion", "26.0")?; bundle.set_info_plist_key("NSHighResolutionCapable", true)?; bundle.set_info_plist_key("CFBundleURLTypes", url_types())?; bundle.add_icon(icns()?)?; for (source, name) in &files { let entry = FileEntry::new_from_path(source, true); if is_macos_bundle_resource(name) { bundle.add_file_resources(name, entry)?; } else { bundle.add_file_macos(name, entry)?; } } let materialized = bundle.materialize_bundle(output_dir)?; let runtime_names = files .iter() .filter(|(_, name)| name.ends_with(".dylib")) .map(|(_, name)| name.clone()) .collect::>(); for (_, name) in &files { let staged = if is_macos_bundle_resource(name) { materialized.join("Contents/Resources").join(name) } else { materialized.join("Contents/MacOS").join(name) }; if is_macos_macho(name) { rewrite_macho_rpaths(&staged, &runtime_names)?; } } let signer = UnifiedSigner::new(SigningSettings::default()); signer.sign_path_in_place(&materialized)?; apple_dmg::create_dmg( &materialized, output, APP_NAME, dmg_sectors(directory_size(&materialized)?)?, )?; signer.sign_path_in_place(output)?; fs::remove_dir_all(materialized)?; Ok(()) } fn is_macos_bundle_resource(name: &str) -> bool { name.ends_with(".dylib") || name.ends_with(".txt") } fn is_macos_macho(name: &str) -> bool { !name.ends_with(".txt") } fn rewrite_macho_rpaths(path: &Path, runtime_names: &[String]) -> Result<(), Box> { let mut bytes = fs::read(path)?; if read_u32(&bytes, 0)? != 0xfeed_facf { return Err(format!( "release file is not a 64-bit little-endian Mach-O: {}", path.display() ) .into()); } let command_count = read_u32(&bytes, 16)? as usize; let commands_size = read_u32(&bytes, 20)? as usize; let commands_end = 32_usize .checked_add(commands_size) .filter(|end| *end <= bytes.len()) .ok_or_else(|| format!("invalid Mach-O load commands in {}", path.display()))?; let mut command_start = 32_usize; for _ in 0..command_count { let command = read_u32(&bytes, command_start)?; let command_size = read_u32(&bytes, command_start + 4)? as usize; let command_end = command_start .checked_add(command_size) .filter(|end| command_size >= 8 && *end <= commands_end) .ok_or_else(|| format!("invalid Mach-O load command in {}", path.display()))?; if is_dylib_command(command) { if command_size < 24 { return Err(format!("invalid Mach-O dylib command in {}", path.display()).into()); } let name_offset = read_u32(&bytes, command_start + 8)? as usize; let name_start = command_start .checked_add(name_offset) .filter(|start| name_offset >= 24 && *start < command_end) .ok_or_else(|| format!("invalid Mach-O dylib name in {}", path.display()))?; let name_length = bytes[name_start..command_end] .iter() .position(|byte| *byte == 0) .ok_or_else(|| format!("unterminated Mach-O dylib name in {}", path.display()))?; let current = std::str::from_utf8(&bytes[name_start..name_start + name_length])?; let file_name = current.rsplit('/').next().unwrap_or(current); if runtime_names.iter().any(|runtime| runtime == file_name) { let replacement = format!("@rpath/{file_name}"); let capacity = command_end - name_start; if replacement.len() + 1 > capacity { return Err(format!( "Mach-O load command has no room for {replacement} in {}", path.display() ) .into()); } bytes[name_start..command_end].fill(0); bytes[name_start..name_start + replacement.len()] .copy_from_slice(replacement.as_bytes()); } } command_start = command_end; } if command_start != commands_end { return Err(format!("inconsistent Mach-O load commands in {}", path.display()).into()); } fs::write(path, bytes)?; Ok(()) } fn read_u32(bytes: &[u8], offset: usize) -> Result> { let value = bytes .get(offset..offset + 4) .ok_or("truncated Mach-O data")?; Ok(u32::from_le_bytes(value.try_into()?)) } fn is_dylib_command(command: u32) -> bool { matches!( command, LC_LOAD_DYLIB | LC_ID_DYLIB | LC_LOAD_WEAK_DYLIB | LC_REEXPORT_DYLIB | LC_LAZY_LOAD_DYLIB | LC_LOAD_UPWARD_DYLIB ) } fn url_types() -> Value { let mut url_type = Dictionary::new(); url_type.insert("CFBundleURLName".to_owned(), BUNDLE_ID.into()); url_type.insert( "CFBundleURLSchemes".to_owned(), Value::Array(vec!["ruds".into()]), ); Value::Array(vec![Value::Dictionary(url_type)]) } fn icns() -> Result, Box> { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../bds-ui/assets/app-icons/bds.png"); let image = Image::read_png(BufReader::new(File::open(path)?))?; let mut family = IconFamily::new(); family.add_icon(&image)?; let mut bytes = Vec::new(); family.write(&mut bytes)?; Ok(bytes) } fn bundle_version(tag: &str) -> Result<&str, Box> { let version = tag.strip_prefix('v').unwrap_or(tag); let version = version.split(['-', '+']).next().unwrap_or_default(); let parts = version.split('.').collect::>(); if !(1..=3).contains(&parts.len()) || parts .iter() .any(|part| part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit())) { return Err(format!("release tag is not a numeric Apple bundle version: {tag}").into()); } Ok(version) } fn safe_tag(tag: &str) -> String { tag.chars() .map(|character| { if character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-') { character } else { '-' } }) .collect() } fn directory_size(path: &Path) -> io::Result { fs::read_dir(path)?.try_fold(0_u64, |total, entry| { let entry = entry?; let metadata = entry.metadata()?; let size = if metadata.is_dir() { directory_size(&entry.path())? } else { metadata.len() }; total .checked_add(size) .ok_or_else(|| io::Error::other("bundle size overflow")) }) } fn dmg_sectors(bundle_bytes: u64) -> Result> { let capacity = bundle_bytes .checked_add((bundle_bytes / 10).max(16 * MIB)) .ok_or("DMG capacity overflow")? .max(64 * MIB); Ok(u32::try_from(capacity.div_ceil(512))?) } #[cfg(test)] mod tests { use super::*; use std::io::{Read, Write}; use tempfile::tempdir; #[test] fn apple_bundle_version_comes_from_semver_tag() { assert_eq!(bundle_version("v1.2.3").unwrap(), "1.2.3"); assert_eq!(bundle_version("2.0.0-rc.1").unwrap(), "2.0.0"); assert!(bundle_version("nightly").is_err()); assert!(bundle_version("1.2.3.4").is_err()); } #[test] fn asset_tag_is_safe_for_paths_and_urls() { assert_eq!(safe_tag("release/1 + beta"), "release-1---beta"); } #[test] fn dmg_capacity_has_filesystem_headroom() { let sectors = dmg_sectors(100 * MIB).unwrap(); assert!(u64::from(sectors) * 512 >= 116 * MIB); assert_eq!(dmg_sectors(1).unwrap(), 131_072); } #[test] fn macos_cross_linker_alias_is_an_empty_static_archive() { let temp = tempdir().unwrap(); prepare_macos_linker_alias(temp.path()).unwrap(); assert_eq!( fs::read(temp.path().join("libclang_rt.osx.a")).unwrap(), b"!\n" ); } #[test] fn macos_x64_runtime_extracts_the_versioned_library_and_license() { let temp = tempdir().unwrap(); let runtime = temp.path().join("runtime"); let archive = Vec::new(); let encoder = GzEncoder::new(archive, Compression::default()); let mut archive = tar::Builder::new(encoder); for (name, contents) in [ ( "onnxruntime-osx-x86_64-1.23.2/lib/libonnxruntime.1.23.2.dylib", b"runtime".as_slice(), ), ( "onnxruntime-osx-x86_64-1.23.2/LICENSE", b"license".as_slice(), ), ( "onnxruntime-osx-x86_64-1.23.2/include/onnxruntime.h", b"header".as_slice(), ), ] { let mut header = tar::Header::new_gnu(); header.set_size(contents.len() as u64); header.set_mode(0o644); header.set_cksum(); archive.append_data(&mut header, name, contents).unwrap(); } let encoder = archive.into_inner().unwrap(); let archive = std::io::Cursor::new(encoder.finish().unwrap()); prepare_macos_x64_runtime_from_archive(archive, &runtime).unwrap(); assert_eq!( fs::read(runtime.join("libonnxruntime.1.23.2.dylib")).unwrap(), b"runtime" ); assert_eq!( fs::read(runtime.join("libonnxruntime.dylib")).unwrap(), b"runtime" ); assert_eq!( fs::read(runtime.join("ONNXRuntime-LICENSE.txt")).unwrap(), b"license" ); assert_eq!(fs::read_dir(runtime).unwrap().count(), 3); } #[test] fn macos_runtime_files_package_one_versioned_ort_library_and_its_license() { let temp = tempdir().unwrap(); let release = temp.path().join("release"); let rust_libs = temp.path().join("rust-libs"); fs::create_dir(&release).unwrap(); fs::create_dir(&rust_libs).unwrap(); for file in [ "libbds_core.dylib", "libbds_server.dylib", "libonnxruntime.dylib", "libonnxruntime.1.23.2.dylib", "ONNXRuntime-LICENSE.txt", ] { fs::write(release.join(file), file).unwrap(); } let files = runtime_files(Platform::MacOs, &release, &rust_libs).unwrap(); let names = files.into_iter().map(|(_, name)| name).collect::>(); assert!(names.contains(&"libonnxruntime.1.23.2.dylib".to_owned())); assert!(names.contains(&"ONNXRuntime-LICENSE.txt".to_owned())); assert!(!names.contains(&"libonnxruntime.dylib".to_owned())); } #[test] fn macos_license_is_a_non_macho_bundle_resource() { assert!(is_macos_bundle_resource("ONNXRuntime-LICENSE.txt")); assert!(!is_macos_macho("ONNXRuntime-LICENSE.txt")); assert!(is_macos_bundle_resource("libonnxruntime.1.23.2.dylib")); assert!(is_macos_macho("libonnxruntime.1.23.2.dylib")); assert!(!is_macos_bundle_resource("bds-ui")); assert!(is_macos_macho("bds-ui")); } #[test] fn windows_runtime_extracts_only_redistributable_files() { let temp = tempdir().unwrap(); let sdk = temp.path().join("sdk"); let runtime = temp.path().join("runtime"); fs::create_dir(&sdk).unwrap(); fs::create_dir(&runtime).unwrap(); fs::write(sdk.join("pathcch.lib"), b"sdk-import-library").unwrap(); let archive = std::io::Cursor::new(Vec::new()); let mut archive = ZipWriter::new(archive); let options = SimpleFileOptions::default(); for (name, contents) in [ ("bin/x64-win/DirectML.lib", b"import-library".as_slice()), ("bin/x64-win/DirectML.dll", b"runtime".as_slice()), ("LICENSE.txt", b"license".as_slice()), ( "bin/arm64-win/DirectML.dll", b"wrong-architecture".as_slice(), ), ] { archive.start_file(name, options).unwrap(); archive.write_all(contents).unwrap(); } let archive = archive.finish().unwrap(); prepare_windows_runtime_from_archive(archive, &runtime).unwrap(); prepare_windows_sdk_linker_aliases(&sdk, &runtime).unwrap(); assert_eq!( fs::read(runtime.join("DirectML.lib")).unwrap(), b"import-library" ); assert_eq!(fs::read(runtime.join("DirectML.dll")).unwrap(), b"runtime"); assert_eq!( fs::read(runtime.join("DirectML-LICENSE.txt")).unwrap(), b"license" ); assert_eq!( fs::read(runtime.join("PathCch.lib")).unwrap(), b"sdk-import-library" ); assert_eq!(fs::read_dir(runtime).unwrap().count(), 4); } #[test] fn windows_package_contains_the_app_without_dynamic_std() { let temp = tempdir().unwrap(); let release = temp.path().join("release"); let rust_libs = temp.path().join("rust-libs"); let dist = temp.path().join("dist"); fs::create_dir(&release).unwrap(); fs::create_dir(&rust_libs).unwrap(); for file in [ "bds-ui.exe", "bds-cli.exe", "bds-mcp.exe", "bds_core.dll", "bds_server.dll", "DirectML.dll", "DirectML-LICENSE.txt", ] { fs::write(release.join(file), format!("MZ-{file}")).unwrap(); } let assets = package("v1.2.3", &release, &rust_libs, "windows-x64", &dist).unwrap(); assert_eq!(assets.len(), 2); let desktop = dist.join("bds-desktop-v1.2.3-windows-x64.zip"); let mut archive = zip::ZipArchive::new(File::open(desktop).unwrap()).unwrap(); let mut contents = Vec::new(); archive .by_name("bds-desktop-v1.2.3-windows-x64/bds-ui.exe") .unwrap() .read_to_end(&mut contents) .unwrap(); assert_eq!(contents, b"MZ-bds-ui.exe"); assert!( archive .by_name("bds-desktop-v1.2.3-windows-x64/bds_core.dll") .is_ok() ); for runtime in ["DirectML.dll", "DirectML-LICENSE.txt"] { assert!( archive .by_name(&format!("bds-desktop-v1.2.3-windows-x64/{runtime}")) .is_ok(), "missing {runtime}" ); } } #[test] fn linux_package_contains_the_desktop_application() { let temp = tempdir().unwrap(); let release = temp.path().join("release"); let rust_libs = temp.path().join("rust-libs"); let dist = temp.path().join("dist"); fs::create_dir(&release).unwrap(); fs::create_dir(&rust_libs).unwrap(); for file in [ "bds-ui", "bds-cli", "bds-mcp", "libbds_core.so", "libbds_server.so", ] { fs::write(release.join(file), file).unwrap(); } let assets = package("v1.2.3", &release, &rust_libs, "linux-arm64", &dist).unwrap(); assert_eq!( assets, [ dist.join("bds-cli-v1.2.3-linux-arm64.tar.gz"), dist.join("bds-desktop-v1.2.3-linux-arm64.tar.gz"), ] ); let desktop = File::open(dist.join("bds-desktop-v1.2.3-linux-arm64.tar.gz")).unwrap(); let decoder = flate2::read::GzDecoder::new(desktop); let mut archive = tar::Archive::new(decoder); let names = archive .entries() .unwrap() .map(|entry| entry.unwrap().path().unwrap().into_owned()) .collect::>(); assert!( !names .iter() .any(|name| name.to_string_lossy().contains("onnxruntime")), "the statically linked Linux runtime must not require an external ORT library" ); assert!(names.iter().any(|name| name.ends_with("bds-ui"))); assert!(names.iter().any(|name| name.ends_with("libbds_core.so"))); } #[test] fn macho_runtime_install_names_are_rewritten_to_rpath() { let temp = tempdir().unwrap(); let macho = temp.path().join("libbds_core.dylib"); let old_name = b"/build/target/release/libbds_core.dylib\0"; let command_size = (24 + old_name.len()).div_ceil(8) * 8; let mut bytes = vec![0_u8; 32 + command_size]; bytes[0..4].copy_from_slice(&0xfeedfacf_u32.to_le_bytes()); bytes[16..20].copy_from_slice(&1_u32.to_le_bytes()); bytes[20..24].copy_from_slice(&(command_size as u32).to_le_bytes()); bytes[32..36].copy_from_slice(&LC_ID_DYLIB.to_le_bytes()); bytes[36..40].copy_from_slice(&(command_size as u32).to_le_bytes()); bytes[40..44].copy_from_slice(&24_u32.to_le_bytes()); bytes[56..56 + old_name.len()].copy_from_slice(old_name); fs::write(&macho, bytes).unwrap(); rewrite_macho_rpaths(&macho, &["libbds_core.dylib".to_owned()]).unwrap(); let rewritten = fs::read(macho).unwrap(); let name_end = rewritten[56..].iter().position(|byte| *byte == 0).unwrap(); assert_eq!(&rewritten[56..56 + name_end], b"@rpath/libbds_core.dylib"); } }