use std::{fs, path::PathBuf, process::Command};
use anyhow::{Context, Result};
use tracing::{debug, error, warn};
use crate::{Binary, CargoEnv, Platform};
pub fn write_bundle(
binary: &Binary,
cargo_env: &CargoEnv,
) -> Result<(PathBuf, PathBuf, Option<String>)> {
if !binary.gui_like {
return Ok((binary.path.to_path_buf(), binary.path.to_path_buf(), None));
}
let bundle_path = binary.path.with_added_extension("app");
if bundle_path.exists() {
fs::remove_dir_all(&bundle_path).context("failed to remove previous app bundle")?;
}
let exe_dir = if binary.platform() == Platform::MACOS {
bundle_path.join("Contents").join("MacOS")
} else {
bundle_path.clone()
};
debug!("mkdir -p {exe_dir:?}");
fs::create_dir_all(&exe_dir).context("failed creating bundle directories")?;
let exe_path = exe_dir.join(binary.path.file_name().expect("binary cannot be root"));
debug!("cp -c {exe_dir:?} {exe_path:?}");
fs::copy(&binary.path, &exe_path).context("failed copying executable")?;
let info_plist = binary
.info_plist_data
.clone()
.unwrap_or_else(|| default_info_plist(binary, cargo_env).into_bytes());
let info_plist_path = if binary.platform() == Platform::MACOS {
bundle_path.join("Contents").join("Info.plist")
} else {
bundle_path.join("Info.plist")
};
debug!("createInfoPlist {info_plist_path:?}");
fs::write(info_plist_path, &info_plist).context("failed writing Info.plist")?;
let plist: plist::Value = plist::from_bytes(&info_plist).context("invalid Info.plist")?;
let bundle_identifier = plist
.as_dictionary()
.context("invalid")?
.get("CFBundleIdentifier")
.and_then(|v| v.as_string())
.map(|s| s.to_string());
debug!("xattr -crs {bundle_path:?}");
match Command::new("xattr").arg("-crs").arg(&bundle_path).status() {
Ok(status) => {
if !status.success() {
warn!(?status, "failed removing excess attributes");
}
}
Err(err) => {
warn!(%err, "failed removing excess attributes");
}
}
Ok((bundle_path, exe_path, bundle_identifier))
}
fn default_info_plist(binary: &Binary, cargo_env: &CargoEnv) -> String {
let name = binary
.path
.file_name()
.unwrap()
.to_str()
.expect("non-UTF8 in file name");
let identifier = format!("{}.{name}", cargo_env.pkg_name);
let display_name = binary
.name
.as_deref()
.map(|bytes| String::from_utf8_lossy(bytes))
.unwrap_or_else(|| name.into());
if binary.platform() == Platform::MACOS {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundleDisplayName</key>
<string>{display_name}</string>
<key>CFBundleIdentifier</key>
<string>{identifier}</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>CFBundleShortVersionString</key>
<string>{short_version_string}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>{name}</string>
<key>LSMinimumSystemVersion</key>
<string>{minimum_system_version}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
</dict>
</plist>
"#,
version = "1.0",
short_version_string = cargo_env.pkg_version,
minimum_system_version = binary.minos(),
)
} else {
let mut family_string = String::new();
for family in device_families(binary.platform()) {
family_string.push_str(&format!(" <integer>{family}</integer>\n"));
}
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>{name}</string>
<key>CFBundleDisplayName</key>
<string>{display_name}</string>
<key>CFBundleIdentifier</key>
<string>{identifier}</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>CFBundleShortVersionString</key>
<string>{short_version_string}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>{name}</string>
<key>MinimumOSVersion</key>
<string>{minimum_os_version}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>{supported_platform}</string>
</array>
<key>UIDeviceFamily</key>
<array>
{family_string} </array>
</dict>
</plist>
"#,
version = "1.0",
short_version_string = cargo_env.pkg_version,
minimum_os_version = binary.minos(),
supported_platform = sdk_name(binary.platform()),
)
}
}
fn sdk_name(platform: Platform) -> &'static str {
match platform {
Platform::MACOS | Platform::MACCATALYST => "MacOSX",
Platform::IOS => "iPhoneOS",
Platform::IOSSIMULATOR => "iPhoneSimulator",
Platform::TVOS => "AppleTVOS",
Platform::TVOSSIMULATOR => "AppleTVSimulator",
Platform::WATCHOS => "WatchOS",
Platform::WATCHOSSIMULATOR => "WatchSimulator",
Platform::VISIONOS => "XROS",
Platform::VISIONOSSIMULATOR => "XRSimulator",
Platform::DRIVERKIT => "DriverKit",
platform => {
error!(?platform, "unknown platform");
"Unknown"
}
}
}
fn device_families(platform: Platform) -> &'static [u32] {
match platform {
Platform::IOS | Platform::IOSSIMULATOR => &[1, 2],
Platform::TVOS | Platform::TVOSSIMULATOR => &[3],
Platform::WATCHOS | Platform::WATCHOSSIMULATOR => &[4],
Platform::VISIONOS | Platform::VISIONOSSIMULATOR => &[7],
_ => &[],
}
}