use anyhow::{Context as _, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub trait Platform: Send + Sync {
fn name(&self) -> &'static str;
fn ensure_binary_loadable(&self, path: &Path) -> Result<()>;
fn package_debug_bundle(&self, binary: &Path, staging_dir: &Path) -> Result<Option<PathBuf>>;
}
pub fn current() -> Box<dyn Platform> {
#[cfg(target_os = "macos")]
{
Box::new(MacOsPlatform)
}
#[cfg(target_os = "linux")]
{
Box::new(LinuxPlatform)
}
#[cfg(target_os = "windows")]
{
Box::new(WindowsPlatform)
}
}
#[allow(dead_code)]
pub struct MacOsPlatform;
impl Platform for MacOsPlatform {
fn name(&self) -> &'static str {
"macos"
}
fn ensure_binary_loadable(&self, path: &Path) -> Result<()> {
if std::env::consts::ARCH != "aarch64" {
return Ok(());
}
if std::env::consts::OS != "macos" {
return Ok(());
}
let verify = match Command::new("codesign")
.args(["--verify", "--strict"])
.arg(path)
.status()
{
Ok(status) => status,
Err(err) => {
tracing::warn!(
"unable to run codesign --verify for {}: {err}",
path.display()
);
return Ok(());
}
};
if verify.success() {
tracing::debug!(
"ad-hoc signature already valid for {}, skipping re-sign",
path.display()
);
return Ok(());
}
tracing::debug!(
"ad-hoc signature missing or invalid for {}, re-applying",
path.display()
);
let status = match Command::new("codesign")
.args(["--sign", "-", "--force"])
.arg(path)
.status()
{
Ok(status) => status,
Err(err) => {
tracing::warn!(
"unable to run codesign --sign for {}: {err}",
path.display()
);
return Ok(());
}
};
if !status.success() {
tracing::warn!("ad-hoc codesign failed for {}", path.display());
}
Ok(())
}
fn package_debug_bundle(&self, binary: &Path, staging_dir: &Path) -> Result<Option<PathBuf>> {
if std::env::consts::OS != "macos" {
return Ok(None);
}
let Some(file_name) = binary.file_name().and_then(|n| n.to_str()) else {
tracing::warn!(
"not packaging a debug bundle: binary has no usable file name: {}",
binary.display()
);
return Ok(None);
};
let bundle_dir = binary.with_file_name(format!("{file_name}.dSYM"));
let status = match Command::new("dsymutil")
.arg(binary)
.arg("-o")
.arg(&bundle_dir)
.status()
{
Ok(status) => status,
Err(err) => {
tracing::warn!("unable to run dsymutil for {}: {err}", binary.display());
return Ok(None);
}
};
if !status.success() {
tracing::warn!("dsymutil failed for {}", binary.display());
return Ok(None);
}
let tar_path = staging_dir.join(format!("{file_name}.dsym.tar"));
match build_deterministic_tar(&bundle_dir, &tar_path) {
Ok(()) => Ok(Some(tar_path)),
Err(err) => {
tracing::warn!(
"failed to package debug bundle for {}: {err:#}",
binary.display()
);
let _ = std::fs::remove_file(&tar_path);
Ok(None)
}
}
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn build_deterministic_tar(bundle_dir: &Path, tar_path: &Path) -> Result<()> {
let mut files = Vec::new();
collect_files_recursively(bundle_dir, bundle_dir, &mut files)?;
files.sort();
let out = std::fs::File::create(tar_path)
.with_context(|| format!("creating {}", tar_path.display()))?;
let mut builder = tar::Builder::new(out);
for rel in files {
let abs = bundle_dir.join(&rel);
let mut file =
std::fs::File::open(&abs).with_context(|| format!("opening {}", abs.display()))?;
let size = file
.metadata()
.with_context(|| format!("stat {}", abs.display()))?
.len();
let mut header = tar::Header::new_gnu();
header.set_size(size);
header.set_mode(0o644);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
header.set_entry_type(tar::EntryType::Regular);
builder
.append_data(&mut header, &rel, &mut file)
.with_context(|| format!("appending {}", rel.display()))?;
}
builder.finish().context("finishing debug bundle tar")?;
Ok(())
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn collect_files_recursively(root: &Path, dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
for entry in std::fs::read_dir(dir).with_context(|| format!("reading dir {}", dir.display()))? {
let entry = entry.with_context(|| format!("reading dir entry in {}", dir.display()))?;
let path = entry.path();
let file_type = entry
.file_type()
.with_context(|| format!("stat {}", path.display()))?;
if file_type.is_dir() {
collect_files_recursively(root, &path, files)?;
} else if file_type.is_file() {
let rel = path
.strip_prefix(root)
.with_context(|| format!("relativizing {}", path.display()))?
.to_path_buf();
files.push(rel);
}
}
Ok(())
}
#[allow(dead_code)]
pub struct LinuxPlatform;
impl Platform for LinuxPlatform {
fn name(&self) -> &'static str {
"linux"
}
fn ensure_binary_loadable(&self, _path: &Path) -> Result<()> {
Ok(())
}
fn package_debug_bundle(&self, _binary: &Path, _staging_dir: &Path) -> Result<Option<PathBuf>> {
Ok(None)
}
}
#[allow(dead_code)]
pub struct WindowsPlatform;
impl Platform for WindowsPlatform {
fn name(&self) -> &'static str {
"windows"
}
fn ensure_binary_loadable(&self, _path: &Path) -> Result<()> {
Ok(())
}
fn package_debug_bundle(&self, _binary: &Path, _staging_dir: &Path) -> Result<Option<PathBuf>> {
Ok(None)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingPlatform {
ensure_binary_loadable_calls: AtomicUsize,
package_debug_bundle_calls: AtomicUsize,
}
impl CountingPlatform {
pub fn new() -> Self {
Self {
ensure_binary_loadable_calls: AtomicUsize::new(0),
package_debug_bundle_calls: AtomicUsize::new(0),
}
}
pub fn ensure_calls(&self) -> usize {
self.ensure_binary_loadable_calls.load(Ordering::Relaxed)
}
pub fn package_calls(&self) -> usize {
self.package_debug_bundle_calls.load(Ordering::Relaxed)
}
}
impl Platform for CountingPlatform {
fn name(&self) -> &'static str {
"counting"
}
fn ensure_binary_loadable(&self, _path: &Path) -> Result<()> {
self.ensure_binary_loadable_calls
.fetch_add(1, Ordering::Relaxed);
Ok(())
}
fn package_debug_bundle(
&self,
_binary: &Path,
_staging_dir: &Path,
) -> Result<Option<PathBuf>> {
self.package_debug_bundle_calls
.fetch_add(1, Ordering::Relaxed);
Ok(None)
}
}
#[test]
fn current_returns_a_platform_named_after_the_host() {
let platform = current();
let expected = if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
panic!("unsupported host OS in test")
};
assert_eq!(platform.name(), expected);
}
#[test]
fn linux_ensure_binary_loadable_is_noop_for_any_path() {
let platform = LinuxPlatform;
platform
.ensure_binary_loadable(Path::new("/no/such/file"))
.unwrap();
}
#[test]
fn windows_ensure_binary_loadable_is_noop_for_any_path() {
let platform = WindowsPlatform;
platform
.ensure_binary_loadable(Path::new("/no/such/file"))
.unwrap();
}
#[test]
fn macos_ensure_binary_loadable_does_not_propagate_errors() {
let platform = MacOsPlatform;
platform
.ensure_binary_loadable(Path::new("/no/such/file"))
.unwrap();
}
#[test]
fn counting_platform_records_ensure_calls() {
let platform = CountingPlatform::new();
assert_eq!(platform.ensure_calls(), 0);
platform.ensure_binary_loadable(Path::new("/x")).unwrap();
platform.ensure_binary_loadable(Path::new("/y")).unwrap();
assert_eq!(platform.ensure_calls(), 2);
}
#[test]
fn linux_package_debug_bundle_is_none_for_any_path() {
let platform = LinuxPlatform;
let dir = tempfile::tempdir().unwrap();
assert!(
platform
.package_debug_bundle(Path::new("/no/such/binary"), dir.path())
.unwrap()
.is_none()
);
}
#[test]
fn windows_package_debug_bundle_is_none_for_any_path() {
let platform = WindowsPlatform;
let dir = tempfile::tempdir().unwrap();
assert!(
platform
.package_debug_bundle(Path::new("/no/such/binary"), dir.path())
.unwrap()
.is_none()
);
}
#[test]
fn macos_package_debug_bundle_does_not_propagate_errors() {
let platform = MacOsPlatform;
let dir = tempfile::tempdir().unwrap();
assert!(
platform
.package_debug_bundle(Path::new("/no/such/binary"), dir.path())
.unwrap()
.is_none()
);
}
#[test]
fn counting_platform_records_package_calls() {
let platform = CountingPlatform::new();
assert_eq!(platform.package_calls(), 0);
let dir = tempfile::tempdir().unwrap();
assert!(
platform
.package_debug_bundle(Path::new("/x"), dir.path())
.unwrap()
.is_none()
);
assert_eq!(platform.package_calls(), 1);
}
#[test]
fn deterministic_tar_captures_the_whole_tree_reproducibly() {
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("fake.dSYM");
std::fs::create_dir_all(bundle.join("Contents/Resources/DWARF")).unwrap();
std::fs::write(bundle.join("Contents/Info.plist"), b"plist").unwrap();
std::fs::write(bundle.join("Contents/Resources/DWARF/fake"), b"dwarf").unwrap();
let tar_a = dir.path().join("a.tar");
let tar_b = dir.path().join("b.tar");
build_deterministic_tar(&bundle, &tar_a).unwrap();
build_deterministic_tar(&bundle, &tar_b).unwrap();
let bytes_a = std::fs::read(&tar_a).unwrap();
assert_eq!(
bytes_a,
std::fs::read(&tar_b).unwrap(),
"two packagings of the same bundle must be byte-identical"
);
let mut archive = tar::Archive::new(std::io::Cursor::new(bytes_a));
let entries: Vec<String> = archive
.entries()
.unwrap()
.map(|e| e.unwrap().path().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(
entries,
vec![
"Contents/Info.plist".to_string(),
"Contents/Resources/DWARF/fake".to_string(),
],
"every file, relative to the bundle root, in sorted order"
);
}
fn compile_debug_c_binary(dir: &Path) -> Option<std::path::PathBuf> {
if std::env::consts::OS != "macos" {
return None;
}
let source = dir.join("hello.c");
std::fs::write(
&source,
"#include <stdio.h>\nint main(void) { printf(\"hi\\n\"); return 0; }\n",
)
.unwrap();
let object = dir.join("hello.o");
let binary = dir.join("hello-bin");
let compile = Command::new("cc")
.args(["-g", "-c"])
.arg(&source)
.arg("-o")
.arg(&object)
.status()
.ok()?;
if !compile.success() {
return None;
}
let link = Command::new("cc")
.arg(&object)
.arg("-o")
.arg(&binary)
.status()
.ok()?;
link.success().then_some(binary)
}
#[test]
fn macos_package_debug_bundle_produces_tar_with_dwarf_and_adjacent_bundle() {
let dir = tempfile::tempdir().unwrap();
let Some(binary) = compile_debug_c_binary(dir.path()) else {
return;
};
let staging = tempfile::tempdir().unwrap();
let tar_path = MacOsPlatform
.package_debug_bundle(&binary, staging.path())
.unwrap()
.expect("macOS host with dsymutil must produce a bundle tar");
assert_eq!(
tar_path.file_name().unwrap().to_str().unwrap(),
"hello-bin.dsym.tar"
);
let bundle = dir.path().join("hello-bin.dSYM");
assert!(
bundle.join("Contents/Resources/DWARF/hello-bin").is_file(),
"dSYM bundle must remain adjacent to the binary"
);
let mut archive = tar::Archive::new(std::fs::File::open(&tar_path).unwrap());
let names: Vec<String> = archive
.entries()
.unwrap()
.map(|e| e.unwrap().path().unwrap().to_string_lossy().into_owned())
.collect();
assert!(
names
.iter()
.any(|n| n == "Contents/Resources/DWARF/hello-bin"),
"tar must contain the DWARF payload, got: {names:?}"
);
}
#[test]
fn macos_package_debug_bundle_tar_bytes_are_reproducible() {
let dir = tempfile::tempdir().unwrap();
let Some(binary) = compile_debug_c_binary(dir.path()) else {
return;
};
let staging_a = tempfile::tempdir().unwrap();
let staging_b = tempfile::tempdir().unwrap();
let tar_a = MacOsPlatform
.package_debug_bundle(&binary, staging_a.path())
.unwrap()
.expect("first packaging must succeed");
let tar_b = MacOsPlatform
.package_debug_bundle(&binary, staging_b.path())
.unwrap()
.expect("second packaging must succeed");
assert_eq!(
std::fs::read(&tar_a).unwrap(),
std::fs::read(&tar_b).unwrap(),
"debug bundle tar bytes must be reproducible for store dedup"
);
}
}