Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/cli/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
150 changes: 138 additions & 12 deletions packages/cli/src/build/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<String> = 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.")?;

Expand Down Expand Up @@ -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
Expand All @@ -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 <name>` against `-L <dir>` from the captured linker argv and
// copy `lib<name>.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
Expand All @@ -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:?}"
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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")?;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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([
Expand Down
17 changes: 1 addition & 16 deletions packages/cli/src/bundler/macos.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Option<SigningIdentity>> {
let certificate_encoded = std::env::var("APPLE_CERTIFICATE").ok();
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/bundler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/config/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ pub struct IosConfig {
#[serde(default)]
pub info_plist: Option<PathBuf>,

/// Frameworks to embed.
#[serde(default)]
pub frameworks: Vec<String>,

/// iOS entitlements configuration.
#[serde(default)]
pub entitlements: IosEntitlements,
Expand Down