From e56a9dd729d5b761bd7d7c4c95aa4403acc3e841 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 22 Aug 2022 21:59:17 -0300 Subject: [PATCH] refactor(cli): move mobile commands to their own module (#5005) --- tooling/cli/src/mobile/android.rs | 406 +--------------- .../mobile/android/android_studio_script.rs | 56 +++ tooling/cli/src/mobile/android/build.rs | 174 +++++++ tooling/cli/src/mobile/android/dev.rs | 163 +++++++ tooling/cli/src/mobile/android/open.rs | 12 + tooling/cli/src/mobile/ios.rs | 443 +----------------- tooling/cli/src/mobile/ios/build.rs | 147 ++++++ tooling/cli/src/mobile/ios/dev.rs | 146 ++++++ tooling/cli/src/mobile/ios/open.rs | 12 + tooling/cli/src/mobile/ios/xcode_script.rs | 141 ++++++ tooling/cli/src/mobile/mod.rs | 13 +- 11 files changed, 894 insertions(+), 819 deletions(-) create mode 100644 tooling/cli/src/mobile/android/android_studio_script.rs create mode 100644 tooling/cli/src/mobile/android/build.rs create mode 100644 tooling/cli/src/mobile/android/dev.rs create mode 100644 tooling/cli/src/mobile/android/open.rs create mode 100644 tooling/cli/src/mobile/ios/build.rs create mode 100644 tooling/cli/src/mobile/ios/dev.rs create mode 100644 tooling/cli/src/mobile/ios/open.rs create mode 100644 tooling/cli/src/mobile/ios/xcode_script.rs diff --git a/tooling/cli/src/mobile/android.rs b/tooling/cli/src/mobile/android.rs index dd0272624..2d9aba649 100644 --- a/tooling/cli/src/mobile/android.rs +++ b/tooling/cli/src/mobile/android.rs @@ -4,7 +4,7 @@ use cargo_mobile::{ android::{ - aab, adb, apk, + adb, config::{Config as AndroidConfig, Metadata as AndroidMetadata}, device::{Device, RunError}, env::{Env, Error as EnvError}, @@ -12,26 +12,22 @@ use cargo_mobile::{ }, config::Config, device::PromptError, - opts::{NoiseLevel, Profile}, os, - target::{call_for_targets_with_fallback, TargetTrait}, util::prompt, }; use clap::{Parser, Subcommand}; use super::{ ensure_init, get_config, - init::{command as init_command, Options as InitOptions}, - write_options, CliOptions, DevChild, Target as MobileTarget, -}; -use crate::{ - helpers::{config::get as get_tauri_config, flock}, - interface::{AppSettings, DevProcess, Interface, MobileOptions, Options as InterfaceOptions}, - Result, + init::{command as init_command, init_dot_cargo, Options as InitOptions}, + log_finished, Target as MobileTarget, }; +use crate::{helpers::config::get as get_tauri_config, Result}; -use std::{fmt::Write, path::PathBuf}; - +mod android_studio_script; +mod build; +mod dev; +mod open; pub(crate) mod project; #[derive(Debug, thiserror::Error)] @@ -73,122 +69,24 @@ pub struct Cli { command: Commands, } -#[derive(Debug, Parser)] -pub struct AndroidStudioScriptOptions { - /// Targets to build. - #[clap( - short, - long = "target", - multiple_occurrences(true), - multiple_values(true), - default_value = Target::DEFAULT_KEY, - value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) - )] - targets: Option>, - /// Builds with the release flag - #[clap(short, long)] - release: bool, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Android dev")] -pub struct DevOptions { - /// List of cargo features to activate - #[clap(short, long, multiple_occurrences(true), multiple_values(true))] - pub features: Option>, - /// Exit on panic - #[clap(short, long)] - exit_on_panic: bool, - /// JSON string or path to JSON file to merge with tauri.conf.json - #[clap(short, long)] - pub config: Option, - /// Disable the file watcher - #[clap(long)] - pub no_watch: bool, - /// Open Android Studio instead of trying to run on a connected device - #[clap(short, long)] - pub open: bool, -} - -impl From for crate::dev::Options { - fn from(options: DevOptions) -> Self { - Self { - runner: None, - target: None, - features: options.features, - exit_on_panic: options.exit_on_panic, - config: options.config, - release_mode: false, - args: Vec::new(), - no_watch: options.no_watch, - } - } -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Android build")] -pub struct BuildOptions { - /// Builds with the debug flag - #[clap(short, long)] - pub debug: bool, - /// Which targets to build (all by default). - #[clap( - short, - long = "target", - multiple_occurrences(true), - multiple_values(true), - value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) - )] - pub targets: Option>, - /// List of cargo features to activate - #[clap(short, long, multiple_occurrences(true), multiple_values(true))] - pub features: Option>, - /// JSON string or path to JSON file to merge with tauri.conf.json - #[clap(short, long)] - pub config: Option, - /// Whether to split the APKs and AABs per ABIs. - #[clap(long)] - pub split_per_abi: bool, - /// Build APKs. - #[clap(long)] - pub apk: bool, - /// Build AABs. - #[clap(long)] - pub aab: bool, -} - -impl From for crate::build::Options { - fn from(options: BuildOptions) -> Self { - Self { - runner: None, - debug: options.debug, - target: None, - features: options.features, - bundles: None, - config: options.config, - args: Vec::new(), - } - } -} - #[derive(Subcommand)] enum Commands { Init(InitOptions), /// Open project in Android Studio Open, - Dev(DevOptions), - Build(BuildOptions), + Dev(dev::Options), + Build(build::Options), #[clap(hide(true))] - AndroidStudioScript(AndroidStudioScriptOptions), + AndroidStudioScript(android_studio_script::Options), } pub fn command(cli: Cli) -> Result<()> { match cli.command { Commands::Init(options) => init_command(options, MobileTarget::Android)?, - Commands::Open => open()?, - Commands::Dev(options) => dev(options)?, - Commands::Build(options) => build(options)?, - Commands::AndroidStudioScript(options) => android_studio_script(options)?, + Commands::Open => open::command()?, + Commands::Dev(options) => dev::command(options)?, + Commands::Build(options) => build::command(options)?, + Commands::AndroidStudioScript(options) => android_studio_script::command(options)?, } Ok(()) @@ -235,280 +133,6 @@ fn device_prompt<'a>(env: &'_ Env) -> Result, PromptError(targets: Vec) -> Result>, Error> { - if targets.is_empty() { - Ok(Target::all().iter().map(|t| t.1).collect()) - } else { - let mut outs = Vec::new(); - - let possible_targets = Target::all() - .keys() - .map(|key| key.to_string()) - .collect::>() - .join(","); - - for t in targets { - let target = Target::for_name(&t).ok_or_else(|| { - Error::TargetInvalid(format!( - "Target {} is invalid; the possible targets are {}", - t, possible_targets - )) - })?; - outs.push(target); - } - Ok(outs) - } -} - -fn build(options: BuildOptions) -> Result<()> { - with_config(|root_conf, config, _metadata| { - ensure_init(config.project_dir(), MobileTarget::Android) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - - let env = Env::new().map_err(Error::EnvInitFailed)?; - super::init::init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; - - run_build(options, config, env).map_err(|e| Error::BuildFailed(e.to_string())) - }) - .map_err(Into::into) -} - -fn run_build(mut options: BuildOptions, config: &AndroidConfig, env: Env) -> Result<()> { - let profile = if options.debug { - Profile::Debug - } else { - Profile::Release - }; - let noise_level = NoiseLevel::Polite; - - if !(options.apk || options.aab) { - // if the user didn't specify the format to build, we'll do both - options.apk = true; - options.aab = true; - } - - let bundle_identifier = { - let tauri_config = get_tauri_config(None)?; - let tauri_config_guard = tauri_config.lock().unwrap(); - let tauri_config_ = tauri_config_guard.as_ref().unwrap(); - tauri_config_.tauri.bundle.identifier.clone() - }; - - let mut build_options = options.clone().into(); - let interface = crate::build::setup(&mut build_options)?; - - let app_settings = interface.app_settings(); - let bin_path = app_settings.app_binary_path(&InterfaceOptions { - debug: build_options.debug, - ..Default::default() - })?; - let out_dir = bin_path.parent().unwrap(); - let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?; - - let cli_options = CliOptions { - features: build_options.features.clone(), - args: build_options.args.clone(), - vars: Default::default(), - }; - write_options(cli_options, &bundle_identifier, MobileTarget::Android)?; - - options - .features - .get_or_insert(Vec::new()) - .push("custom-protocol".into()); - - let apk_outputs = if options.apk { - apk::build( - config, - &env, - noise_level, - profile, - get_targets_or_all(Vec::new())?, - options.split_per_abi, - )? - } else { - Vec::new() - }; - - let aab_outputs = if options.aab { - aab::build( - config, - &env, - noise_level, - profile, - get_targets_or_all(Vec::new())?, - options.split_per_abi, - )? - } else { - Vec::new() - }; - - log_finished(apk_outputs, "APK"); - log_finished(aab_outputs, "AAB"); - - Ok(()) -} - -fn log_finished(outputs: Vec, kind: &str) { - if !outputs.is_empty() { - let mut printable_paths = String::new(); - for path in &outputs { - writeln!(printable_paths, " {}", path.display()).unwrap(); - } - - log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths); - } -} - -fn dev(options: DevOptions) -> Result<()> { - with_config(|root_conf, config, metadata| { - ensure_init(config.project_dir(), MobileTarget::Android) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - run_dev(options, root_conf, config, metadata).map_err(|e| Error::DevFailed(e.to_string())) - }) - .map_err(Into::into) -} - -fn run_dev( - options: DevOptions, - root_conf: &Config, - config: &AndroidConfig, - metadata: &AndroidMetadata, -) -> Result<()> { - let mut dev_options = options.clone().into(); - let mut interface = crate::dev::setup(&mut dev_options)?; - - let bundle_identifier = { - let tauri_config = get_tauri_config(None)?; - let tauri_config_guard = tauri_config.lock().unwrap(); - let tauri_config_ = tauri_config_guard.as_ref().unwrap(); - tauri_config_.tauri.bundle.identifier.clone() - }; - - let app_settings = interface.app_settings(); - let bin_path = app_settings.app_binary_path(&InterfaceOptions { - debug: !dev_options.release_mode, - ..Default::default() - })?; - let out_dir = bin_path.parent().unwrap(); - let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?; - - let open = options.open; - interface.mobile_dev( - MobileOptions { - debug: true, - features: options.features, - args: Vec::new(), - config: options.config, - no_watch: options.no_watch, - }, - |options| { - let cli_options = CliOptions { - features: options.features.clone(), - args: options.args.clone(), - vars: Default::default(), - }; - write_options(cli_options, &bundle_identifier, MobileTarget::Android)?; - - if open { - open_dev(config) - } else { - match run(options, root_conf, config, metadata) { - Ok(c) => Ok(Box::new(c) as Box), - Err(Error::FailedToPromptForDevice(e)) => { - log::error!("{}", e); - open_dev(config) - } - Err(e) => Err(e.into()), - } - } - }, - ) -} - -fn open_dev(config: &AndroidConfig) -> ! { - log::info!("Opening Android Studio"); - if let Err(e) = os::open_file_with("Android Studio", config.project_dir()) { - log::error!("{}", e); - } - loop { - std::thread::sleep(std::time::Duration::from_secs(24 * 60 * 60)); - } -} - -fn open() -> Result<()> { - with_config(|_, config, _metadata| { - ensure_init(config.project_dir(), MobileTarget::Android) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - os::open_file_with("Android Studio", config.project_dir()).map_err(Error::OpenFailed) - }) - .map_err(Into::into) -} - -fn run( - options: MobileOptions, - root_conf: &Config, - config: &AndroidConfig, - metadata: &AndroidMetadata, -) -> Result { - let profile = if options.debug { - Profile::Debug - } else { - Profile::Release - }; - let noise_level = NoiseLevel::Polite; - - let build_app_bundle = metadata.asset_packs().is_some(); - - let env = Env::new().map_err(Error::EnvInitFailed)?; - super::init::init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; - - device_prompt(&env) - .map_err(Error::FailedToPromptForDevice)? - .run( - config, - &env, - noise_level, - profile, - None, - build_app_bundle, - false, - ".MainActivity".into(), - ) - .map(|c| DevChild(Some(c))) - .map_err(Error::RunFailed) -} - fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { device_prompt(env).map(|device| device.target()).ok() } - -fn android_studio_script(options: AndroidStudioScriptOptions) -> Result<()> { - let profile = if options.release { - Profile::Release - } else { - Profile::Debug - }; - let noise_level = NoiseLevel::Polite; - - with_config(|root_conf, config, metadata| { - ensure_init(config.project_dir(), MobileTarget::Android) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - - let env = Env::new().map_err(Error::EnvInitFailed)?; - super::init::init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; - - call_for_targets_with_fallback( - options.targets.unwrap_or_default().iter(), - &detect_target_ok, - &env, - |target: &Target| { - target - .build(config, metadata, &env, noise_level, true, profile) - .map_err(Error::AndroidStudioScriptFailed) - }, - ) - .map_err(|e| Error::TargetInvalid(e.to_string()))? - }) - .map_err(Into::into) -} diff --git a/tooling/cli/src/mobile/android/android_studio_script.rs b/tooling/cli/src/mobile/android/android_studio_script.rs new file mode 100644 index 000000000..d78213941 --- /dev/null +++ b/tooling/cli/src/mobile/android/android_studio_script.rs @@ -0,0 +1,56 @@ +use super::{detect_target_ok, ensure_init, init_dot_cargo, with_config, Error, MobileTarget}; +use crate::Result; +use clap::Parser; + +use cargo_mobile::{ + android::{env::Env, target::Target}, + opts::{NoiseLevel, Profile}, + target::{call_for_targets_with_fallback, TargetTrait}, +}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Targets to build. + #[clap( + short, + long = "target", + multiple_occurrences(true), + multiple_values(true), + default_value = Target::DEFAULT_KEY, + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + targets: Option>, + /// Builds with the release flag + #[clap(short, long)] + release: bool, +} + +pub fn command(options: Options) -> Result<()> { + let profile = if options.release { + Profile::Release + } else { + Profile::Debug + }; + let noise_level = NoiseLevel::Polite; + + with_config(|root_conf, config, metadata| { + ensure_init(config.project_dir(), MobileTarget::Android) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + + let env = Env::new().map_err(Error::EnvInitFailed)?; + init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; + + call_for_targets_with_fallback( + options.targets.unwrap_or_default().iter(), + &detect_target_ok, + &env, + |target: &Target| { + target + .build(config, metadata, &env, noise_level, true, profile) + .map_err(Error::AndroidStudioScriptFailed) + }, + ) + .map_err(|e| Error::TargetInvalid(e.to_string()))? + }) + .map_err(Into::into) +} diff --git a/tooling/cli/src/mobile/android/build.rs b/tooling/cli/src/mobile/android/build.rs new file mode 100644 index 000000000..a6fbeb1eb --- /dev/null +++ b/tooling/cli/src/mobile/android/build.rs @@ -0,0 +1,174 @@ +use super::{ensure_init, init_dot_cargo, log_finished, with_config, Error, MobileTarget}; +use crate::{ + helpers::{config::get as get_tauri_config, flock}, + interface::{AppSettings, Interface, Options as InterfaceOptions}, + mobile::{write_options, CliOptions}, + Result, +}; +use clap::Parser; + +use cargo_mobile::{ + android::{aab, apk, config::Config as AndroidConfig, env::Env, target::Target}, + opts::{NoiseLevel, Profile}, + target::TargetTrait, +}; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Android build")] +pub struct Options { + /// Builds with the debug flag + #[clap(short, long)] + pub debug: bool, + /// Which targets to build (all by default). + #[clap( + short, + long = "target", + multiple_occurrences(true), + multiple_values(true), + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + pub targets: Option>, + /// List of cargo features to activate + #[clap(short, long, multiple_occurrences(true), multiple_values(true))] + pub features: Option>, + /// JSON string or path to JSON file to merge with tauri.conf.json + #[clap(short, long)] + pub config: Option, + /// Whether to split the APKs and AABs per ABIs. + #[clap(long)] + pub split_per_abi: bool, + /// Build APKs. + #[clap(long)] + pub apk: bool, + /// Build AABs. + #[clap(long)] + pub aab: bool, +} + +impl From for crate::build::Options { + fn from(options: Options) -> Self { + Self { + runner: None, + debug: options.debug, + target: None, + features: options.features, + bundles: None, + config: options.config, + args: Vec::new(), + } + } +} + +pub fn command(options: Options) -> Result<()> { + with_config(|root_conf, config, _metadata| { + ensure_init(config.project_dir(), MobileTarget::Android) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + + let env = Env::new().map_err(Error::EnvInitFailed)?; + init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; + + run_build(options, config, env).map_err(|e| Error::BuildFailed(e.to_string())) + }) + .map_err(Into::into) +} + +fn run_build(mut options: Options, config: &AndroidConfig, env: Env) -> Result<()> { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + let noise_level = NoiseLevel::Polite; + + if !(options.apk || options.aab) { + // if the user didn't specify the format to build, we'll do both + options.apk = true; + options.aab = true; + } + + let bundle_identifier = { + let tauri_config = get_tauri_config(None)?; + let tauri_config_guard = tauri_config.lock().unwrap(); + let tauri_config_ = tauri_config_guard.as_ref().unwrap(); + tauri_config_.tauri.bundle.identifier.clone() + }; + + let mut build_options = options.clone().into(); + let interface = crate::build::setup(&mut build_options)?; + + let app_settings = interface.app_settings(); + let bin_path = app_settings.app_binary_path(&InterfaceOptions { + debug: build_options.debug, + ..Default::default() + })?; + let out_dir = bin_path.parent().unwrap(); + let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?; + + let cli_options = CliOptions { + features: build_options.features.clone(), + args: build_options.args.clone(), + vars: Default::default(), + }; + write_options(cli_options, &bundle_identifier, MobileTarget::Android)?; + + options + .features + .get_or_insert(Vec::new()) + .push("custom-protocol".into()); + + let apk_outputs = if options.apk { + apk::build( + config, + &env, + noise_level, + profile, + get_targets_or_all(Vec::new())?, + options.split_per_abi, + )? + } else { + Vec::new() + }; + + let aab_outputs = if options.aab { + aab::build( + config, + &env, + noise_level, + profile, + get_targets_or_all(Vec::new())?, + options.split_per_abi, + )? + } else { + Vec::new() + }; + + log_finished(apk_outputs, "APK"); + log_finished(aab_outputs, "AAB"); + + Ok(()) +} + +fn get_targets_or_all<'a>(targets: Vec) -> Result>, Error> { + if targets.is_empty() { + Ok(Target::all().iter().map(|t| t.1).collect()) + } else { + let mut outs = Vec::new(); + + let possible_targets = Target::all() + .keys() + .map(|key| key.to_string()) + .collect::>() + .join(","); + + for t in targets { + let target = Target::for_name(&t).ok_or_else(|| { + Error::TargetInvalid(format!( + "Target {} is invalid; the possible targets are {}", + t, possible_targets + )) + })?; + outs.push(target); + } + Ok(outs) + } +} diff --git a/tooling/cli/src/mobile/android/dev.rs b/tooling/cli/src/mobile/android/dev.rs new file mode 100644 index 000000000..f6e1033d2 --- /dev/null +++ b/tooling/cli/src/mobile/android/dev.rs @@ -0,0 +1,163 @@ +use super::{device_prompt, ensure_init, init_dot_cargo, with_config, Error, MobileTarget}; +use crate::{ + helpers::{config::get as get_tauri_config, flock}, + interface::{AppSettings, Interface, MobileOptions, Options as InterfaceOptions}, + mobile::{write_options, CliOptions, DevChild, DevProcess}, + Result, +}; +use clap::Parser; + +use cargo_mobile::{ + android::{ + config::{Config as AndroidConfig, Metadata as AndroidMetadata}, + env::Env, + }, + config::Config, + opts::{NoiseLevel, Profile}, + os, +}; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Android dev")] +pub struct Options { + /// List of cargo features to activate + #[clap(short, long, multiple_occurrences(true), multiple_values(true))] + pub features: Option>, + /// Exit on panic + #[clap(short, long)] + exit_on_panic: bool, + /// JSON string or path to JSON file to merge with tauri.conf.json + #[clap(short, long)] + pub config: Option, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Open Android Studio instead of trying to run on a connected device + #[clap(short, long)] + pub open: bool, +} + +impl From for crate::dev::Options { + fn from(options: Options) -> Self { + Self { + runner: None, + target: None, + features: options.features, + exit_on_panic: options.exit_on_panic, + config: options.config, + release_mode: false, + args: Vec::new(), + no_watch: options.no_watch, + } + } +} + +pub fn command(options: Options) -> Result<()> { + with_config(|root_conf, config, metadata| { + ensure_init(config.project_dir(), MobileTarget::Android) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + run_dev(options, root_conf, config, metadata).map_err(|e| Error::DevFailed(e.to_string())) + }) + .map_err(Into::into) +} + +fn run_dev( + options: Options, + root_conf: &Config, + config: &AndroidConfig, + metadata: &AndroidMetadata, +) -> Result<()> { + let mut dev_options = options.clone().into(); + let mut interface = crate::dev::setup(&mut dev_options)?; + + let bundle_identifier = { + let tauri_config = get_tauri_config(None)?; + let tauri_config_guard = tauri_config.lock().unwrap(); + let tauri_config_ = tauri_config_guard.as_ref().unwrap(); + tauri_config_.tauri.bundle.identifier.clone() + }; + + let app_settings = interface.app_settings(); + let bin_path = app_settings.app_binary_path(&InterfaceOptions { + debug: !dev_options.release_mode, + ..Default::default() + })?; + let out_dir = bin_path.parent().unwrap(); + let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?; + + let open = options.open; + interface.mobile_dev( + MobileOptions { + debug: true, + features: options.features, + args: Vec::new(), + config: options.config, + no_watch: options.no_watch, + }, + |options| { + let cli_options = CliOptions { + features: options.features.clone(), + args: options.args.clone(), + vars: Default::default(), + }; + write_options(cli_options, &bundle_identifier, MobileTarget::Android)?; + + if open { + open_dev(config) + } else { + match run(options, root_conf, config, metadata) { + Ok(c) => Ok(Box::new(c) as Box), + Err(Error::FailedToPromptForDevice(e)) => { + log::error!("{}", e); + open_dev(config) + } + Err(e) => Err(e.into()), + } + } + }, + ) +} + +fn open_dev(config: &AndroidConfig) -> ! { + log::info!("Opening Android Studio"); + if let Err(e) = os::open_file_with("Android Studio", config.project_dir()) { + log::error!("{}", e); + } + loop { + std::thread::sleep(std::time::Duration::from_secs(24 * 60 * 60)); + } +} + +fn run( + options: MobileOptions, + root_conf: &Config, + config: &AndroidConfig, + metadata: &AndroidMetadata, +) -> Result { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + let noise_level = NoiseLevel::Polite; + + let build_app_bundle = metadata.asset_packs().is_some(); + + let env = Env::new().map_err(Error::EnvInitFailed)?; + init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?; + + device_prompt(&env) + .map_err(Error::FailedToPromptForDevice)? + .run( + config, + &env, + noise_level, + profile, + None, + build_app_bundle, + false, + ".MainActivity".into(), + ) + .map(|c| DevChild(Some(c))) + .map_err(Error::RunFailed) +} diff --git a/tooling/cli/src/mobile/android/open.rs b/tooling/cli/src/mobile/android/open.rs new file mode 100644 index 000000000..e58fba14a --- /dev/null +++ b/tooling/cli/src/mobile/android/open.rs @@ -0,0 +1,12 @@ +use super::{ensure_init, with_config, Error, MobileTarget}; +use crate::Result; +use cargo_mobile::os; + +pub fn command() -> Result<()> { + with_config(|_, config, _metadata| { + ensure_init(config.project_dir(), MobileTarget::Android) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + os::open_file_with("Android Studio", config.project_dir()).map_err(Error::OpenFailed) + }) + .map_err(Into::into) +} diff --git a/tooling/cli/src/mobile/ios.rs b/tooling/cli/src/mobile/ios.rs index a8ab6c8ff..388e9b318 100644 --- a/tooling/cli/src/mobile/ios.rs +++ b/tooling/cli/src/mobile/ios.rs @@ -12,28 +12,25 @@ use cargo_mobile::{ config::Config, device::PromptError, env::{Env, Error as EnvError}, - opts::{NoiseLevel, Profile}, - os, - target::{call_for_targets_with_fallback, TargetInvalid, TargetTrait}, - util, + os, util, util::prompt, }; use clap::{Parser, Subcommand}; use super::{ ensure_init, env_vars, get_config, - init::{command as init_command, Options as InitOptions}, - write_options, CliOptions, DevChild, Target as MobileTarget, -}; -use crate::{ - helpers::{config::get as get_tauri_config, flock}, - interface::{AppSettings, DevProcess, Interface, MobileOptions, Options as InterfaceOptions}, - Result, + init::{command as init_command, init_dot_cargo, Options as InitOptions}, + log_finished, Target as MobileTarget, }; +use crate::{helpers::config::get as get_tauri_config, Result}; -use std::{collections::HashMap, ffi::OsStr, fmt::Write, fs, path::PathBuf}; +use std::path::PathBuf; +mod build; +mod dev; +mod open; pub(crate) mod project; +mod xcode_script; #[derive(Debug, thiserror::Error)] enum Error { @@ -84,121 +81,23 @@ pub struct Cli { command: Commands, } -#[derive(Debug, Parser)] -pub struct XcodeScriptOptions { - /// Value of `PLATFORM_DISPLAY_NAME` env var - #[clap(long)] - platform: String, - /// Value of `SDKROOT` env var - #[clap(long)] - sdk_root: PathBuf, - /// Value of `CONFIGURATION` env var - #[clap(long)] - configuration: String, - /// Value of `FORCE_COLOR` env var - #[clap(long)] - force_color: bool, - /// Value of `ARCHS` env var - #[clap(index = 1, required = true)] - arches: Vec, -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "iOS dev")] -pub struct DevOptions { - /// List of cargo features to activate - #[clap(short, long, multiple_occurrences(true), multiple_values(true))] - pub features: Option>, - /// Exit on panic - #[clap(short, long)] - exit_on_panic: bool, - /// JSON string or path to JSON file to merge with tauri.conf.json - #[clap(short, long)] - pub config: Option, - /// Run the code in release mode - #[clap(long = "release")] - pub release_mode: bool, - /// Disable the file watcher - #[clap(long)] - pub no_watch: bool, - /// Open Xcode instead of trying to run on a connected device - #[clap(short, long)] - pub open: bool, -} - -impl From for crate::dev::Options { - fn from(options: DevOptions) -> Self { - Self { - runner: None, - target: None, - features: options.features, - exit_on_panic: options.exit_on_panic, - config: options.config, - release_mode: options.release_mode, - args: Vec::new(), - no_watch: options.no_watch, - } - } -} - -#[derive(Debug, Clone, Parser)] -#[clap(about = "Android build")] -pub struct BuildOptions { - /// Builds with the debug flag - #[clap(short, long)] - pub debug: bool, - /// Which targets to build. - #[clap( - short, - long = "target", - multiple_occurrences(true), - multiple_values(true), - default_value = Target::DEFAULT_KEY, - value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) - )] - pub targets: Vec, - /// List of cargo features to activate - #[clap(short, long, multiple_occurrences(true), multiple_values(true))] - pub features: Option>, - /// JSON string or path to JSON file to merge with tauri.conf.json - #[clap(short, long)] - pub config: Option, - /// Build number to append to the app version. - #[clap(long)] - pub build_number: Option, -} - -impl From for crate::build::Options { - fn from(options: BuildOptions) -> Self { - Self { - runner: None, - debug: options.debug, - target: None, - features: options.features, - bundles: None, - config: options.config, - args: Vec::new(), - } - } -} - #[derive(Subcommand)] enum Commands { Init(InitOptions), Open, - Dev(DevOptions), - Build(BuildOptions), + Dev(dev::Options), + Build(build::Options), #[clap(hide(true))] - XcodeScript(XcodeScriptOptions), + XcodeScript(xcode_script::Options), } pub fn command(cli: Cli) -> Result<()> { match cli.command { Commands::Init(options) => init_command(options, MobileTarget::Ios)?, - Commands::Open => open()?, - Commands::Dev(options) => dev(options)?, - Commands::Build(options) => build(options)?, - Commands::XcodeScript(options) => xcode_script(options)?, + Commands::Open => open::command()?, + Commands::Dev(options) => dev::command(options)?, + Commands::Build(options) => build::command(options)?, + Commands::XcodeScript(options) => xcode_script::command(options)?, } Ok(()) @@ -255,313 +154,3 @@ fn device_prompt<'a>(env: &'_ Env) -> Result, PromptError(env: &Env) -> Option<&'a Target<'a>> { device_prompt(env).map(|device| device.target()).ok() } - -fn build(options: BuildOptions) -> Result<()> { - with_config(|root_conf, config, _metadata| { - ensure_init(config.project_dir(), MobileTarget::Ios) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - - let env = env()?; - super::init::init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; - - run_build(options, config, env).map_err(|e| Error::BuildFailed(e.to_string())) - }) - .map_err(Into::into) -} - -fn run_build(mut options: BuildOptions, config: &AppleConfig, env: Env) -> Result<()> { - let profile = if options.debug { - Profile::Debug - } else { - Profile::Release - }; - let noise_level = NoiseLevel::Polite; - - let bundle_identifier = { - let tauri_config = get_tauri_config(None)?; - let tauri_config_guard = tauri_config.lock().unwrap(); - let tauri_config_ = tauri_config_guard.as_ref().unwrap(); - tauri_config_.tauri.bundle.identifier.clone() - }; - - let mut build_options = options.clone().into(); - let interface = crate::build::setup(&mut build_options)?; - - let app_settings = interface.app_settings(); - let bin_path = app_settings.app_binary_path(&InterfaceOptions { - debug: build_options.debug, - ..Default::default() - })?; - let out_dir = bin_path.parent().unwrap(); - let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?; - - let cli_options = CliOptions { - features: build_options.features.clone(), - args: build_options.args.clone(), - vars: Default::default(), - }; - write_options(cli_options, &bundle_identifier, MobileTarget::Ios)?; - - options - .features - .get_or_insert(Vec::new()) - .push("custom-protocol".into()); - - let mut out_files = Vec::new(); - - call_for_targets_with_fallback( - options.targets.iter(), - &detect_target_ok, - &env, - |target: &Target| { - let mut app_version = config.bundle_version().clone(); - if let Some(build_number) = options.build_number { - app_version.push_extra(build_number); - } - - target.build(config, &env, noise_level, profile)?; - target.archive(config, &env, noise_level, profile, Some(app_version))?; - target.export(config, &env, noise_level)?; - - if let Ok(ipa_path) = config.ipa_path() { - let out_dir = config.export_dir().join(target.arch); - fs::create_dir_all(&out_dir)?; - let path = out_dir.join(ipa_path.file_name().unwrap()); - fs::rename(&ipa_path, &path)?; - out_files.push(path); - } - - anyhow::Result::Ok(()) - }, - ) - .map_err(|e: TargetInvalid| Error::TargetInvalid(e.to_string()))? - .map_err(|e: anyhow::Error| e)?; - - log_finished(out_files, "IPA"); - - Ok(()) -} - -fn log_finished(outputs: Vec, kind: &str) { - if !outputs.is_empty() { - let mut printable_paths = String::new(); - for path in &outputs { - writeln!(printable_paths, " {}", path.display()).unwrap(); - } - - log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths); - } -} - -fn dev(options: DevOptions) -> Result<()> { - with_config(|root_conf, config, _metadata| { - ensure_init(config.project_dir(), MobileTarget::Ios) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - run_dev(options, root_conf, config).map_err(|e| Error::DevFailed(e.to_string())) - }) - .map_err(Into::into) -} - -fn run_dev(options: DevOptions, root_conf: &Config, config: &AppleConfig) -> Result<()> { - let mut dev_options = options.clone().into(); - let mut interface = crate::dev::setup(&mut dev_options)?; - - let bundle_identifier = { - let tauri_config = - get_tauri_config(None).map_err(|e| Error::InvalidTauriConfig(e.to_string()))?; - let tauri_config_guard = tauri_config.lock().unwrap(); - let tauri_config_ = tauri_config_guard.as_ref().unwrap(); - tauri_config_.tauri.bundle.identifier.clone() - }; - - let app_settings = interface.app_settings(); - let bin_path = app_settings.app_binary_path(&InterfaceOptions { - debug: !dev_options.release_mode, - ..Default::default() - })?; - let out_dir = bin_path.parent().unwrap(); - let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?; - - let open = options.open; - interface.mobile_dev( - MobileOptions { - debug: true, - features: options.features, - args: Vec::new(), - config: options.config, - no_watch: options.no_watch, - }, - |options| { - let cli_options = CliOptions { - features: options.features.clone(), - args: options.args.clone(), - vars: Default::default(), - }; - write_options(cli_options, &bundle_identifier, MobileTarget::Ios)?; - if open { - open_dev(config) - } else { - match run(options, root_conf, config) { - Ok(c) => Ok(Box::new(c) as Box), - Err(Error::FailedToPromptForDevice(e)) => { - log::error!("{}", e); - open_dev(config) - } - Err(e) => Err(e.into()), - } - } - }, - ) -} - -fn open_dev(config: &AppleConfig) -> ! { - log::info!("Opening Xcode"); - if let Err(e) = os::open_file_with("Xcode", config.project_dir()) { - log::error!("{}", e); - } - loop { - std::thread::sleep(std::time::Duration::from_secs(24 * 60 * 60)); - } -} - -fn open() -> Result<()> { - with_config(|_, config, _metadata| { - ensure_init(config.project_dir(), MobileTarget::Ios) - .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; - os::open_file_with("Xcode", config.project_dir()).map_err(Error::OpenFailed) - }) - .map_err(Into::into) -} - -fn run( - options: MobileOptions, - root_conf: &Config, - config: &AppleConfig, -) -> Result { - let profile = if options.debug { - Profile::Debug - } else { - Profile::Release - }; - let noise_level = NoiseLevel::Polite; - - let env = env()?; - super::init::init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; - - device_prompt(&env) - .map_err(Error::FailedToPromptForDevice)? - .run(config, &env, noise_level, false, profile) - .map(|c| DevChild(Some(c))) - .map_err(Error::RunFailed) -} - -fn xcode_script(options: XcodeScriptOptions) -> Result<()> { - fn macos_from_platform(platform: &str) -> bool { - platform == "macOS" - } - - fn profile_from_configuration(configuration: &str) -> Profile { - if configuration == "release" { - Profile::Release - } else { - Profile::Debug - } - } - - let profile = profile_from_configuration(&options.configuration); - let macos = macos_from_platform(&options.platform); - let noise_level = NoiseLevel::Polite; - - with_config(|root_conf, config, metadata| { - let env = env()?; - super::init::init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; - // The `PATH` env var Xcode gives us is missing any additions - // made by the user's profile, so we'll manually add cargo's - // `PATH`. - let env = env.prepend_to_path( - util::home_dir() - .map_err(Error::NoHomeDir)? - .join(".cargo/bin"), - ); - - if !options.sdk_root.is_dir() { - return Err(Error::SdkRootInvalid { - sdk_root: options.sdk_root, - }); - } - let include_dir = options.sdk_root.join("usr/include"); - if !include_dir.is_dir() { - return Err(Error::IncludeDirInvalid { include_dir }); - } - - let mut host_env = HashMap::<&str, &OsStr>::new(); - - // Host flags that are used by build scripts - let (macos_isysroot, library_path) = { - let macos_sdk_root = options - .sdk_root - .join("../../../../MacOSX.platform/Developer/SDKs/MacOSX.sdk"); - if !macos_sdk_root.is_dir() { - return Err(Error::MacosSdkRootInvalid { macos_sdk_root }); - } - ( - format!("-isysroot {}", macos_sdk_root.display()), - format!("{}/usr/lib", macos_sdk_root.display()), - ) - }; - host_env.insert("MAC_FLAGS", macos_isysroot.as_ref()); - host_env.insert("CFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); - host_env.insert("CXXFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); - - host_env.insert( - "OBJC_INCLUDE_PATH_x86_64_apple_darwin", - include_dir.as_os_str(), - ); - - host_env.insert("RUST_BACKTRACE", "1".as_ref()); - - let macos_target = Target::macos(); - - let isysroot = format!("-isysroot {}", options.sdk_root.display()); - - for arch in options.arches { - // Set target-specific flags - let triple = match arch.as_str() { - "arm64" => "aarch64_apple_ios", - "x86_64" => "x86_64_apple_ios", - _ => return Err(Error::ArchInvalid { arch }), - }; - let cflags = format!("CFLAGS_{}", triple); - let cxxflags = format!("CFLAGS_{}", triple); - let objc_include_path = format!("OBJC_INCLUDE_PATH_{}", triple); - let mut target_env = host_env.clone(); - target_env.insert(cflags.as_ref(), isysroot.as_ref()); - target_env.insert(cxxflags.as_ref(), isysroot.as_ref()); - target_env.insert(objc_include_path.as_ref(), include_dir.as_ref()); - // Prevents linker errors in build scripts and proc macros: - // https://github.com/signalapp/libsignal-client/commit/02899cac643a14b2ced7c058cc15a836a2165b6d - target_env.insert("LIBRARY_PATH", library_path.as_ref()); - - let target = if macos { - &macos_target - } else { - Target::for_arch(&arch).ok_or_else(|| Error::ArchInvalid { - arch: arch.to_owned(), - })? - }; - target - .compile_lib( - config, - metadata, - noise_level, - true, - profile, - &env, - target_env, - ) - .map_err(Error::CompileLibFailed)?; - } - Ok(()) - }) - .map_err(Into::into) -} diff --git a/tooling/cli/src/mobile/ios/build.rs b/tooling/cli/src/mobile/ios/build.rs new file mode 100644 index 000000000..9eb126ff3 --- /dev/null +++ b/tooling/cli/src/mobile/ios/build.rs @@ -0,0 +1,147 @@ +use super::{ + detect_target_ok, ensure_init, env, init_dot_cargo, log_finished, with_config, Error, + MobileTarget, +}; +use crate::{ + helpers::{config::get as get_tauri_config, flock}, + interface::{AppSettings, Interface, Options as InterfaceOptions}, + mobile::{write_options, CliOptions}, + Result, +}; +use clap::Parser; + +use cargo_mobile::{ + apple::{config::Config as AppleConfig, target::Target}, + env::Env, + opts::{NoiseLevel, Profile}, + target::{call_for_targets_with_fallback, TargetInvalid, TargetTrait}, +}; + +use std::fs; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "Android build")] +pub struct Options { + /// Builds with the debug flag + #[clap(short, long)] + pub debug: bool, + /// Which targets to build. + #[clap( + short, + long = "target", + multiple_occurrences(true), + multiple_values(true), + default_value = Target::DEFAULT_KEY, + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + pub targets: Vec, + /// List of cargo features to activate + #[clap(short, long, multiple_occurrences(true), multiple_values(true))] + pub features: Option>, + /// JSON string or path to JSON file to merge with tauri.conf.json + #[clap(short, long)] + pub config: Option, + /// Build number to append to the app version. + #[clap(long)] + pub build_number: Option, +} + +impl From for crate::build::Options { + fn from(options: Options) -> Self { + Self { + runner: None, + debug: options.debug, + target: None, + features: options.features, + bundles: None, + config: options.config, + args: Vec::new(), + } + } +} + +pub fn command(options: Options) -> Result<()> { + with_config(|root_conf, config, _metadata| { + ensure_init(config.project_dir(), MobileTarget::Ios) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + + let env = env()?; + init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; + + run_build(options, config, env).map_err(|e| Error::BuildFailed(e.to_string())) + }) + .map_err(Into::into) +} + +fn run_build(mut options: Options, config: &AppleConfig, env: Env) -> Result<()> { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + let noise_level = NoiseLevel::Polite; + + let bundle_identifier = { + let tauri_config = get_tauri_config(None)?; + let tauri_config_guard = tauri_config.lock().unwrap(); + let tauri_config_ = tauri_config_guard.as_ref().unwrap(); + tauri_config_.tauri.bundle.identifier.clone() + }; + + let mut build_options = options.clone().into(); + let interface = crate::build::setup(&mut build_options)?; + + let app_settings = interface.app_settings(); + let bin_path = app_settings.app_binary_path(&InterfaceOptions { + debug: build_options.debug, + ..Default::default() + })?; + let out_dir = bin_path.parent().unwrap(); + let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?; + + let cli_options = CliOptions { + features: build_options.features.clone(), + args: build_options.args.clone(), + vars: Default::default(), + }; + write_options(cli_options, &bundle_identifier, MobileTarget::Ios)?; + + options + .features + .get_or_insert(Vec::new()) + .push("custom-protocol".into()); + + let mut out_files = Vec::new(); + + call_for_targets_with_fallback( + options.targets.iter(), + &detect_target_ok, + &env, + |target: &Target| { + let mut app_version = config.bundle_version().clone(); + if let Some(build_number) = options.build_number { + app_version.push_extra(build_number); + } + + target.build(config, &env, noise_level, profile)?; + target.archive(config, &env, noise_level, profile, Some(app_version))?; + target.export(config, &env, noise_level)?; + + if let Ok(ipa_path) = config.ipa_path() { + let out_dir = config.export_dir().join(target.arch); + fs::create_dir_all(&out_dir)?; + let path = out_dir.join(ipa_path.file_name().unwrap()); + fs::rename(&ipa_path, &path)?; + out_files.push(path); + } + + anyhow::Result::Ok(()) + }, + ) + .map_err(|e: TargetInvalid| Error::TargetInvalid(e.to_string()))? + .map_err(|e: anyhow::Error| e)?; + + log_finished(out_files, "IPA"); + + Ok(()) +} diff --git a/tooling/cli/src/mobile/ios/dev.rs b/tooling/cli/src/mobile/ios/dev.rs new file mode 100644 index 000000000..654e5a6b5 --- /dev/null +++ b/tooling/cli/src/mobile/ios/dev.rs @@ -0,0 +1,146 @@ +use super::{device_prompt, ensure_init, env, init_dot_cargo, with_config, Error, MobileTarget}; +use crate::{ + helpers::{config::get as get_tauri_config, flock}, + interface::{AppSettings, Interface, MobileOptions, Options as InterfaceOptions}, + mobile::{write_options, CliOptions, DevChild, DevProcess}, + Result, +}; +use clap::Parser; + +use cargo_mobile::{ + apple::config::Config as AppleConfig, + config::Config, + opts::{NoiseLevel, Profile}, + os, +}; + +#[derive(Debug, Clone, Parser)] +#[clap(about = "iOS dev")] +pub struct Options { + /// List of cargo features to activate + #[clap(short, long, multiple_occurrences(true), multiple_values(true))] + pub features: Option>, + /// Exit on panic + #[clap(short, long)] + exit_on_panic: bool, + /// JSON string or path to JSON file to merge with tauri.conf.json + #[clap(short, long)] + pub config: Option, + /// Run the code in release mode + #[clap(long = "release")] + pub release_mode: bool, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Open Xcode instead of trying to run on a connected device + #[clap(short, long)] + pub open: bool, +} + +impl From for crate::dev::Options { + fn from(options: Options) -> Self { + Self { + runner: None, + target: None, + features: options.features, + exit_on_panic: options.exit_on_panic, + config: options.config, + release_mode: options.release_mode, + args: Vec::new(), + no_watch: options.no_watch, + } + } +} + +pub fn command(options: Options) -> Result<()> { + with_config(|root_conf, config, _metadata| { + ensure_init(config.project_dir(), MobileTarget::Ios) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + run_dev(options, root_conf, config).map_err(|e| Error::DevFailed(e.to_string())) + }) + .map_err(Into::into) +} + +fn run_dev(options: Options, root_conf: &Config, config: &AppleConfig) -> Result<()> { + let mut dev_options = options.clone().into(); + let mut interface = crate::dev::setup(&mut dev_options)?; + + let bundle_identifier = { + let tauri_config = + get_tauri_config(None).map_err(|e| Error::InvalidTauriConfig(e.to_string()))?; + let tauri_config_guard = tauri_config.lock().unwrap(); + let tauri_config_ = tauri_config_guard.as_ref().unwrap(); + tauri_config_.tauri.bundle.identifier.clone() + }; + + let app_settings = interface.app_settings(); + let bin_path = app_settings.app_binary_path(&InterfaceOptions { + debug: !dev_options.release_mode, + ..Default::default() + })?; + let out_dir = bin_path.parent().unwrap(); + let _lock = flock::open_rw(&out_dir.join("lock").with_extension("ios"), "iOS")?; + + let open = options.open; + interface.mobile_dev( + MobileOptions { + debug: true, + features: options.features, + args: Vec::new(), + config: options.config, + no_watch: options.no_watch, + }, + |options| { + let cli_options = CliOptions { + features: options.features.clone(), + args: options.args.clone(), + vars: Default::default(), + }; + write_options(cli_options, &bundle_identifier, MobileTarget::Ios)?; + if open { + open_dev(config) + } else { + match run(options, root_conf, config) { + Ok(c) => Ok(Box::new(c) as Box), + Err(Error::FailedToPromptForDevice(e)) => { + log::error!("{}", e); + open_dev(config) + } + Err(e) => Err(e.into()), + } + } + }, + ) +} + +fn open_dev(config: &AppleConfig) -> ! { + log::info!("Opening Xcode"); + if let Err(e) = os::open_file_with("Xcode", config.project_dir()) { + log::error!("{}", e); + } + loop { + std::thread::sleep(std::time::Duration::from_secs(24 * 60 * 60)); + } +} + +fn run( + options: MobileOptions, + root_conf: &Config, + config: &AppleConfig, +) -> Result { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + let noise_level = NoiseLevel::Polite; + + let env = env()?; + init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; + + device_prompt(&env) + .map_err(Error::FailedToPromptForDevice)? + .run(config, &env, noise_level, false, profile) + .map(|c| DevChild(Some(c))) + .map_err(Error::RunFailed) +} diff --git a/tooling/cli/src/mobile/ios/open.rs b/tooling/cli/src/mobile/ios/open.rs new file mode 100644 index 000000000..331a073ae --- /dev/null +++ b/tooling/cli/src/mobile/ios/open.rs @@ -0,0 +1,12 @@ +use super::{ensure_init, with_config, Error, MobileTarget}; +use crate::Result; +use cargo_mobile::os; + +pub fn command() -> Result<()> { + with_config(|_, config, _metadata| { + ensure_init(config.project_dir(), MobileTarget::Ios) + .map_err(|e| Error::ProjectNotInitialized(e.to_string()))?; + os::open_file_with("Xcode", config.project_dir()).map_err(Error::OpenFailed) + }) + .map_err(Into::into) +} diff --git a/tooling/cli/src/mobile/ios/xcode_script.rs b/tooling/cli/src/mobile/ios/xcode_script.rs new file mode 100644 index 000000000..bf5df1d40 --- /dev/null +++ b/tooling/cli/src/mobile/ios/xcode_script.rs @@ -0,0 +1,141 @@ +use super::{env, init_dot_cargo, with_config, Error}; +use crate::Result; +use clap::Parser; + +use cargo_mobile::{ + apple::target::Target, + opts::{NoiseLevel, Profile}, + util, +}; + +use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Value of `PLATFORM_DISPLAY_NAME` env var + #[clap(long)] + platform: String, + /// Value of `SDKROOT` env var + #[clap(long)] + sdk_root: PathBuf, + /// Value of `CONFIGURATION` env var + #[clap(long)] + configuration: String, + /// Value of `FORCE_COLOR` env var + #[clap(long)] + force_color: bool, + /// Value of `ARCHS` env var + #[clap(index = 1, required = true)] + arches: Vec, +} + +pub fn command(options: Options) -> Result<()> { + fn macos_from_platform(platform: &str) -> bool { + platform == "macOS" + } + + fn profile_from_configuration(configuration: &str) -> Profile { + if configuration == "release" { + Profile::Release + } else { + Profile::Debug + } + } + + let profile = profile_from_configuration(&options.configuration); + let macos = macos_from_platform(&options.platform); + let noise_level = NoiseLevel::Polite; + + with_config(|root_conf, config, metadata| { + let env = env()?; + init_dot_cargo(root_conf, None).map_err(Error::InitDotCargo)?; + // The `PATH` env var Xcode gives us is missing any additions + // made by the user's profile, so we'll manually add cargo's + // `PATH`. + let env = env.prepend_to_path( + util::home_dir() + .map_err(Error::NoHomeDir)? + .join(".cargo/bin"), + ); + + if !options.sdk_root.is_dir() { + return Err(Error::SdkRootInvalid { + sdk_root: options.sdk_root, + }); + } + let include_dir = options.sdk_root.join("usr/include"); + if !include_dir.is_dir() { + return Err(Error::IncludeDirInvalid { include_dir }); + } + + let mut host_env = HashMap::<&str, &OsStr>::new(); + + // Host flags that are used by build scripts + let (macos_isysroot, library_path) = { + let macos_sdk_root = options + .sdk_root + .join("../../../../MacOSX.platform/Developer/SDKs/MacOSX.sdk"); + if !macos_sdk_root.is_dir() { + return Err(Error::MacosSdkRootInvalid { macos_sdk_root }); + } + ( + format!("-isysroot {}", macos_sdk_root.display()), + format!("{}/usr/lib", macos_sdk_root.display()), + ) + }; + host_env.insert("MAC_FLAGS", macos_isysroot.as_ref()); + host_env.insert("CFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); + host_env.insert("CXXFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); + + host_env.insert( + "OBJC_INCLUDE_PATH_x86_64_apple_darwin", + include_dir.as_os_str(), + ); + + host_env.insert("RUST_BACKTRACE", "1".as_ref()); + + let macos_target = Target::macos(); + + let isysroot = format!("-isysroot {}", options.sdk_root.display()); + + for arch in options.arches { + // Set target-specific flags + let triple = match arch.as_str() { + "arm64" => "aarch64_apple_ios", + "x86_64" => "x86_64_apple_ios", + _ => return Err(Error::ArchInvalid { arch }), + }; + let cflags = format!("CFLAGS_{}", triple); + let cxxflags = format!("CFLAGS_{}", triple); + let objc_include_path = format!("OBJC_INCLUDE_PATH_{}", triple); + let mut target_env = host_env.clone(); + target_env.insert(cflags.as_ref(), isysroot.as_ref()); + target_env.insert(cxxflags.as_ref(), isysroot.as_ref()); + target_env.insert(objc_include_path.as_ref(), include_dir.as_ref()); + // Prevents linker errors in build scripts and proc macros: + // https://github.com/signalapp/libsignal-client/commit/02899cac643a14b2ced7c058cc15a836a2165b6d + target_env.insert("LIBRARY_PATH", library_path.as_ref()); + + let target = if macos { + &macos_target + } else { + Target::for_arch(&arch).ok_or_else(|| Error::ArchInvalid { + arch: arch.to_owned(), + })? + }; + target + .compile_lib( + config, + metadata, + noise_level, + true, + profile, + &env, + target_env, + ) + .map_err(Error::CompileLibFailed)?; + } + Ok(()) + }) + .map_err(Into::into) +} diff --git a/tooling/cli/src/mobile/mod.rs b/tooling/cli/src/mobile/mod.rs index db6696db7..71e8a6064 100644 --- a/tooling/cli/src/mobile/mod.rs +++ b/tooling/cli/src/mobile/mod.rs @@ -17,7 +17,7 @@ use cargo_mobile::{ config::{app::Raw as RawAppConfig, metadata::Metadata, Config, Raw}, }; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, ffi::OsString, path::PathBuf, process::ExitStatus}; +use std::{collections::HashMap, ffi::OsString, fmt::Write, path::PathBuf, process::ExitStatus}; pub mod android; mod init; @@ -238,3 +238,14 @@ fn ensure_init(project_dir: PathBuf, target: Target) -> Result<()> { Ok(()) } } + +fn log_finished(outputs: Vec, kind: &str) { + if !outputs.is_empty() { + let mut printable_paths = String::new(); + for path in &outputs { + writeln!(printable_paths, " {}", path.display()).unwrap(); + } + + log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths); + } +}