/
githubmirror
/
tauri
Обзор
Документация
Войти
/
githubmirror
/
tauri
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
crates/tauri-cli/src/mobile/ios/mod.rs
698 строк
21 KB
sftse
Less statics fixup (#14833)
27 янв 2026, 11:33
Не верифицирован
27 янв 2026, 11:33
3a4e165
Код
Авторство
О чём код?
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use cargo_mobile2::{ apple::{ config::{ Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform, Raw as RawAppleConfig, }, device::{self, Device}, target::Target, teams::find_development_teams, }, config::app::{App, DEFAULT_ASSET_DIR}, env::Env, opts::NoiseLevel, os, util::{prompt, relativize_path}, }; use clap::{Parser, Subcommand}; use serde::Deserialize; use sublime_fuzzy::best_match; use tauri_utils::resources::ResourcePaths; use super::{ ensure_init, env, get_app, init::command as init_command, log_finished, read_options, CliOptions, OptionsHandle, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE, }; use crate::{ error::{Context, ErrorExt}, helpers::{ config::{BundleResources, Config as TauriConfig, ConfigMetadata}, pbxproj, strip_semver_prerelease_tag, }, ConfigValue, Error, Result, }; use std::{ env::{set_var, var_os}, fs::create_dir_all, path::Path, str::FromStr, thread::sleep, time::Duration, }; mod build; mod dev; pub(crate) mod project; mod run; mod xcode_script; pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "APPLE_DEVELOPMENT_TEAM"; pub const LIB_OUTPUT_FILE_NAME: &str = "libapp.a"; #[derive(Parser)] #[clap( author, version, about = "iOS commands", subcommand_required(true), arg_required_else_help(true) )] pub struct Cli { #[clap(subcommand)] command: Commands, } #[derive(Debug, Parser)] #[clap(about = "Initialize iOS target in the project")] pub struct InitOptions { /// Skip prompting for values #[clap(long, env = "CI")] ci: bool, /// Reinstall dependencies #[clap(short, long)] reinstall_deps: bool, /// Skips installing rust toolchains via rustup #[clap(long)] skip_targets_install: bool, /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file /// /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. /// /// Note that a platform-specific file is looked up and merged with the default file by default /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) /// but you can use this for more specific use cases such as different build flavors. #[clap(short, long)] pub config: Vec<ConfigValue>, } #[derive(Subcommand)] enum Commands { Init(InitOptions), Dev(dev::Options), Build(build::Options), Run(run::Options), #[clap(hide(true))] XcodeScript(xcode_script::Options), } pub fn command(cli: Cli, verbosity: u8) -> Result<()> { let noise_level = NoiseLevel::from_occurrences(verbosity as u64); match cli.command { Commands::Init(options) => init_command( MobileTarget::Ios, options.ci, options.reinstall_deps, options.skip_targets_install, options.config, )?, Commands::Dev(options) => dev::command(options, noise_level)?, Commands::Build(options) => build::command(options, noise_level).map(|_| ())?, Commands::Run(options) => run::command(options, noise_level)?, Commands::XcodeScript(options) => xcode_script::command(options)?, } Ok(()) } pub fn get_config( app: &App, tauri_config: &TauriConfig, features: &[String], cli_options: &CliOptions, tauri_dir: &Path, ) -> Result<(AppleConfig, AppleMetadata)> { let mut ios_options = cli_options.clone(); ios_options.features.extend_from_slice(features); let bundle_version = if let Some(bundle_version) = tauri_config .bundle .ios .bundle_version .clone() .or_else(|| tauri_config.version.clone()) { // if it's a semver string, we must strip the prerelease tag if let Ok(mut version) = semver::Version::from_str(&bundle_version) { if !version.pre.is_empty() { log::warn!("CFBundleVersion cannot have prerelease tag; stripping from {bundle_version}"); strip_semver_prerelease_tag(&mut version)?; } // correctly serialize version - cannot contain `+` as build metadata separator Some(format!( "{}.{}.{}{}", version.major, version.minor, version.patch, if version.build.is_empty() { "".to_string() } else { format!(".{}", version.build.as_str()) } )) } else { // let it go as is - cargo-mobile2 will validate it Some(bundle_version) } } else { None }; let full_bundle_version_short = if let Some(app_version) = &tauri_config.version { if let Ok(mut version) = semver::Version::from_str(app_version) { if !version.pre.is_empty() { log::warn!( "CFBundleShortVersionString cannot have prerelease tag; stripping from {app_version}" ); strip_semver_prerelease_tag(&mut version)?; } // correctly serialize version - cannot contain `+` as build metadata separator Some(format!( "{}.{}.{}{}", version.major, version.minor, version.patch, if version.build.is_empty() { "".to_string() } else { format!(".{}", version.build.as_str()) } )) } else { // let it go as is - cargo-mobile2 will validate it Some(app_version.clone()) } } else { bundle_version.clone() }; let bundle_version_short = if let Some(full_version) = full_bundle_version_short.as_deref() { let mut s = full_version.split('.'); let short_version = format!( "{}.{}.{}", s.next().unwrap_or("0"), s.next().unwrap_or("0"), s.next().unwrap_or("0") ); if short_version != full_version { log::warn!("{full_version:?} is not a valid CFBundleShortVersionString since it must contain exactly three dot separated integers; setting it to {short_version} instead"); } Some(short_version) } else { None }; let raw = RawAppleConfig { development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME) .ok() .or_else(|| tauri_config.bundle.ios.development_team.clone()) .or_else(|| { let teams = find_development_teams().unwrap_or_default(); match teams.len() { 0 => { log::warn!("No code signing certificates found. You must add one and set the certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. To list the available certificates, run `tauri info`."); None } 1 => None, _ => { log::warn!("You must set the code signing certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::<Vec<String>>().join(", ")); None } } }), ios_features: Some(ios_options.features.clone()), bundle_version, bundle_version_short, ios_version: Some(tauri_config.bundle.ios.minimum_system_version.clone()), ..Default::default() }; let config = AppleConfig::from_raw(app.clone(), Some(raw)) .context("failed to create Apple configuration")?; let mut vendor_frameworks = Vec::new(); let mut frameworks = Vec::new(); for framework in tauri_config .bundle .ios .frameworks .clone() .unwrap_or_default() { let framework_path = Path::new(&framework); let ext = framework_path.extension().unwrap_or_default(); if ext.is_empty() { frameworks.push(framework); } else if ext == "framework" { frameworks.push( framework_path .file_stem() .unwrap() .to_string_lossy() .to_string(), ); } else { vendor_frameworks.push( relativize_path(tauri_dir.join(framework_path), config.project_dir()) .to_string_lossy() .to_string(), ); } } let metadata = AppleMetadata { supported: true, ios: ApplePlatform { cargo_args: Some(ios_options.args), features: Some(ios_options.features), frameworks: Some(frameworks), vendor_frameworks: Some(vendor_frameworks), ..Default::default() }, macos: Default::default(), }; set_var("TAURI_IOS_PROJECT_PATH", config.project_dir()); set_var("TAURI_IOS_APP_NAME", config.app().name()); Ok((config, metadata)) } fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> { let device_list = device::list_devices(env).map_err(|cause| { Error::GenericError(format!("Failed to detect connected iOS devices: {cause}")) })?; if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list .into_iter() .rev() .map(|d| { let score = best_match(t, d.name()).map_or(0, |m| m.score()); (d, score) }) .max_by_key(|(_, score)| *score) // we already checked the list is not empty .unwrap(); if score > MIN_DEVICE_MATCH_SCORE { device } else { crate::error::bail!("Could not find an iOS device matching {t}") } } else { let index = if device_list.len() > 1 { prompt::list( concat!("Detected ", "iOS", " devices"), device_list.iter(), "device", None, "Device", ) .context("failed to prompt for device")? } else { 0 }; device_list.into_iter().nth(index).unwrap() }; println!( "Detected connected device: {} with target {:?}", device, device.target().triple, ); Ok(device) } else { crate::error::bail!("No connected iOS devices detected") } } #[derive(Default, Deserialize)] struct InstalledRuntimesList { runtimes: Vec<InstalledRuntime>, } #[derive(Deserialize)] struct InstalledRuntime { name: String, } fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<device::Simulator> { let simulator_list = device::list_simulators(env).map_err(|cause| { Error::GenericError(format!( "Failed to detect connected iOS Simulator devices: {cause}" )) })?; if !simulator_list.is_empty() { let device = if let Some(t) = target { let (device, score) = simulator_list .into_iter() .rev() .map(|d| { let score = best_match(t, d.name()).map_or(0, |m| m.score()); (d, score) }) .max_by_key(|(_, score)| *score) // we already checked the list is not empty .unwrap(); if score > MIN_DEVICE_MATCH_SCORE { device } else { crate::error::bail!("Could not find an iOS Simulator matching {t}") } } else if simulator_list.len() > 1 { let index = prompt::list( concat!("Detected ", "iOS", " simulators"), simulator_list.iter(), "simulator", None, "Simulator", ) .context("failed to prompt for simulator")?; simulator_list.into_iter().nth(index).unwrap() } else { simulator_list.into_iter().next().unwrap() }; Ok(device) } else { log::warn!("No available iOS Simulator detected"); let install_ios = crate::helpers::prompts::confirm( "Would you like to install the latest iOS runtime?", Some(false), ) .unwrap_or_default(); if install_ios { duct::cmd("xcodebuild", ["-downloadPlatform", "iOS"]) .stdout_file(os_pipe::dup_stdout().unwrap()) .stderr_file(os_pipe::dup_stderr().unwrap()) .run() .map_err(|e| Error::CommandFailed { command: "xcodebuild -downloadPlatform iOS".to_string(), error: e, })?; return simulator_prompt(env, target); } crate::error::bail!("No available iOS Simulator detected") } } fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> { if let Ok(device) = connected_device_prompt(env, target) { Ok(device) } else { let simulator = simulator_prompt(env, target)?; log::info!("Starting simulator {}", simulator.name()); simulator .start_detached(env) .context("failed to start simulator")?; Ok(simulator.into()) } } fn ensure_ios_runtime_installed() -> Result<()> { let installed_platforms_json = duct::cmd("xcrun", ["simctl", "list", "runtimes", "--json"]) .read() .map_err(|e| Error::CommandFailed { command: "xcrun simctl list runtimes --json".to_string(), error: e, })?; let installed_platforms: InstalledRuntimesList = serde_json::from_str(&installed_platforms_json).unwrap_or_default(); if !installed_platforms .runtimes .iter() .any(|r| r.name.starts_with("iOS")) { log::warn!("iOS platform not installed"); let install_ios = crate::helpers::prompts::confirm( "Would you like to install the latest iOS runtime?", Some(false), ) .unwrap_or_default(); if install_ios { duct::cmd("xcodebuild", ["-downloadPlatform", "iOS"]) .stdout_file(os_pipe::dup_stdout().unwrap()) .stderr_file(os_pipe::dup_stderr().unwrap()) .run() .map_err(|e| Error::CommandFailed { command: "xcodebuild -downloadPlatform iOS".to_string(), error: e, })?; } else { crate::error::bail!("iOS platform not installed"); } } Ok(()) } fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { device_prompt(env, None).map(|device| device.target()).ok() } fn open_and_wait(config: &AppleConfig, env: &Env) -> ! { log::info!("Opening Xcode"); if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) { log::error!("{e}"); } loop { sleep(Duration::from_secs(24 * 60 * 60)); } } fn inject_resources(config: &AppleConfig, tauri_config: &TauriConfig) -> Result<()> { let asset_dir = config.project_dir().join(DEFAULT_ASSET_DIR); create_dir_all(&asset_dir).fs_context("failed to create asset directory", asset_dir.clone())?; let resources = match &tauri_config.bundle.resources { Some(BundleResources::List(paths)) => Some(ResourcePaths::new(paths.as_slice(), true)), Some(BundleResources::Map(map)) => Some(ResourcePaths::from_map(map, true)), None => None, }; if let Some(resources) = resources { for resource in resources.iter() { let resource = resource.context("failed to get resource")?; let dest = asset_dir.join(resource.target()); crate::helpers::fs::copy_file(resource.path(), dest)?; } } Ok(()) } pub fn signing_from_env() -> Result<( Option<tauri_macos_sign::Keychain>, Option<tauri_macos_sign::ProvisioningProfile>, )> { let keychain = match ( var_os("IOS_CERTIFICATE"), var_os("IOS_CERTIFICATE_PASSWORD"), ) { (Some(certificate), Some(certificate_password)) => { log::info!("Reading iOS certificates from "); tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password) .map(Some) .map_err(Box::new)? } (Some(_), None) => { log::warn!("The IOS_CERTIFICATE environment variable is set but not IOS_CERTIFICATE_PASSWORD. Ignoring the certificate..."); None } _ => None, }; let provisioning_profile = if let Some(provisioning_profile) = var_os("IOS_MOBILE_PROVISION") { tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile) .map(Some) .map_err(Box::new)? } else { if keychain.is_some() { log::warn!("You have provided an iOS certificate via environment variables but the IOS_MOBILE_PROVISION environment variable is not set. This will fail when signing unless the profile is set in your Xcode project."); } None }; Ok((keychain, provisioning_profile)) } pub struct ProjectConfig { pub code_sign_identity: Option<String>, pub team_id: Option<String>, pub provisioning_profile_uuid: Option<String>, } pub fn project_config( keychain: Option<&tauri_macos_sign::Keychain>, provisioning_profile: Option<&tauri_macos_sign::ProvisioningProfile>, ) -> Result<ProjectConfig> { Ok(ProjectConfig { code_sign_identity: keychain.map(|k| k.signing_identity()), team_id: keychain.and_then(|k| k.team_id().map(ToString::to_string)), provisioning_profile_uuid: provisioning_profile.and_then(|p| p.uuid().ok()), }) } pub fn load_pbxproj(config: &AppleConfig) -> Result<pbxproj::Pbxproj> { pbxproj::parse( config .project_dir() .join(format!("{}.xcodeproj", config.app().name())) .join("project.pbxproj"), ) } pub fn synchronize_project_config( config: &AppleConfig, tauri_config: &ConfigMetadata, pbxproj: &mut pbxproj::Pbxproj, export_options_plist: &mut plist::Dictionary, project_config: &ProjectConfig, debug: bool, ) -> Result<()> { let identifier = tauri_config.identifier.clone(); let product_name = tauri_config.product_name.clone(); let manual_signing = project_config.code_sign_identity.is_some() || project_config.provisioning_profile_uuid.is_some(); if let Some(xc_configuration_list) = pbxproj .xc_configuration_list .clone() .into_values() .find(|l| l.comment.contains("_iOS")) { for build_configuration_ref in xc_configuration_list.build_configurations { if manual_signing { pbxproj.set_build_settings(&build_configuration_ref.id, "CODE_SIGN_STYLE", "Manual"); } if let Some(team) = config.development_team() { let team = format!("\"{team}\""); pbxproj.set_build_settings(&build_configuration_ref.id, "DEVELOPMENT_TEAM", &team); } pbxproj.set_build_settings( &build_configuration_ref.id, "PRODUCT_BUNDLE_IDENTIFIER", &identifier, ); if let Some(product_name) = &product_name { pbxproj.set_build_settings( &build_configuration_ref.id, "PRODUCT_NAME", &format!("\"{product_name}\""), ); } if let Some(identity) = &project_config.code_sign_identity { let identity = format!("\"{identity}\""); pbxproj.set_build_settings(&build_configuration_ref.id, "CODE_SIGN_IDENTITY", &identity); pbxproj.set_build_settings( &build_configuration_ref.id, "\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"", &identity, ); } if let Some(id) = &project_config.team_id { let id = format!("\"{id}\""); pbxproj.set_build_settings(&build_configuration_ref.id, "DEVELOPMENT_TEAM", &id); pbxproj.set_build_settings( &build_configuration_ref.id, "\"DEVELOPMENT_TEAM[sdk=iphoneos*]\"", &id, ); } if let Some(profile_uuid) = &project_config.provisioning_profile_uuid { let profile_uuid = format!("\"{profile_uuid}\""); pbxproj.set_build_settings( &build_configuration_ref.id, "PROVISIONING_PROFILE_SPECIFIER", &profile_uuid, ); pbxproj.set_build_settings( &build_configuration_ref.id, "\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"", &profile_uuid, ); } } } let build_configuration = { if let Some(xc_configuration_list) = pbxproj .xc_configuration_list .clone() .into_values() .find(|l| l.comment.contains("_iOS")) { let mut configuration = None; let target = if debug { "debug" } else { "release" }; for build_configuration_ref in xc_configuration_list.build_configurations { if build_configuration_ref.comments.contains(target) { configuration = pbxproj .xc_build_configuration .get(&build_configuration_ref.id); break; } } configuration } else { None } }; if let Some(build_configuration) = build_configuration { if let Some(style) = build_configuration.get_build_setting("CODE_SIGN_STYLE") { export_options_plist.insert( "signingStyle".to_string(), style.value.to_lowercase().into(), ); } else { export_options_plist.insert("signingStyle".to_string(), "automatic".into()); } if manual_signing { if let Some(identity) = build_configuration .get_build_setting("\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"") .or_else(|| build_configuration.get_build_setting("CODE_SIGN_IDENTITY")) { export_options_plist.insert( "signingCertificate".to_string(), identity.value.trim_matches('"').into(), ); } let profile_uuid = project_config .provisioning_profile_uuid .clone() .or_else(|| { build_configuration .get_build_setting("\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"") .or_else(|| build_configuration.get_build_setting("PROVISIONING_PROFILE_SPECIFIER")) .map(|setting| setting.value.trim_matches('"').to_string()) }); if let Some(profile_uuid) = profile_uuid { let mut provisioning_profiles = plist::Dictionary::new(); provisioning_profiles.insert(config.app().identifier().to_string(), profile_uuid.into()); export_options_plist.insert( "provisioningProfiles".to_string(), provisioning_profiles.into(), ); } } if let Some(id) = build_configuration .get_build_setting("\"DEVELOPMENT_TEAM[sdk=iphoneos*]\"") .or_else(|| build_configuration.get_build_setting("DEVELOPMENT_TEAM")) { export_options_plist.insert("teamID".to_string(), id.value.trim_matches('"').into()); } } Ok(()) }