Skip to content

Commit 4ef5797

Browse files
feat(ios): add --no-sign and --archive-only flags to ios build (#15061)
* feat(cli): add --no-sign and --archive-only to tauri ios build * feat(cli): fix manual IPA packaging for --no-sign * feat(cli): add explicit Payload/ directory to IPA zip * separate fn * fmt --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
1 parent 110336c commit 4ef5797

3 files changed

Lines changed: 114 additions & 13 deletions

File tree

.changes/ios-skip-codesign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'tauri-cli': 'minor:feat'
3+
---
4+
5+
Add `--no-sign` and `--archive-only` flags to `tauri ios build`.

crates/tauri-cli/src/mobile/ios/build.rs

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use rand::distr::{Alphanumeric, SampleString};
3636
use std::{
3737
env::{set_current_dir, var, var_os},
3838
fs,
39-
path::PathBuf,
39+
path::{Path, PathBuf},
4040
};
4141

4242
#[derive(Debug, Clone, Parser)]
@@ -94,6 +94,12 @@ pub struct Options {
9494
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
9595
#[clap(long)]
9696
pub ignore_version_mismatches: bool,
97+
/// Skip code signing when bundling the app
98+
#[clap(long)]
99+
pub no_sign: bool,
100+
/// Only archive the app, skip generating the IPA.
101+
#[clap(long)]
102+
pub archive_only: bool,
97103
/// Target device of this build
98104
#[clap(skip)]
99105
pub target_device: Option<TargetDevice>,
@@ -154,7 +160,7 @@ impl From<Options> for BuildOptions {
154160
ci: options.ci,
155161
skip_stapling: false,
156162
ignore_version_mismatches: options.ignore_version_mismatches,
157-
no_sign: false,
163+
no_sign: options.no_sign,
158164
}
159165
}
160166
}
@@ -423,20 +429,26 @@ fn run_build(
423429
}
424430

425431
let credentials = auth_credentials_from_env()?;
426-
let skip_signing = credentials.is_some();
432+
let skip_signing = options.no_sign || credentials.is_some();
427433

428-
let mut build_config = BuildConfig::new().allow_provisioning_updates();
429-
if let Some(credentials) = &credentials {
430-
build_config = build_config
431-
.authentication_credentials(credentials.clone())
432-
.skip_codesign();
433-
}
434+
if !(options.archive_only || options.no_sign) {
435+
let mut build_config = BuildConfig::new().allow_provisioning_updates();
436+
if let Some(credentials) = &credentials {
437+
build_config = build_config.authentication_credentials(credentials.clone());
438+
}
439+
if skip_signing {
440+
build_config = build_config.skip_codesign();
441+
}
434442

435-
target
436-
.build(None, config, env, noise_level, profile, build_config)
437-
.context("failed to build iOS app")?;
443+
target
444+
.build(None, config, env, noise_level, profile, build_config)
445+
.context("failed to build iOS app")?;
446+
}
438447

439-
let mut archive_config = ArchiveConfig::new();
448+
let mut archive_config = ArchiveConfig::new().allow_provisioning_updates();
449+
if let Some(credentials) = &credentials {
450+
archive_config = archive_config.authentication_credentials(credentials.clone());
451+
}
440452
if skip_signing {
441453
archive_config = archive_config.skip_codesign();
442454
}
@@ -452,6 +464,15 @@ fn run_build(
452464
)
453465
.context("failed to archive iOS app")?;
454466

467+
if options.archive_only {
468+
out_files.push(
469+
config
470+
.archive_dir()
471+
.join(format!("{}.xcarchive", config.scheme())),
472+
);
473+
return Ok(());
474+
}
475+
455476
let out_dir = config.export_dir().join(target.arch);
456477

457478
if target.sdk == "iphonesimulator" {
@@ -469,6 +490,23 @@ fn run_build(
469490
let path = out_dir.join(app_path.file_name().unwrap());
470491
fs::rename(&app_path, &path).fs_context("failed to rename app", app_path)?;
471492
out_files.push(path);
493+
} else if options.no_sign {
494+
fs::create_dir_all(&out_dir)
495+
.fs_context("failed to create Xcode output directory", out_dir.clone())?;
496+
497+
let app_path = config
498+
.archive_dir()
499+
.join(format!("{}.xcarchive", config.scheme()))
500+
.join("Products")
501+
.join("Applications")
502+
.join(config.app().stylized_name())
503+
.with_extension("app");
504+
505+
let ipa_path = out_dir
506+
.join(config.app().stylized_name())
507+
.with_extension("ipa");
508+
create_ipa(&app_path, &ipa_path)?;
509+
out_files.push(ipa_path);
472510
} else {
473511
// if we skipped code signing, we do not have the entitlements applied to our exported IPA
474512
// we must force sign the app binary with a dummy certificate just to preserve the entitlements
@@ -545,6 +583,62 @@ fn run_build(
545583
Ok(handle)
546584
}
547585

586+
fn create_ipa(app_path: &Path, ipa_path: &Path) -> Result<()> {
587+
let ipa_file =
588+
fs::File::create(ipa_path).fs_context("failed to create IPA file", ipa_path.to_path_buf())?;
589+
let mut zip = zip::ZipWriter::new(ipa_file);
590+
let options = zip::write::SimpleFileOptions::default()
591+
.compression_method(zip::CompressionMethod::Deflated)
592+
.unix_permissions(0o755);
593+
594+
zip
595+
.add_directory("Payload/", options)
596+
.context("failed to add Payload directory to zip")?;
597+
598+
let mut app_files = Vec::new();
599+
let mut stack = vec![app_path.to_path_buf()];
600+
while let Some(path) = stack.pop() {
601+
if path.is_dir() {
602+
app_files.push(path.clone());
603+
for entry in fs::read_dir(&path).fs_context("failed to read directory", path.clone())? {
604+
stack.push(
605+
entry
606+
.fs_context("failed to read directory entry", path.clone())?
607+
.path(),
608+
);
609+
}
610+
} else {
611+
app_files.push(path);
612+
}
613+
}
614+
615+
for file_path in app_files {
616+
let name = file_path.strip_prefix(app_path.parent().unwrap()).unwrap();
617+
let mut name_str = name.to_string_lossy().to_string();
618+
// zip expects forward slashes
619+
if std::path::MAIN_SEPARATOR == '\\' {
620+
name_str = name_str.replace('\\', "/");
621+
}
622+
let mut name_in_zip = format!("Payload/{name_str}");
623+
624+
if file_path.is_dir() {
625+
name_in_zip.push('/');
626+
zip
627+
.add_directory(name_in_zip, options)
628+
.context("failed to add directory to zip")?;
629+
} else {
630+
zip
631+
.start_file(name_in_zip, options)
632+
.context("failed to start file in zip")?;
633+
let mut f = fs::File::open(&file_path).fs_context("failed to open file", file_path)?;
634+
std::io::copy(&mut f, &mut zip).context("failed to copy file to zip")?;
635+
}
636+
}
637+
638+
zip.finish().context("failed to finish zip")?;
639+
Ok(())
640+
}
641+
548642
fn auth_credentials_from_env() -> Result<Option<cargo_mobile2::apple::AuthCredentials>> {
549643
match (
550644
var("APPLE_API_KEY"),

crates/tauri-cli/src/mobile/ios/run.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
8888
export_method: None,
8989
args: options.args,
9090
ignore_version_mismatches: options.ignore_version_mismatches,
91+
no_sign: false,
92+
archive_only: false,
9193
target_device: device.as_ref().map(|d| TargetDevice {
9294
id: d.id().to_string(),
9395
name: d.name().to_string(),

0 commit comments

Comments
 (0)