use crate::makepad_shell::{shell_env_cap, write_text}; use crate::utils::{ get_build_crate_from_args, get_crate_dir, get_package_binary_name, get_profile_from_args, get_target_from_args, resolve_app_icon_env, AppIconEnv, APP_ICON_ENV_VARS, APP_ICON_IDX_1024, APP_ICON_IDX_512, }; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Default, Clone)] struct MacosSignOptions { enabled: bool, cert: Option, entitlements: Option, debugger: bool, get_task_allow: bool, runtime: bool, } fn is_windows_target(args: &[String]) -> bool { get_target_from_args(args) .map(|t| t.contains("windows")) .unwrap_or(cfg!(target_os = "windows")) } fn is_macos_target(args: &[String]) -> bool { get_target_from_args(args) .map(|t| t.contains("apple-darwin")) .unwrap_or(cfg!(target_os = "macos")) } fn is_linux_target(args: &[String]) -> bool { get_target_from_args(args) .map(|t| t.contains("linux")) .unwrap_or(cfg!(target_os = "linux")) } fn binary_path(args: &[String], binary_name: &str) -> PathBuf { let profile = get_profile_from_args(args); let mut path = cargo_target_dir(); if let Some(target) = get_target_from_args(args) { path.push(target); } path.push(profile); path.push(binary_name); if is_windows_target(args) { path.set_extension("exe"); } path } fn cargo_target_dir() -> PathBuf { if let Some(target_dir) = std::env::var_os("CARGO_TARGET_DIR") { let target_dir = PathBuf::from(target_dir); if target_dir.is_absolute() { target_dir } else { std::env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join(target_dir) } } else { PathBuf::from("target") } } /// The executable's icon resource, made in-process (no rc.exe/windres) from the /// same source as the macOS bundle in `write_macos_app_bundle`: /// `resources/icon.icns`, else the largest `icon_*.png`. fn write_windows_icon_resource( args: &[String], icon_env: &AppIconEnv, build_crate: &str, ) -> Result, String> { use makepad_win_resource::{app_link_input, LinkFormat}; let target = get_target_from_args(args).unwrap_or_else(|| { format!("{}-pc-windows-msvc", std::env::consts::ARCH) }); let env = if target.ends_with("-gnu") || target.ends_with("-gnullvm") { "gnu" } else { "msvc" }; let arch = target.split('-').next().unwrap_or_default(); let Some(format) = LinkFormat::for_target("windows", env, arch)? else { return Ok(None); }; let icns = get_crate_dir(build_crate)?.join("resources/icon.icns"); let source = if icns.is_file() { icns } else { PathBuf::from(&icon_env[APP_ICON_IDX_1024]) }; let bytes = fs::read(&source).map_err(|e| format!("failed to read {:?}: {e}", source))?; let input = app_link_input(format, Some(&bytes), None) .map_err(|e| format!("Windows icon from {:?}: {e}", source))?; let out_dir = PathBuf::from("target/makepad-desktop/windows-res").join(build_crate); fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create {:?}: {e}", out_dir))?; let path = out_dir.join(format!("app_icon.{}", format.extension())); fs::write(&path, input).map_err(|e| format!("failed to write {:?}: {e}", path))?; Ok(Some(path)) } fn add_windows_icon_link_arg( cmd: &mut Command, args: &[String], icon_env: &AppIconEnv, build_crate: &str, ) -> Result<(), String> { if !is_windows_target(args) { return Ok(()); } let Some(res) = write_windows_icon_resource(args, icon_env, build_crate)? else { return Ok(()); }; let res_abs = res.canonicalize().unwrap_or(res); let res_arg = format!("-C link-arg={}", res_abs.to_string_lossy()); let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); if !rustflags.is_empty() { rustflags.push(' '); } rustflags.push_str(&res_arg); cmd.env("RUSTFLAGS", rustflags); Ok(()) } fn write_macos_app_bundle( args: &[String], build_crate: &str, icon_env: &AppIconEnv, ) -> Result<(), String> { if !is_macos_target(args) { return Ok(()); } let binary_name = get_package_binary_name(build_crate).unwrap_or_else(|| build_crate.to_string()); let bin = binary_path(args, &binary_name); if !bin.is_file() { eprintln!( "warning: built binary not found at {} (package: {}, binary: {})", bin.display(), build_crate, binary_name ); return Ok(()); } let profile = get_profile_from_args(args); let app_dir = macos_app_bundle_path_for(&binary_name, &profile); let macos_dir = app_dir.join("Contents/MacOS"); let resources_dir = app_dir.join("Contents/Resources"); fs::create_dir_all(&macos_dir).map_err(|e| format!("failed to create {:?}: {e}", macos_dir))?; fs::create_dir_all(&resources_dir) .map_err(|e| format!("failed to create {:?}: {e}", resources_dir))?; let dst_bin = macos_dir.join(&binary_name); fs::copy(&bin, &dst_bin) .map_err(|e| format!("failed to copy {:?} to {:?}: {e}", bin, dst_bin))?; let crate_dir = get_crate_dir(build_crate)?; let icns_src = crate_dir.join("resources/icon.icns"); if icns_src.is_file() { fs::copy(&icns_src, resources_dir.join("AppIcon.icns")) .map_err(|e| format!("failed to copy {:?}: {e}", icns_src))?; } else { let fallback = Path::new(&icon_env[APP_ICON_IDX_1024]); fs::copy(fallback, resources_dir.join("AppIcon.png")) .map_err(|e| format!("failed to copy {:?}: {e}", fallback))?; } let plist = format!( "\n\ \n\ \n\ \n\ CFBundleExecutable{binary_name}\n\ CFBundleIdentifierdev.makepad.{build_crate}\n\ CFBundleName{binary_name}\n\ CFBundleDisplayName{binary_name}\n\ CFBundlePackageTypeAPPL\n\ CFBundleVersion1.0.0\n\ CFBundleShortVersionString1.0.0\n\ CFBundleIconFileAppIcon\n\ GCSupportsControllerUserInteraction\n\ GCSupportedGameControllers\n\ \n\ \n\ ProfileNameExtendedGamepad\n\ \n\ \n\ \n\ \n" ); let plist_path = app_dir.join("Contents/Info.plist"); fs::write(&plist_path, plist).map_err(|e| format!("failed to write Info.plist: {e}"))?; println!("[cargo-makepad] macOS app bundle: {}", app_dir.display()); println!("[cargo-makepad] macOS app binary: {}", dst_bin.display()); println!( "[cargo-makepad] macOS app resources: {}", resources_dir.display() ); println!("[cargo-makepad] macOS info plist: {}", plist_path.display()); Ok(()) } fn macos_app_bundle_path_for(binary_name: &str, profile: &str) -> PathBuf { cargo_target_dir() .join("makepad-desktop/macos") .join(profile) .join(format!("{binary_name}.app")) } pub(crate) fn resolve_codesign_identity(cert: Option<&str>) -> Result { if let Some(cert) = cert { return Ok(cert.to_string()); } // A standing choice for machines with several identities. if let Some(cert) = std::env::var("MAKEPAD_CODESIGN_IDENTITY").ok().filter(|cert| !cert.trim().is_empty()) { return Ok(cert.trim().to_string()); } let cwd = std::env::current_dir().unwrap(); let identities = shell_env_cap( &[], &cwd, "security", &["find-identity", "-v", "-p", "codesigning"], )?; pick_codesign_identity(&identities) } /// The identity to sign with from `security find-identity -v` output: the /// Apple Development one, or the only one there is. Several certificates /// under one name (a renewed certificate beside the old one) are the same /// signer as far as a designated requirement is concerned, which names the /// certificate's subject, not the certificate: the first is taken, by hash, /// since the shared name alone is ambiguous to codesign. fn pick_codesign_identity(listing: &str) -> Result { let mut apple_development: Vec<(String, String)> = Vec::new(); let mut all: Vec<(String, String)> = Vec::new(); for line in listing.lines() { let name = line .split('"') .nth(1) .map(str::trim) .filter(|value| !value.is_empty()); let hash = line .split_whitespace() .nth(1) .filter(|hash| hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit())); if let (Some(name), Some(hash)) = (name, hash) { all.push((hash.to_string(), name.to_string())); if name.starts_with("Apple Development:") { apple_development.push((hash.to_string(), name.to_string())); } } } let pick = |mut found: Vec<(String, String)>| { if found.len() == 1 { let (_, name) = found.remove(0); Some(name) } else if found.windows(2).all(|pair| pair[0].1 == pair[1].1) { found.first().map(|(hash, _)| hash.clone()) } else { None } }; match (apple_development.is_empty(), all.is_empty()) { (_, true) => Err( "no codesigning identity found; pass --cert=\"Apple Development: ...\" explicitly" .to_string(), ), (false, _) => pick(apple_development).ok_or_else(|| { "multiple Apple Development identities found; pass --cert= or set MAKEPAD_CODESIGN_IDENTITY" .to_string() }), (true, false) => pick(all).ok_or_else(|| { "multiple codesigning identities and no Apple Development one; pass --cert=" .to_string() }), } } #[cfg(test)] mod codesign_identity_tests { use super::pick_codesign_identity; #[test] fn a_renewed_certificate_beside_the_old_one_is_one_signer() { let listing = " 1) 82B72FFCA0221B8B84962F0D6D10B2743ED9E118 \"Apple Development: A B (TEAM)\"\n 2) ED88B9ACB31D482F8746EEE08B057BE803095162 \"Apple Development: A B (TEAM)\"\n 2 valid identities found\n"; assert_eq!(pick_codesign_identity(listing).unwrap(), "82B72FFCA0221B8B84962F0D6D10B2743ED9E118"); } #[test] fn one_identity_is_named_and_different_names_need_a_choice() { let one = " 1) 82B72FFCA0221B8B84962F0D6D10B2743ED9E118 \"Apple Development: A B (TEAM)\"\n"; assert_eq!(pick_codesign_identity(one).unwrap(), "Apple Development: A B (TEAM)"); let two = " 1) 82B72FFCA0221B8B84962F0D6D10B2743ED9E118 \"Apple Development: A B (TEAM)\"\n 2) ED88B9ACB31D482F8746EEE08B057BE803095162 \"Apple Development: C D (TEAM2)\"\n"; assert!(pick_codesign_identity(two).is_err()); assert!(pick_codesign_identity(" 0 valid identities found\n").is_err()); } } fn maybe_write_generated_entitlements( args: &[String], build_crate: &str, binary_name: &str, sign: &MacosSignOptions, ) -> Result, String> { if let Some(path) = &sign.entitlements { return Ok(Some(path.clone())); } if !sign.debugger && !sign.get_task_allow { return Ok(None); } let profile = get_profile_from_args(args); let out_dir = cargo_target_dir() .join("makepad-desktop/macos-sign") .join(profile) .join(build_crate); fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create {:?}: {e}", out_dir))?; let out_path = out_dir.join(format!("{binary_name}.entitlements")); let mut xml = String::from( "\n\ \n\ \n\ \n", ); if sign.debugger { xml.push_str(" com.apple.security.cs.debugger\n \n"); } if sign.get_task_allow { xml.push_str(" com.apple.security.get-task-allow\n \n"); } xml.push_str("\n\n"); write_text(&out_path, &xml)?; Ok(Some(out_path)) } pub(crate) fn codesign_path( path: &Path, identity: &str, entitlements: Option<&Path>, runtime: bool, ) -> Result<(), String> { let cwd = std::env::current_dir().unwrap(); let mut args = vec![ "--force".to_string(), "--timestamp=none".to_string(), "--sign".to_string(), identity.to_string(), ]; if runtime { args.push("--options".to_string()); args.push("runtime".to_string()); } if let Some(entitlements) = entitlements { args.push("--entitlements".to_string()); args.push(entitlements.to_string_lossy().to_string()); args.push("--generate-entitlement-der".to_string()); } args.push(path.to_string_lossy().to_string()); let refs = args.iter().map(|v| v.as_str()).collect::>(); shell_env_cap(&[], &cwd, "codesign", &refs)?; Ok(()) } fn sign_macos_artifacts(args: &[String], sign: &MacosSignOptions) -> Result<(), String> { if !is_macos_target(args) || !sign.enabled { return Ok(()); } let build_crate = get_build_crate_from_args(args)?; let binary_name = get_package_binary_name(build_crate).unwrap_or_else(|| build_crate.to_string()); let identity = resolve_codesign_identity(sign.cert.as_deref())?; let entitlements = maybe_write_generated_entitlements(args, build_crate, &binary_name, sign)?; let entitlements_ref = entitlements.as_deref(); let bin = binary_path(args, &binary_name); if !bin.is_file() { return Err(format!( "built binary not found at {} (package: {}, binary: {})", bin.display(), build_crate, binary_name )); } codesign_path( &bin, &identity, entitlements_ref, sign.runtime || sign.debugger, )?; println!("[cargo-makepad] macOS signed binary: {}", bin.display()); let profile = get_profile_from_args(args); let app_dir = macos_app_bundle_path_for(&binary_name, &profile); if app_dir.is_dir() { codesign_path( &app_dir, &identity, entitlements_ref, sign.runtime || sign.debugger, )?; println!( "[cargo-makepad] macOS signed app bundle: {}", app_dir.display() ); } Ok(()) } fn write_linux_desktop_entry( args: &[String], build_crate: &str, icon_env: &AppIconEnv, ) -> Result<(), String> { if !is_linux_target(args) { return Ok(()); } let binary_name = get_package_binary_name(build_crate).unwrap_or_else(|| build_crate.to_string()); let bin = binary_path(args, &binary_name); if !bin.is_file() { eprintln!( "warning: built binary not found at {} (package: {}, binary: {})", bin.display(), build_crate, binary_name ); return Ok(()); } let profile = get_profile_from_args(args); let out = PathBuf::from("target/makepad-desktop/linux").join(profile); fs::create_dir_all(&out).map_err(|e| format!("failed to create {:?}: {e}", out))?; let icon = Path::new(&icon_env[APP_ICON_IDX_512]); let icon_dst = out.join(format!("{binary_name}.png")); fs::copy(icon, &icon_dst) .map_err(|e| format!("failed to copy {:?} to {:?}: {e}", icon, icon_dst))?; let desktop_path = out.join(format!("{binary_name}.desktop")); let exec_path = bin.canonicalize().unwrap_or(bin); let icon_path = icon_dst.canonicalize().unwrap_or(icon_dst.clone()); let body = format!( "[Desktop Entry]\nType=Application\nName={binary_name}\nExec={}\nIcon={}\nTerminal=false\nCategories=Development;\n", exec_path.to_string_lossy(), icon_path.to_string_lossy(), ); fs::write(&desktop_path, body) .map_err(|e| format!("failed to write {:?}: {e}", desktop_path))?; println!( "[cargo-makepad] Linux desktop entry: {}", desktop_path.display() ); println!("[cargo-makepad] Linux desktop icon: {}", icon_dst.display()); Ok(()) } fn post_build_assets(args: &[String], icon_env: &AppIconEnv) -> Result<(), String> { let build_crate = get_build_crate_from_args(args)?; write_macos_app_bundle(args, build_crate, icon_env)?; write_linux_desktop_entry(args, build_crate, icon_env)?; Ok(()) } fn run_cargo( subcommand: &str, args: &[String], icon_env: Option, sign: &MacosSignOptions, ) -> Result<(), String> { let mut cmd = Command::new("cargo"); cmd.arg(subcommand); cmd.args(args); if let Some(icon) = icon_env.as_ref() { for (var, value) in APP_ICON_ENV_VARS.iter().zip(icon.iter()) { cmd.env(var, value); } let build_crate = get_build_crate_from_args(args)?; add_windows_icon_link_arg(&mut cmd, args, icon, build_crate)?; } let status = cmd .status() .map_err(|e| format!("failed to run cargo {subcommand}: {e}"))?; if !status.success() { return Err(format!("cargo {subcommand} failed with status {status}")); } if subcommand == "build" { if let Some(icon) = icon_env.as_ref() { post_build_assets(args, icon)?; } sign_macos_artifacts(args, sign)?; } Ok(()) } fn resolve_icons_for_args(args: &[String]) -> Result, String> { let build_crate = get_build_crate_from_args(args)?; resolve_app_icon_env(build_crate) } pub fn handle_desktop(args: &[String]) -> Result<(), String> { if args.is_empty() { return Err("desktop requires a subcommand: build, run, check, bundle, sign".to_string()); } match args[0].as_str() { "build" => { let (sign, cargo_args) = parse_macos_sign_options(&args[1..])?; let icon_env = resolve_icons_for_args(&cargo_args)?; run_cargo("build", &cargo_args, icon_env, &sign) } "run" => { let (sign, cargo_args) = parse_macos_sign_options(&args[1..])?; if sign.enabled { return Err( "desktop run does not support signing inline; use `cargo makepad desktop build --sign ...` followed by the signed binary" .to_string(), ); } let icon_env = resolve_icons_for_args(&cargo_args)?; run_cargo("run", &cargo_args, icon_env, &sign) } "check" => { let (sign, cargo_args) = parse_macos_sign_options(&args[1..])?; if sign.enabled { return Err("desktop check cannot sign artifacts".to_string()); } let icon_env = resolve_icons_for_args(&cargo_args)?; run_cargo("check", &cargo_args, icon_env, &sign) } "bundle" => crate::desktop_bundle::handle_desktop_bundle(&args[1..]), "sign" => { let (mut sign, cargo_args) = parse_macos_sign_options(&args[1..])?; sign.enabled = true; sign_macos_artifacts(&cargo_args, &sign) } cmd => Err(format!("{cmd} is not a valid desktop command")), } } fn parse_macos_sign_options(args: &[String]) -> Result<(MacosSignOptions, Vec), String> { let mut sign = MacosSignOptions::default(); let mut cargo_args = Vec::new(); for arg in args { if arg == "--sign" { sign.enabled = true; } else if let Some(value) = arg.strip_prefix("--cert=") { sign.enabled = true; sign.cert = Some(value.to_string()); } else if let Some(value) = arg.strip_prefix("--entitlements=") { sign.enabled = true; sign.entitlements = Some(PathBuf::from(value)); } else if arg == "--debugger" { sign.enabled = true; sign.debugger = true; } else if arg == "--get-task-allow" { sign.enabled = true; sign.get_task_allow = true; } else if arg == "--runtime" { sign.enabled = true; sign.runtime = true; } else { cargo_args.push(arg.clone()); } } if sign.entitlements.is_some() && (sign.debugger || sign.get_task_allow) { return Err( "use either --entitlements= or generated entitlement flags (--debugger/--get-task-allow), not both" .to_string(), ); } Ok((sign, cargo_args)) }