mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
* fix(tauri-cli): be more conservative to preserve behavior (#14804) * refactor(tauri-cli): move app path initialization into commands
699 lines
21 KiB
Rust
699 lines
21 KiB
Rust
// 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(())
|
|
}
|