diff --git a/bindings/packager/nodejs/schema.json b/bindings/packager/nodejs/schema.json index 21f89fac..1dfa4496 100644 --- a/bindings/packager/nodejs/schema.json +++ b/bindings/packager/nodejs/schema.json @@ -470,6 +470,13 @@ "enum": [ "pacman" ] + }, + { + "description": "iOS application bundle", + "type": "string", + "enum": [ + "ios" + ] } ] }, diff --git a/crates/packager/schema.json b/crates/packager/schema.json index 21f89fac..1dfa4496 100644 --- a/crates/packager/schema.json +++ b/crates/packager/schema.json @@ -470,6 +470,13 @@ "enum": [ "pacman" ] + }, + { + "description": "iOS application bundle", + "type": "string", + "enum": [ + "ios" + ] } ] }, diff --git a/crates/packager/src/package/ios/LaunchScreen.storyboard b/crates/packager/src/package/ios/LaunchScreen.storyboard new file mode 100644 index 00000000..ee58efff --- /dev/null +++ b/crates/packager/src/package/ios/LaunchScreen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/packager/src/package/ios/PkgInfo b/crates/packager/src/package/ios/PkgInfo new file mode 100644 index 00000000..bd04210f --- /dev/null +++ b/crates/packager/src/package/ios/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/crates/packager/src/package/ios/mod.rs b/crates/packager/src/package/ios/mod.rs new file mode 100644 index 00000000..6fd49a2b --- /dev/null +++ b/crates/packager/src/package/ios/mod.rs @@ -0,0 +1,538 @@ +// Copyright 2016-2019 Cargo-Bundle developers +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// Copyright 2023-2023 CrabNebula Ltd. +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::Context; +use crate::{ + codesign::macos::{self as codesign, SignTarget}, + shell::CommandExt, +}; +use crate::{config::Config, util}; +use std::{ + collections::BinaryHeap, + path::{Path, PathBuf}, +}; + +#[tracing::instrument(level = "trace")] +pub(crate) fn package(ctx: &Context) -> crate::Result> { + let Context { config, .. } = ctx; + + // we should use the bundle name (App name) as a MacOS standard. + // version or platform shouldn't be included in the App name. + let app_product_name = format!("{}.app", config.product_name); + let app_bundle_path = config.out_dir().join("Payload").join(&app_product_name); + + if app_bundle_path.exists() { + std::fs::remove_dir_all(&app_bundle_path)?; + } + + tracing::info!( + "Packaging {} ({})", + app_product_name, + app_bundle_path.display() + ); + + std::fs::create_dir_all(&app_bundle_path)?; + + let bundle_icon_file = util::create_asset_car_file(&config.out_dir(), config)?; + + tracing::debug!("Creating Info.plist"); + create_info_plist(&app_bundle_path, bundle_icon_file.is_some(), config)?; + + tracing::debug!("Copying frameworks"); + let framework_paths = copy_frameworks_to_bundle(&app_bundle_path, config)?; + + let mut sign_paths = BinaryHeap::from_iter( + framework_paths + .into_iter() + .filter(|p| { + let ext = p.extension(); + ext == Some(std::ffi::OsStr::new("framework")) + }) + .map(|path| SignTarget { + path, + is_native_binary: false, + }), + ); + + // tracing::debug!("Copying resources"); + // config.copy_resources(&resources_dir)?; + + // tracing::debug!("Copying external binaries"); + // config.copy_external_binaries(&bin_dir)?; + + tracing::debug!("Copying binaries"); + for bin in &config.binaries { + let bin_path = config.binary_path(bin); + let dest_path = app_bundle_path.join(bin.path.file_name().unwrap()); + std::fs::copy(&bin_path, &dest_path)?; + } + + tracing::debug!("Copying other files"); + std::fs::write(app_bundle_path.join("PkgInfo"), include_bytes!("PkgInfo"))?; + std::fs::write(config.out_dir().join("LaunchScreen.storyboard"), include_bytes!("LaunchScreen.storyboard"))?; + // cp -rf {{ProjectDir}}/_deployment/ios/PrivacyInfo.xcprivacy {{AppBundle}}/PrivacyInfo.xcprivacy + + // All dylib files and native executables should be signed manually + // It is highly discouraged by Apple to use the --deep codesign parameter in larger projects. + // https://developer.apple.com/forums/thread/129980 + + // Find all files in the app bundle + let files = walkdir::WalkDir::new(&app_bundle_path) + .into_iter() + .flatten() + .map(|dir| dir.into_path()); + + // Filter all files for Mach-O headers. This will target all .dylib and native executable files + for file in files { + let metadata = match std::fs::metadata(&file) { + Ok(f) => f, + Err(err) => { + tracing::warn!("Failed to get metadata for {}: {err}, this file will not be scanned for Mach-O header!", file.display()); + continue; + } + }; + + // ignore folders and files that do not include at least the header size + if !metadata.is_file() || metadata.len() < 4 { + continue; + } + + let mut open_file = match std::fs::File::open(&file) { + Ok(f) => f, + Err(err) => { + tracing::warn!("Failed to open {} for reading: {err}, this file will not be scanned for Mach-O header!", file.display()); + continue; + } + }; + + let mut buffer = [0; 4]; + std::io::Read::read_exact(&mut open_file, &mut buffer)?; + + const MACH_O_MAGIC_NUMBERS: [u32; 5] = + [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xcefaedfe, 0xcffaedfe]; + + let magic = u32::from_be_bytes(buffer); + + let is_mach = MACH_O_MAGIC_NUMBERS.contains(&magic); + if !is_mach { + continue; + } + + sign_paths.push(SignTarget { + path: file, + is_native_binary: true, + }); + } + + if let Some(identity) = config + .macos() + .and_then(|macos| macos.signing_identity.as_ref()) + { + tracing::debug!("Codesigning {}", app_bundle_path.display()); + // Sign frameworks and sidecar binaries first, per apple, signing must be done inside out + // https://developer.apple.com/forums/thread/701514 + sign_paths.push(SignTarget { + path: app_bundle_path.clone(), + is_native_binary: true, + }); + + // Remove extra attributes, which could cause codesign to fail + // https://developer.apple.com/library/archive/qa/qa1940/_index.html + remove_extra_attr(&app_bundle_path)?; + + // sign application + let sign_paths = sign_paths.into_sorted_vec(); + codesign::try_sign(sign_paths, identity, config)?; + + // notarization is required for distribution + match config + .macos() + .and_then(|m| m.notarization_credentials.clone()) + .ok_or(crate::Error::MissingNotarizeAuthVars) + .or_else(|_| codesign::notarize_auth()) + { + Ok(auth) => { + tracing::debug!("Notarizing {}", app_bundle_path.display()); + codesign::notarize(app_bundle_path.clone(), auth, config)?; + } + Err(e) => { + tracing::warn!("Skipping app notarization, {}", e.to_string()); + } + } + } + + let out = std::process::Command::new("ibtool") + .args([ + "--errors", + "--warnings", + "--notices", + "--module", + config.main_binary_name()?.as_str(), + "--target-device", + "iphone", + "--target-device", + "ipad", + "--minimum-deployment-target", + "14.0", + "--output-format", + "human-readable-text", + "--auto-activate-custom-fonts", + "--compilation-directory", config.out_dir().to_str().unwrap(), + config.out_dir().join("LaunchScreen.storyboard").to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(out.status.success()); + + let out = std::process::Command::new("ibtool") + .args([ + "--errors", + "--warnings", + "--notices", + "--module", + config.main_binary_name()?.as_str(), + "--target-device", + "iphone", + "--target-device", + "ipad", + "--minimum-deployment-target", + "14.0", + "--output-format", + "human-readable-text", + "--link", + app_bundle_path.to_str().unwrap(), + config.out_dir().join("LaunchScreen.storyboardc").to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(out.status.success()); + + let out = std::process::Command::new("zip") + .args([ + "-r", + config.out_dir().join(format!("{}.ipa", config.product_name)).to_str().unwrap(), + config.out_dir().join("Payload").to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(out.status.success()); + + // build-and-package + // Generate entitlements + // PlistBuddy -x -c "Add :application-identifier string {{TeamID}}.{{BundleIdentifier}}" {{AppBundle}}/../../entitlements.xcent + // PlistBuddy -x -c "Add :com.apple.developer.team-identifier string {{TeamID}}" {{AppBundle}}/../../entitlements.xcent + // PlistBuddy -x -c "Add :com.apple.developer.kernel.increased-memory-limit bool true" {{AppBundle}}/../../entitlements.xcent + // PlistBuddy -x -c "Add :get-task-allow bool false" {{AppBundle}}/../../entitlements.xcent + // PlistBuddy -x -c "Add :keychain-access-groups array" {{AppBundle}}/../../entitlements.xcent + // PlistBuddy -x -c "Add :keychain-access-groups:0 string {{TeamID}}.{{BundleIdentifier}}" {{AppBundle}}/../../entitlements.xcent + + // compile launchscreen + + // copy provisioning profile + // cp -f "$PROVISIONING_PROFILE" {{AppBundle}}/embedded.mobileprovision + + Ok(vec![app_bundle_path]) +} + +// Creates the Info.plist file. +#[tracing::instrument(level = "trace")] +fn create_info_plist( + contents_directory: &Path, + has_icon: bool, + config: &Config, +) -> crate::Result<()> { + let format = time::format_description::parse("[year][month][day].[hour][minute][second]") + .map_err(time::error::Error::from)?; + let build_number = time::OffsetDateTime::now_utc() + .format(&format) + .map_err(time::error::Error::from)?; + + let mut plist = plist::Dictionary::new(); + plist.insert("CFBundleDevelopmentRegion".into(), "English".into()); + plist.insert( + "CFBundleDisplayName".into(), + config.product_name.clone().into(), + ); + plist.insert( + "CFBundleExecutable".into(), + config.main_binary_name()?.clone().into(), + ); + plist.insert("UILaunchStoryboardName".into(), "LaunchScreen".into()); + + if has_icon { + let mut bundle_primary_icon = plist::Dictionary::new(); + bundle_primary_icon.insert("CFBundleIconFiles".to_string(), plist::Value::Array(vec!["AppIcon60x60".into(), "AppIcon76x76".into()])); + bundle_primary_icon.insert("CFBundleIconName".into(), "AppIcon".into()); + + let mut bundle_icons = plist::Dictionary::new(); + bundle_icons.insert("CFBundlePrimaryIcon".into(), bundle_primary_icon.into()); + + plist.insert("CFBundleIcons".into(), bundle_icons.into()); + } + + plist.insert("CFBundleIdentifier".into(), config.identifier().into()); + plist.insert("CFBundleInfoDictionaryVersion".into(), "6.0".into()); + plist.insert("CFBundleName".into(), config.product_name.clone().into()); + plist.insert("CFBundlePackageType".into(), "APPL".into()); + plist.insert( + "CFBundleShortVersionString".into(), + config.version.clone().into(), + ); + plist.insert("CFBundleVersion".into(), build_number.into()); + plist.insert("CSResourcesFileMapped".into(), true.into()); + if let Some(category) = &config.category { + plist.insert( + "LSApplicationCategoryType".into(), + category.macos_application_category_type().into(), + ); + } + if let Some(version) = config + .macos() + .and_then(|macos| macos.minimum_system_version.as_deref()) + { + plist.insert("LSMinimumSystemVersion".into(), version.into()); + } + + if let Some(associations) = &config.file_associations { + plist.insert( + "CFBundleDocumentTypes".into(), + plist::Value::Array( + associations + .iter() + .map(|association| { + let mut dict = plist::Dictionary::new(); + dict.insert( + "CFBundleTypeExtensions".into(), + plist::Value::Array( + association + .extensions + .iter() + .map(|ext| ext.to_string().into()) + .collect(), + ), + ); + dict.insert( + "CFBundleTypeName".into(), + association + .name + .as_ref() + .unwrap_or(&association.extensions[0]) + .to_string() + .into(), + ); + dict.insert( + "CFBundleTypeRole".into(), + association.role.to_string().into(), + ); + plist::Value::Dictionary(dict) + }) + .collect(), + ), + ); + } + + if let Some(protocols) = &config.deep_link_protocols { + plist.insert( + "CFBundleURLTypes".into(), + plist::Value::Array( + protocols + .iter() + .map(|protocol| { + let mut dict = plist::Dictionary::new(); + dict.insert( + "CFBundleURLSchemes".into(), + plist::Value::Array( + protocol + .schemes + .iter() + .map(|s| s.to_string().into()) + .collect(), + ), + ); + dict.insert( + "CFBundleURLName".into(), + protocol + .name + .clone() + .unwrap_or(format!( + "{} {}", + config.identifier(), + protocol.schemes[0] + )) + .into(), + ); + dict.insert("CFBundleTypeRole".into(), protocol.role.to_string().into()); + plist::Value::Dictionary(dict) + }) + .collect(), + ), + ); + } + + plist.insert("NSHighResolutionCapable".into(), true.into()); + if let Some(copyright) = &config.copyright { + plist.insert("NSHumanReadableCopyright".into(), copyright.clone().into()); + } + + if let Some(exception_domain) = config + .macos() + .and_then(|macos| macos.exception_domain.clone()) + { + let mut security = plist::Dictionary::new(); + let mut domain = plist::Dictionary::new(); + domain.insert("NSExceptionAllowsInsecureHTTPLoads".into(), true.into()); + domain.insert("NSIncludesSubdomains".into(), true.into()); + + let mut exception_domains = plist::Dictionary::new(); + exception_domains.insert(exception_domain, domain.into()); + security.insert("NSExceptionDomains".into(), exception_domains.into()); + plist.insert("NSAppTransportSecurity".into(), security.into()); + } + + if let Some(user_plist_path) = config + .macos() + .and_then(|macos| macos.info_plist_path.as_ref()) + { + let user_plist = plist::Value::from_file(user_plist_path)?; + if let Some(dict) = user_plist.into_dictionary() { + for (key, value) in dict { + plist.insert(key, value); + } + } + } + + plist::Value::Dictionary(plist).to_file_xml(contents_directory.join("Info.plist"))?; + + Ok(()) +} + +#[tracing::instrument(level = "trace")] +fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> { + if !from.exists() { + return Err(crate::Error::DoesNotExist(from.to_path_buf())); + } + if !from.is_dir() { + return Err(crate::Error::IsNotDirectory(from.to_path_buf())); + } + if to.exists() { + return Err(crate::Error::AlreadyExists(to.to_path_buf())); + } + + let parent = to + .parent() + .ok_or_else(|| crate::Error::ParentDirNotFound(to.to_path_buf()))?; + std::fs::create_dir_all(parent)?; + for entry in walkdir::WalkDir::new(from) { + let entry = entry?; + debug_assert!(entry.path().starts_with(from)); + let rel_path = entry.path().strip_prefix(from)?; + let dest_path = to.join(rel_path); + if entry.file_type().is_symlink() { + let target = std::fs::read_link(entry.path())?; + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &dest_path)?; + #[cfg(windows)] + { + if entry.file_type().is_file() { + std::os::windows::fs::symlink_file(&target, &dest_path)?; + } else { + std::os::windows::fs::symlink_dir(&target, &dest_path)?; + } + } + } else if entry.file_type().is_dir() { + std::fs::create_dir(dest_path)?; + } else { + std::fs::copy(entry.path(), dest_path)?; + } + } + Ok(()) +} + +// Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`. +#[tracing::instrument(level = "trace")] +fn copy_framework_from(dest_dir: &Path, framework: &str, src_dir: &Path) -> crate::Result { + let src_name = format!("{}.framework", framework); + let src_path = src_dir.join(&src_name); + if src_path.exists() { + copy_dir(&src_path, &dest_dir.join(&src_name))?; + Ok(true) + } else { + Ok(false) + } +} + +// Copies the macOS application bundle frameworks to the .app +#[tracing::instrument(level = "trace")] +fn copy_frameworks_to_bundle( + contents_directory: &Path, + config: &Config, +) -> crate::Result> { + let mut paths = Vec::new(); + + if let Some(frameworks) = config.macos().and_then(|m| m.frameworks.as_ref()) { + let dest_dir = contents_directory.join("Frameworks"); + std::fs::create_dir_all(contents_directory)?; + + for framework in frameworks { + if framework.ends_with(".framework") || framework.ends_with(".app") { + let src_path = PathBuf::from(framework); + let src_name = src_path + .file_name() + .ok_or_else(|| crate::Error::FailedToExtractFilename(src_path.clone()))?; + let dest_path = dest_dir.join(src_name); + copy_dir(&src_path, &dest_path)?; + paths.push(dest_path); + continue; + } else if framework.ends_with(".dylib") { + let src_path = PathBuf::from(&framework); + if !src_path.exists() { + return Err(crate::Error::FrameworkNotFound(framework.to_string())); + } + let src_name = src_path + .file_name() + .ok_or_else(|| crate::Error::FailedToExtractFilename(src_path.clone()))?; + std::fs::create_dir_all(&dest_dir)?; + let dest_path = dest_dir.join(src_name); + std::fs::copy(&src_path, &dest_path)?; + paths.push(dest_path); + continue; + } else if framework.contains('/') { + return Err(crate::Error::InvalidFramework { + framework: framework.to_string(), + reason: "framework extension should be either .framework, .dylib or .app", + }); + } + if let Some(home_dir) = dirs::home_dir() { + if copy_framework_from(&dest_dir, framework, &home_dir.join("Library/Frameworks/"))? + { + continue; + } + } + if copy_framework_from(&dest_dir, framework, &PathBuf::from("/Library/Frameworks/"))? + || copy_framework_from( + &dest_dir, + framework, + &PathBuf::from("/Network/Library/Frameworks/"), + )? + { + continue; + } + + return Err(crate::Error::FrameworkNotFound(framework.to_string())); + } + } + + Ok(paths) +} + +fn remove_extra_attr(app_bundle_path: &Path) -> crate::Result<()> { + std::process::Command::new("xattr") + .arg("-cr") + .arg(app_bundle_path) + .output_ok() + .map(|_| ()) + .map_err(crate::Error::FailedToRemoveExtendedAttributes) +} diff --git a/crates/packager/src/package/mod.rs b/crates/packager/src/package/mod.rs index 30c15dd0..60fe7dac 100644 --- a/crates/packager/src/package/mod.rs +++ b/crates/packager/src/package/mod.rs @@ -27,6 +27,8 @@ mod appimage; mod deb; #[cfg(target_os = "macos")] mod dmg; +#[cfg(target_os = "macos")] +mod ios; mod nsis; #[cfg(any( target_os = "linux", @@ -104,6 +106,8 @@ pub fn package(config: &Config) -> crate::Result> { let paths = match format { PackageFormat::App => app::package(&ctx), #[cfg(target_os = "macos")] + PackageFormat::Ios => ios::package(&ctx), + #[cfg(target_os = "macos")] PackageFormat::Dmg => { // PackageFormat::App is required for the DMG bundle if !packages diff --git a/crates/packager/src/util.rs b/crates/packager/src/util.rs index fa3f9bc8..22fcc179 100644 --- a/crates/packager/src/util.rs +++ b/crates/packager/src/util.rs @@ -400,6 +400,111 @@ fn make_icns_image(img: image::DynamicImage) -> std::io::Result { icns::Image::from_data(pixel_format, img.width(), img.height(), img.into_bytes()) } +pub fn create_asset_car_file(out_dir: &Path, config: &crate::Config) -> crate::Result> { + use image::GenericImageView; + + if let Some(icons) = config.icons()? { + let icon = image::open(&icons[0])?; + let xcassets = out_dir.join("Images.xcassets"); + let icon_set = xcassets.join("AppIcon.appiconset"); + std::fs::create_dir_all(&icon_set)?; + + let save_icon = |width: u32, height: u32, name: &str| -> crate::Result<()> { + let icon = icon.resize_exact( + width, + height, + image::imageops::FilterType::Lanczos3, + ); + icon. save_with_format(icon_set.join(name), image::ImageFormat::Png)?; + Ok(()) + }; + + save_icon(40, 40, "AppIcon-20@2x.png")?; + save_icon(40, 40, "AppIcon-20@2x~ipad.png")?; + save_icon(60, 60, "AppIcon-20@3x.png")?; + save_icon(20, 20, "AppIcon-20~ipad.png")?; + save_icon(29, 29, "AppIcon-29.png")?; + save_icon(58, 58, "AppIcon-29@2x.png")?; + save_icon(58, 58, "AppIcon-29@2x~ipad.png")?; + save_icon(87, 87, "AppIcon-29@3x.png")?; + save_icon(29, 29, "AppIcon-29~ipad.png")?; + save_icon(80, 80, "AppIcon-40@2x.png")?; + save_icon(80, 80, "AppIcon-40@2x~ipad.png")?; + save_icon(120, 120, "AppIcon-40@3x.png")?; + save_icon(40, 40, "AppIcon-40~ipad.png")?; + save_icon(120, 120, "AppIcon-60@2x~car.png")?; + save_icon(180, 180, "AppIcon-60@3x~car.png")?; + save_icon(167, 167, "AppIcon-83.5@2x~ipad.png")?; + save_icon(120, 120, "AppIcon@2x.png")?; + save_icon(152, 152, "AppIcon@2x~ipad.png")?; + save_icon(180, 180, "AppIcon@3x.png")?; + save_icon(1024, 1024, "AppIcon~ios-marketing.png")?; + save_icon(76, 76, "AppIcon~ipad.png")?; + + std::fs::write(icon_set.join("Contents.json"), r#"{ + "images": [ + { "filename": "AppIcon@2x.png", "idiom": "iphone", "scale": "2x", "size": "60x60" }, + { "filename": "AppIcon@3x.png", "idiom": "iphone", "scale": "3x", "size": "60x60" }, + { "filename": "AppIcon~ipad.png", "idiom": "ipad", "scale": "1x", "size": "76x76" }, + { "filename": "AppIcon@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "76x76" }, + { "filename": "AppIcon-83.5@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "83.5x83.5" }, + { "filename": "AppIcon-40@2x.png", "idiom": "iphone", "scale": "2x", "size": "40x40" }, + { "filename": "AppIcon-40@3x.png", "idiom": "iphone", "scale": "3x", "size": "40x40" }, + { "filename": "AppIcon-40~ipad.png", "idiom": "ipad", "scale": "1x", "size": "40x40" }, + { "filename": "AppIcon-40@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "40x40" }, + { "filename": "AppIcon-20@2x.png", "idiom": "iphone", "scale": "2x", "size": "20x20" }, + { "filename": "AppIcon-20@3x.png", "idiom": "iphone", "scale": "3x", "size": "20x20" }, + { "filename": "AppIcon-20~ipad.png", "idiom": "ipad", "scale": "1x", "size": "20x20" }, + { "filename": "AppIcon-20@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "20x20" }, + { "filename": "AppIcon-29.png", "idiom": "iphone", "scale": "1x", "size": "29x29" }, + { "filename": "AppIcon-29@2x.png", "idiom": "iphone", "scale": "2x", "size": "29x29" }, + { "filename": "AppIcon-29@3x.png", "idiom": "iphone", "scale": "3x", "size": "29x29" }, + { "filename": "AppIcon-29~ipad.png", "idiom": "ipad", "scale": "1x", "size": "29x29" }, + { "filename": "AppIcon-29@2x~ipad.png", "idiom": "ipad", "scale": "2x", "size": "29x29" }, + { "filename": "AppIcon-60@2x~car.png", "idiom": "car", "scale": "2x", "size": "60x60" }, + { "filename": "AppIcon-60@3x~car.png", "idiom": "car", "scale": "3x", "size": "60x60" }, + { "filename": "AppIcon~ios-marketing.png", "idiom": "ios-marketing", "scale": "1x", "size": "1024x1024" } + ], + "info": { + "author": "xcode", + "version": 1 + } + }"#)?; + + std::fs::write(xcassets.join("Contents.json"), r#"{ + "info" : { + "version" : 1, + "author" : "xcode" + } + }"#)?; + + let app_product_name = format!("{}.app", config.product_name); + let app_bundle_path = config.out_dir().join("Payload").join(&app_product_name); + let out_path = out_dir.join("AppIcon.plist"); + + let out = std::process::Command::new("actool") + .args([ + xcassets.to_str().unwrap(), + "--compile", + app_bundle_path.to_str().unwrap(), + "--platform", + "iphoneos", + "--minimum-deployment-target", + "15", + "--app-icon", + "AppIcon", + "--output-partial-info-plist", + out_path.to_str().unwrap() + ]).output().unwrap(); + + assert!(out.status.success()); + + Ok(Some(out_path)) + } else { + Ok(None) + } +} + /// Writes a tar file to the given writer containing the given directory. /// /// The generated tar contains the `src_dir` as a whole and not just its files, diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 2977f470..a786841a 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -45,6 +45,8 @@ pub enum PackageFormat { AppImage, /// The Linux Pacman package (.tar.gz and PKGBUILD) Pacman, + /// iOS application bundle + Ios, } impl Display for PackageFormat { @@ -60,6 +62,7 @@ impl PackageFormat { // Other types we may eventually want to support: apk. match name { "app" => Some(PackageFormat::App), + "ios" => Some(PackageFormat::Ios), "dmg" => Some(PackageFormat::Dmg), "wix" => Some(PackageFormat::Wix), "nsis" => Some(PackageFormat::Nsis), @@ -77,6 +80,7 @@ impl PackageFormat { #[cfg(feature = "cli")] PackageFormat::Default => "default", PackageFormat::App => "app", + PackageFormat::Ios => "ios", PackageFormat::Dmg => "dmg", PackageFormat::Wix => "wix", PackageFormat::Nsis => "nsis", @@ -181,6 +185,7 @@ impl PackageFormat { #[cfg(feature = "cli")] PackageFormat::Default => 0, PackageFormat::App => 0, + PackageFormat::Ios => 0, PackageFormat::Wix => 0, PackageFormat::Nsis => 0, PackageFormat::Deb => 0, diff --git a/package.json b/package.json index 4b6ba25a..588df339 100644 --- a/package.json +++ b/package.json @@ -8,5 +8,6 @@ }, "devDependencies": { "prettier": "^3.1.0" - } + }, + "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" }