#[cfg(target_os = "macos")]
use std::ffi::OsStr;
#[cfg(target_os = "macos")]
use std::fs;
#[cfg(target_os = "macos")]
use std::io::Read;
#[cfg(target_os = "macos")]
use std::path::{Path, PathBuf};
#[cfg(target_os = "macos")]
use std::process::Command;
#[cfg(target_os = "macos")]
use super::{run_codesign, tag_fail, tmp_manifests};
#[cfg(target_os = "macos")]
pub(crate) fn is_production_identity(identity: &str) -> bool {
identity != "-"
}
#[cfg(target_os = "macos")]
pub(crate) fn write_entitlements_plist() -> PathBuf {
let path = tmp_manifests().join("entitlements.plist");
let content = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>"#;
let _ = fs::write(&path, content);
path
}
#[cfg_attr(
not(target_os = "macos"),
allow(unused_variables, clippy::unnecessary_wraps)
)]
pub(crate) fn codesign_bundle(bundle: &str, identity: &str, use_sudo: bool) -> crate::Res {
#[cfg(target_os = "macos")]
{
let production = is_production_identity(identity);
let entitlements = write_entitlements_plist();
let bundle_path = Path::new(bundle);
let sign_one = |target: &OsStr| -> crate::Res {
let mut args: Vec<&OsStr> = vec![
OsStr::new("--force"),
OsStr::new("--sign"),
OsStr::new(identity),
];
if production {
args.extend_from_slice(&[
OsStr::new("--options"),
OsStr::new("runtime"),
OsStr::new("--timestamp"),
OsStr::new("--entitlements"),
entitlements.as_os_str(),
]);
}
args.push(target);
run_codesign(&args, use_sudo)
};
if bundle_path.is_dir() {
let mach_os = enumerate_mach_os(bundle_path);
for mach_o in &mach_os {
sign_one(mach_o.as_os_str())?;
}
}
sign_one(OsStr::new(bundle))?;
if production {
run_codesign(
&[
OsStr::new("--verify"),
OsStr::new("--strict"),
OsStr::new(bundle),
],
use_sudo,
)?;
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn is_mach_o_file(path: &Path) -> bool {
let Ok(mut f) = fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 4];
if f.read_exact(&mut buf).is_err() {
return false;
}
let magic_be = u32::from_be_bytes(buf);
matches!(
magic_be,
0xFEED_FACE | 0xFEED_FACF | 0xCEFA_EDFE | 0xCFFA_EDFE | 0xCAFE_BABE | 0xBEBA_FECA )
}
#[cfg(target_os = "macos")]
fn enumerate_mach_os(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
walk_mach_os(dir, &mut out);
out
}
#[cfg(target_os = "macos")]
fn walk_mach_os(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = entry.metadata() else {
continue;
};
if metadata.is_dir() {
walk_mach_os(&path, out);
} else if metadata.is_file() && is_mach_o_file(&path) {
out.push(path);
}
}
}
#[cfg(target_os = "macos")]
pub(crate) fn verify_signed_for_notarization(path: &Path, identity: &str) -> crate::Res {
if !is_production_identity(identity) {
return Ok(());
}
let mach_os = enumerate_mach_os(path);
if mach_os.is_empty() {
return Ok(());
}
let mut failures: Vec<(PathBuf, Vec<String>)> = Vec::new();
for mach_o in &mach_os {
let issues = check_mach_o_signing(mach_o)?;
if !issues.is_empty() {
failures.push((mach_o.clone(), issues));
}
}
if failures.is_empty() {
return Ok(());
}
eprintln!();
eprintln!(
"{} Notarization-readiness check failed for {} Mach-O(s) under {}:",
tag_fail(),
failures.len(),
path.display()
);
for (path, issues) in &failures {
eprintln!(" {}", path.display());
for issue in issues {
eprintln!(" - {issue}");
}
}
eprintln!();
eprintln!(
"These issues mirror Apple's notarization-server checks. \
Submitting now would fail the same way, with a ~6-minute \
round-trip per attempt."
);
Err("notarization-readiness check failed".into())
}
#[cfg(target_os = "macos")]
fn check_mach_o_signing(path: &Path) -> Result<Vec<String>, crate::CargoTruceError> {
let path_str = path.to_str().ok_or("Mach-O path is not UTF-8")?;
let output = Command::new("codesign")
.args(["-d", "-vvvv", path_str])
.output()?;
let report = String::from_utf8_lossy(&output.stderr);
let mut issues = Vec::new();
if report.contains("code object is not signed at all")
|| report.contains("is not signed at all")
{
issues.push("not signed".to_string());
return Ok(issues);
}
if !report.contains("Authority=Developer ID Application:") {
if report.contains("Signature=adhoc") {
issues.push("ad-hoc signature (not a Developer ID cert)".to_string());
} else {
issues.push("not signed with a Developer ID Application certificate".to_string());
}
}
let has_timestamp = report
.lines()
.any(|l| l.starts_with("Timestamp=") && !l.contains("Timestamp=none"));
if !has_timestamp {
issues.push("missing secure timestamp (--timestamp)".to_string());
}
if !report.contains("(runtime)") {
issues.push("hardened runtime not enabled (--options runtime)".to_string());
}
Ok(issues)
}
#[cfg(target_os = "macos")]
pub(crate) fn locate_wraptool_macos() -> Option<PathBuf> {
if let Ok(p) = which_unix("wraptool") {
return Some(p);
}
for canonical in [
"/Applications/PACEAntiPiracy/Eden/Fusion/Current/bin/wraptool",
"/Applications/PACEAntiPiracy/Eden/Fusion/Versions/5/bin/wraptool",
] {
let p = PathBuf::from(canonical);
if p.exists() {
return Some(p);
}
}
None
}
#[cfg(target_os = "macos")]
pub(crate) fn which_unix(name: &str) -> std::result::Result<PathBuf, std::io::Error> {
let path = std::env::var_os("PATH")
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "PATH not set"))?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
name.to_string(),
))
}
#[cfg(target_os = "macos")]
pub(crate) fn pace_sign_aax_macos(bundle: &Path) -> crate::Res {
let Some(wraptool) = locate_wraptool_macos() else {
eprintln!(
" wraptool not found - AAX bundle is unsigned for PACE. \
Pro Tools Developer will load it; retail Pro Tools won't."
);
return Ok(());
};
let Ok(account) = std::env::var("PACE_ACCOUNT") else {
eprintln!(" PACE_ACCOUNT not set - skipping PACE signing.");
return Ok(());
};
let Ok(signid) = std::env::var("PACE_SIGN_ID") else {
eprintln!(" PACE_SIGN_ID not set - skipping PACE signing.");
return Ok(());
};
eprintln!(" wraptool: PACE-signing {}", bundle.display());
let bundle_str = bundle
.to_str()
.ok_or("AAX bundle path is not valid UTF-8")?;
let status = Command::new(&wraptool)
.args([
"sign",
"--account",
&account,
"--signid",
&signid,
"--allowsigningservice",
"--dsigharden",
"--dsig1-compat",
"off",
"--in",
bundle_str,
"--out",
bundle_str,
])
.status()?;
if !status.success() {
return Err("wraptool failed".into());
}
Ok(())
}