diff --git a/packages/cli/schema.json b/packages/cli/schema.json index 73a61448f3..39fbfd85a8 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -972,6 +972,13 @@ "$ref": "#/definitions/IosTypeIdentifier" } }, + "frameworks": { + "description": "Frameworks to embed.", + "type": "array", + "items": { + "type": "string" + } + }, "icon": { "description": "Icons for the app. Overrides `bundle.icon` for iOS builds.", "type": [ diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index b8214c99c8..f71a420f16 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -971,6 +971,17 @@ impl BuildRequest { ); } + // Apple bundles route through dx-as-linker so we can capture resolved -l/-L + // flags and bundle -sys crate dylibs into Frameworks/ (forwards to `cc`). + if custom_linker.is_none() + && matches!( + triple.operating_system, + OperatingSystem::IOS(_) | OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) + ) + { + custom_linker = Some(PathBuf::from("cc")); + } + let target_dir = std::env::var("CARGO_TARGET_DIR") .ok() .map(PathBuf::from) @@ -1057,6 +1068,7 @@ impl BuildRequest { let cache_dir = self.session_cache_dir(); _ = std::fs::create_dir_all(&cache_dir); _ = std::fs::create_dir_all(self.rustc_wrapper_args_dir()); + _ = std::fs::create_dir_all(self.link_args_file().parent().unwrap()); _ = std::fs::File::create_new(self.link_err_file()); _ = std::fs::File::create_new(self.link_args_file()); _ = std::fs::File::create_new(self.windows_command_file()); @@ -1376,16 +1388,17 @@ impl BuildRequest { } } - // Collect the linker args and attach them to the tip crate's bin entry + // Always attach the persisted linker args, even on cached cargo builds where + // the rustc wrapper didn't run and the bin entry doesn't exist yet. let tip_crate_name = self.tip_crate_name(); let tip_bin_key = format!("{tip_crate_name}.bin"); - if let Some(tip_args) = workspace_rustc_args.get_mut(&tip_bin_key) { - tip_args.link_args = std::fs::read_to_string(self.link_args_file()) - .context("Failed to read link args from file")? - .lines() - .map(|s| s.to_string()) - .collect(); - } + let link_args: Vec = std::fs::read_to_string(self.link_args_file()) + .map(|s| s.lines().map(String::from).collect()) + .unwrap_or_default(); + workspace_rustc_args + .entry(tip_bin_key.clone()) + .or_default() + .link_args = link_args; let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?; @@ -2114,6 +2127,11 @@ impl BuildRequest { ) -> Result<()> { let framework_dir = self.frameworks_folder(); + // iOS `installd` rejects any symlinks inside `Frameworks/` (both on-device + // and on the simulator), so we must always copy real files on iOS — even in + // dev builds, where we'd otherwise symlink for speed. + let is_ios = matches!(self.triple.operating_system, OperatingSystem::IOS(_)); + // We use the rustc for the tip crate `main.rs` because that's where the linking happens let direct_rustc = artifacts .workspace_rustc_args @@ -2125,6 +2143,55 @@ impl BuildRequest { let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple); let openssl_dir_disp = openssl_dir.display().to_string(); + // Resolve `-l ` against `-L ` from the captured linker argv and + // copy `lib.dylib` into the frameworks dir. Anything ld linked from a + // project-local `-L` is a -sys crate dylib, not a system lib (those come + // from SDK paths that aren't in `-L`). + if matches!( + self.triple.operating_system, + OperatingSystem::IOS(_) | OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) + ) { + let mut search_dirs: Vec<&str> = Vec::new(); + let mut requested: Vec<&str> = Vec::new(); + let mut iter = direct_rustc.link_args.iter(); + while let Some(arg) = iter.next() { + if arg == "-L" { + if let Some(dir) = iter.next() { + search_dirs.push(dir); + } + } else if let Some(rest) = arg.strip_prefix("-L") { + search_dirs.push(rest); + } else if arg == "-l" { + if let Some(name) = iter.next() { + requested.push(name); + } + } else if let Some(rest) = arg.strip_prefix("-l") { + requested.push(rest); + } + } + for name in &requested { + let filename = format!("lib{name}.dylib"); + for dir in &search_dirs { + let candidate = PathBuf::from(dir).join(&filename); + if candidate.exists() { + let to = framework_dir.join(&filename); + _ = std::fs::remove_file(&to); + _ = std::fs::create_dir_all(&framework_dir); + tracing::debug!("Copying framework from {candidate:?} to {to:?}"); + if cfg!(unix) && !self.release && !is_ios { + #[cfg(unix)] + std::os::unix::fs::symlink(&candidate, &to).with_context(|| { + "Failed to symlink framework into bundle: {candidate:?} -> {to:?}" + })?; + } else { + std::fs::copy(&candidate, &to)?; + } + break; + } + } + } + } + for arg in &direct_rustc.link_args { // todo - how do we handle windows dlls? we don't want to bundle the system dlls // for now, we don't do anything with dlls, and only use .dylibs and .so files @@ -2140,8 +2207,10 @@ impl BuildRequest { _ = std::fs::create_dir_all(&framework_dir); // in dev and on normal oses, we want to symlink the file - // otherwise, just copy it (since in release you want to distribute the framework) - if cfg!(any(windows, unix)) && !self.release { + // otherwise, just copy it (since in release you want to distribute the framework). + // iOS is an exception: `installd` rejects symlinks in `Frameworks/`, so we + // always copy real files on iOS regardless of profile. + if cfg!(any(windows, unix)) && !self.release && !is_ios { #[cfg(windows)] std::os::windows::fs::symlink_file(from, to).with_context(|| { "Failed to symlink framework into bundle: {from:?} -> {to:?}" @@ -2186,6 +2255,34 @@ impl BuildRequest { } } + // Also copy frameworks declared in [ios]/[macos] frameworks, to cover the + // `-L dir -l name` case that the link-args loop above doesn't see. + let declared_frameworks: &[String] = match self.triple.operating_system { + OperatingSystem::IOS(_) => &self.config.ios.frameworks, + OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => { + &self.config.macos.frameworks + } + _ => &[], + }; + + for framework in declared_frameworks { + let framework_path = PathBuf::from(framework); + let resolved = if framework_path.is_absolute() { + framework_path.clone() + } else { + self.crate_dir().join(&framework_path) + }; + + if !resolved.exists() { + tracing::debug!( + "Framework not found as file, assuming system framework: {framework}" + ); + continue; + } + + crate::bundler::copy_framework(&resolved, &framework_dir)?; + } + Ok(()) } @@ -3377,7 +3474,7 @@ impl BuildRequest { .args(args) .envs(env.iter().map(|(k, v)| (k.as_ref(), v))); - if matches!(build_mode, BuildMode::Fat | BuildMode::Base { run: true }) { + if matches!(build_mode, BuildMode::Fat | BuildMode::Base { .. }) { let args_dir = self.rustc_wrapper_args_dir(); std::fs::create_dir_all(&args_dir) .context("Failed to create rustc wrapper args directory")?; @@ -4480,7 +4577,13 @@ impl BuildRequest { } fn link_args_file(&self) -> PathBuf { - self.session_cache_dir().join("link_args.json") + // Persisted under `target/dx/...` so cached `dx bundle` runs (where cargo + // skips the link and the wrapper doesn't run) still see the previous args. + self.target_dir + .join("dx") + .join(&self.main_target) + .join(self.bundle.to_string()) + .join("link_args.json") } fn windows_command_file(&self) -> PathBuf { @@ -6605,6 +6708,29 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ } } + // Sign embedded frameworks first (deepest-first, required by codesign). + let frameworks_dir = self.frameworks_folder(); + if frameworks_dir.exists() { + for entry in std::fs::read_dir(&frameworks_dir)? { + let entry = entry?; + let path = entry.path(); + let output = Command::new("codesign") + .args(["--force", "--sign", app_dev_name]) + .arg(&path) + .output() + .await + .with_context(|| format!("Failed to codesign {}", path.display()))?; + + if !output.status.success() { + bail!( + "Failed to codesign {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr).trim() + ); + } + } + } + // codesign the app let output = Command::new("codesign") .args([ diff --git a/packages/cli/src/bundler/macos.rs b/packages/cli/src/bundler/macos.rs index f9805be3ac..de8b47c59c 100644 --- a/packages/cli/src/bundler/macos.rs +++ b/packages/cli/src/bundler/macos.rs @@ -1,4 +1,4 @@ -use crate::bundler::{copy_dir_recursive, AppCategory, Bundle, BundleContext}; +use crate::bundler::{copy_dir_recursive, copy_framework, AppCategory, Bundle, BundleContext}; use crate::{MacOsSettings, PackageType}; use anyhow::{bail, Context, Result}; use image::{DynamicImage, ImageReader}; @@ -620,21 +620,6 @@ fn write_plist(dict: &plist::Dictionary, path: &Path) -> Result<()> { .with_context(|| format!("Failed to write Info.plist to {}", path.display())) } -/// Copy a framework (directory or .dylib) to the Frameworks directory. -fn copy_framework(src: &Path, frameworks_dir: &Path) -> Result<()> { - let dest = frameworks_dir.join(src.file_name().context("Framework path has no filename")?); - - tracing::debug!("Copying framework: {} -> {}", src.display(), dest.display()); - - if src.is_dir() { - copy_dir_recursive(src, &dest)?; - } else { - fs::copy(src, &dest)?; - } - - Ok(()) -} - /// Set up the signing identity. async fn setup_keychain(identity: Option<&str>) -> Result> { let certificate_encoded = std::env::var("APPLE_CERTIFICATE").ok(); diff --git a/packages/cli/src/bundler/mod.rs b/packages/cli/src/bundler/mod.rs index a4c0a1ae38..3efac50394 100644 --- a/packages/cli/src/bundler/mod.rs +++ b/packages/cli/src/bundler/mod.rs @@ -805,6 +805,18 @@ pub(crate) fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> { Ok(()) } +/// Copy a framework directory or bare .dylib into an Apple `.app` Frameworks directory. +pub(crate) fn copy_framework(src: &Path, frameworks_dir: &Path) -> Result<()> { + std::fs::create_dir_all(frameworks_dir)?; + let dest = frameworks_dir.join(src.file_name().context("Framework path has no filename")?); + if src.is_dir() { + copy_dir_recursive(src, &dest)?; + } else { + std::fs::copy(src, &dest)?; + } + Ok(()) +} + /// Recursively zip a directory tree while preserving relative paths and Unix modes. pub(crate) fn zip_dir_recursive(src: &Path, dest: &Path) -> Result<()> { use std::fs::File; diff --git a/packages/cli/src/config/manifest.rs b/packages/cli/src/config/manifest.rs index 2d133fd84d..5a4a5b6485 100644 --- a/packages/cli/src/config/manifest.rs +++ b/packages/cli/src/config/manifest.rs @@ -334,6 +334,10 @@ pub struct IosConfig { #[serde(default)] pub info_plist: Option, + /// Frameworks to embed. + #[serde(default)] + pub frameworks: Vec, + /// iOS entitlements configuration. #[serde(default)] pub entitlements: IosEntitlements,