use std::path::{Path, PathBuf};
use crate::error::ShunError;
use crate::targets::install::{InstallContext, Registration};
#[cfg(target_os = "macos")]
use crate::targets::plist::render_info_plist;
#[derive(Debug, Clone, Copy, Default)]
pub struct MacOSRegistration;
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn app_bundle(ctx: &InstallContext) -> Option<PathBuf> {
let main_exe = ctx.main_exe.as_ref()?;
let mut ancestor = ctx.install_dir.join(main_exe);
while ancestor.pop() {
if ancestor == ctx.install_dir {
return None;
}
if ancestor
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(".app"))
{
return Some(ancestor);
}
}
None
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
const LSREGISTER: &str = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
#[cfg(target_os = "macos")]
impl Registration for MacOSRegistration {
fn register(&self, ctx: &InstallContext) -> Result<(), ShunError> {
use std::os::unix::fs::PermissionsExt;
if ctx.scope == crate::targets::install::InstallScope::Machine {
return Err(ShunError::Unsupported(
"machine install scope (windows only)",
));
}
let Some(main_exe) = &ctx.main_exe else {
return Ok(());
};
std::fs::set_permissions(
ctx.install_dir.join(main_exe),
std::fs::Permissions::from_mode(0o755),
)?;
let Some(bundle) = app_bundle(ctx) else {
return Ok(()); };
let plist = bundle.join("Contents").join("Info.plist");
if !plist.exists() {
std::fs::create_dir_all(bundle.join("Contents"))?;
let executable = main_exe
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
std::fs::write(
&plist,
render_info_plist(
&ctx.product,
&ctx.version,
ctx.publisher.as_deref(),
&executable,
&ctx.deep_links,
),
)?;
}
let _ = std::process::Command::new("/usr/bin/xattr")
.args(["-rd", "com.apple.quarantine"])
.arg(&ctx.install_dir)
.status();
let _ = std::process::Command::new(LSREGISTER)
.arg("-f")
.arg(&bundle)
.status();
Ok(())
}
fn unregister(&self, ctx: &InstallContext) -> Result<(), ShunError> {
if let Some(bundle) = app_bundle(ctx) {
let _ = std::process::Command::new(LSREGISTER)
.arg("-u")
.arg(&bundle)
.status();
}
Ok(())
}
}
#[cfg(not(target_os = "macos"))]
impl Registration for MacOSRegistration {
fn register(&self, _ctx: &InstallContext) -> Result<(), ShunError> {
Err(ShunError::Unsupported(
"macos registration backend (compile on macos)",
))
}
fn unregister(&self, _ctx: &InstallContext) -> Result<(), ShunError> {
Err(ShunError::Unsupported(
"macos registration backend (compile on macos)",
))
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn bundle_for(install_dir: &Path, main_exe: &Path) -> Option<PathBuf> {
app_bundle(&InstallContext {
product: String::new(),
version: String::new(),
publisher: None,
install_dir: install_dir.to_path_buf(),
main_exe: Some(main_exe.to_path_buf()),
portable: false,
portable_marker: crate::targets::install::PORTABLE_MARKER.to_string(),
scope: crate::targets::install::InstallScope::User,
desktop_shortcut: false,
start_menu_shortcut: false,
launch_after_install: false,
verbs: Vec::new(),
deep_links: Vec::new(),
aumid: None,
icon: None,
estimated_size_kb: 0,
language: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn app_bundle_is_found_through_ancestors() {
let install = Path::new("/Applications");
assert_eq!(
bundle_for(install, Path::new("ShunDemo.app/Contents/MacOS/shun-demo")),
Some(install.join("ShunDemo.app"))
);
assert_eq!(
bundle_for(
install,
Path::new("ShunDemo.app/Contents/Frameworks/Helper.app/Contents/MacOS/helper")
),
Some(install.join("ShunDemo.app/Contents/Frameworks/Helper.app"))
);
assert_eq!(bundle_for(install, Path::new("bin/app")), None);
}
}