use std::{ env, error::Error, ffi::OsStr, fs, path::{Path, PathBuf}, process::Command, }; fn main() -> Result<(), Box> { let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); if env::args_os().nth(1).as_deref() == Some(OsStr::new("sign-app-for-dmg")) { return sign_app_for_dmg(&workspace); } run( Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) .current_dir(&workspace) .args([ "build", "--release", "-p", "bds-ui", "-p", "bds-cli", "-p", "bds-mcp", ]), "release build", )?; let target = env::var_os("CARGO_TARGET_DIR") .map(PathBuf::from) .unwrap_or_else(|| workspace.join("target")); let release = target.join("release"); let staging = release.join("bds-runtime"); if staging.exists() { fs::remove_dir_all(&staging)?; } fs::create_dir_all(&staging)?; let shared_libraries = shared_library_names().map(|name| release.join(name)); for library in &shared_libraries { copy_runtime(library, &staging)?; } let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let output = Command::new(rustc) .args(["--print", "target-libdir"]) .output()?; if !output.status.success() { return Err("rustc --print target-libdir failed".into()); } let rust_lib_dir = PathBuf::from(String::from_utf8(output.stdout)?.trim()); let mut std_libraries = fs::read_dir(&rust_lib_dir)? .filter_map(Result::ok) .map(|entry| entry.path()) .filter(|path| is_dynamic_std(path)) .collect::>(); std_libraries.sort(); if std_libraries.is_empty() { return Err(format!( "no dynamic Rust standard library found in {}", rust_lib_dir.display() ) .into()); } for library in &std_libraries { copy_runtime(library, &staging)?; } #[cfg(target_os = "macos")] rewrite_macos_install_names(&release, &staging, &shared_libraries, &std_libraries)?; Ok(()) } fn sign_app_for_dmg(workspace: &Path) -> Result<(), Box> { #[cfg(not(target_os = "macos"))] let _ = workspace; #[cfg(target_os = "macos")] if env::var_os("CARGO_PACKAGER_FORMAT").as_deref() == Some(OsStr::new("dmg")) { let target = env::var_os("CARGO_TARGET_DIR") .map(PathBuf::from) .unwrap_or_else(|| workspace.join("target")); run( Command::new("codesign") .args(["--force", "--sign", "-"]) .arg(target.join("release/Blogging Desktop Server.app")), "macOS app ad-hoc signing", )?; } Ok(()) } fn run(command: &mut Command, description: &str) -> Result<(), Box> { if command.status()?.success() { Ok(()) } else { Err(format!("{description} failed").into()) } } fn copy_runtime(source: &Path, staging: &Path) -> Result<(), Box> { let name = source .file_name() .ok_or_else(|| format!("runtime path has no filename: {}", source.display()))?; let destination = staging.join(name); fs::copy(source, &destination)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&destination, fs::Permissions::from_mode(0o755))?; } #[cfg(not(unix))] { let mut permissions = fs::metadata(&destination)?.permissions(); permissions.set_readonly(false); fs::set_permissions(&destination, permissions)?; } Ok(()) } fn shared_library_names() -> [&'static str; 2] { if cfg!(target_os = "windows") { ["bds_core.dll", "bds_server.dll"] } else if cfg!(target_os = "macos") { ["libbds_core.dylib", "libbds_server.dylib"] } else { ["libbds_core.so", "libbds_server.so"] } } fn is_dynamic_std(path: &Path) -> bool { let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default(); if cfg!(target_os = "windows") { name.starts_with("std-") && name.ends_with(".dll") } else if cfg!(target_os = "macos") { name.starts_with("libstd-") && name.ends_with(".dylib") } else { name.starts_with("libstd-") && name.ends_with(".so") } } #[cfg(target_os = "macos")] fn rewrite_macos_install_names( release: &Path, staging: &Path, shared_libraries: &[PathBuf], std_libraries: &[PathBuf], ) -> Result<(), Box> { let mut replacements = Vec::new(); for source in shared_libraries.iter().chain(std_libraries) { let old_name = macho_install_name(source)?; let file_name = source .file_name() .and_then(OsStr::to_str) .ok_or_else(|| format!("invalid dylib filename: {}", source.display()))?; replacements.push((old_name, format!("@rpath/{file_name}"))); } let mut targets = ["bds-ui", "bds-cli", "bds-mcp"] .map(|name| release.join(name)) .into_iter() .collect::>(); targets.extend( shared_libraries .iter() .filter_map(|source| source.file_name().map(|name| staging.join(name))), ); targets.extend( std_libraries .iter() .filter_map(|source| source.file_name().map(|name| staging.join(name))), ); for target in &targets { for (old_name, new_name) in &replacements { run( Command::new("install_name_tool") .args(["-change", old_name, new_name]) .arg(target), "Mach-O dependency rewrite", )?; } } for (_, new_name) in &replacements { let file_name = new_name.trim_start_matches("@rpath/"); run( Command::new("install_name_tool") .args(["-id", new_name]) .arg(staging.join(file_name)), "Mach-O install-name rewrite", )?; } for target in targets { run( Command::new("codesign") .args(["--force", "--sign", "-"]) .arg(target), "Mach-O ad-hoc signing", )?; } Ok(()) } #[cfg(target_os = "macos")] fn macho_install_name(library: &Path) -> Result> { let output = Command::new("otool").arg("-D").arg(library).output()?; if !output.status.success() { return Err(format!("otool -D failed for {}", library.display()).into()); } String::from_utf8(output.stdout)? .lines() .nth(1) .map(str::to_owned) .ok_or_else(|| format!("no Mach-O install name in {}", library.display()).into()) } #[cfg(test)] mod tests { use super::*; #[test] fn recognizes_only_the_platform_dynamic_standard_library() { let valid = if cfg!(target_os = "windows") { "std-123.dll" } else if cfg!(target_os = "macos") { "libstd-123.dylib" } else { "libstd-123.so" }; assert!(is_dynamic_std(Path::new(valid))); assert!(!is_dynamic_std(Path::new("libstd-123.rlib"))); for library in shared_library_names() { assert!(!is_dynamic_std(Path::new(library))); } } }