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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/mobile-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tauri-apps/cli": minor:feat
"tauri-cli": minor:feat
---

Added `ios run` and `android run` commands to run the app in production mode.
7 changes: 6 additions & 1 deletion crates/tauri-cli/src/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{
use crate::{error::Context, helpers::config::Config};
use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder};

pub use rust::{MobileOptions, Options, Rust as AppInterface};
pub use rust::{MobileOptions, Options, Rust as AppInterface, WatcherOptions};

pub trait DevProcess {
fn kill(&self) -> std::io::Result<()>;
Expand Down Expand Up @@ -113,4 +113,9 @@ pub trait Interface: Sized {
options: MobileOptions,
runner: R,
) -> crate::Result<()>;
fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
&mut self,
options: WatcherOptions,
runner: R,
) -> crate::Result<()>;
}
26 changes: 23 additions & 3 deletions crates/tauri-cli/src/interface/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ pub struct MobileOptions {
pub additional_watch_folders: Vec<PathBuf>,
}

#[derive(Debug, Clone)]
pub struct WatcherOptions {
pub config: Vec<ConfigValue>,
pub additional_watch_folders: Vec<PathBuf>,
}

#[derive(Debug)]
pub struct RustupTarget {
name: String,
Expand Down Expand Up @@ -245,12 +251,26 @@ impl Interface for Rust {
runner(options)?;
Ok(())
} else {
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
let run = Arc::new(|_rust: &mut Rust| runner(options.clone()));
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
self.watch(
WatcherOptions {
config: options.config.clone(),
additional_watch_folders: options.additional_watch_folders.clone(),
},
move || runner(options.clone()),
)
}
}

fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
&mut self,
options: WatcherOptions,
runner: R,
) -> crate::Result<()> {
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
let run = Arc::new(|_rust: &mut Rust| runner());
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
}

fn env(&self) -> HashMap<&str, String> {
let mut env = HashMap::new();
env.insert(
Expand Down
51 changes: 35 additions & 16 deletions crates/tauri-cli/src/mobile/android/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
flock,
},
interface::{AppInterface, Interface, Options as InterfaceOptions},
mobile::{write_options, CliOptions},
mobile::{write_options, CliOptions, TargetDevice},
ConfigValue, Error, Result,
};
use clap::{ArgAction, Parser};
Expand Down Expand Up @@ -63,10 +63,10 @@ pub struct Options {
pub split_per_abi: bool,
/// Build APKs.
#[clap(long)]
pub apk: bool,
pub apk: Option<bool>,
/// Build AABs.
#[clap(long)]
pub aab: bool,
pub aab: Option<bool>,
/// Open Android Studio
#[clap(short, long)]
pub open: bool,
Expand All @@ -83,6 +83,9 @@ pub struct Options {
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
#[clap(long)]
pub ignore_version_mismatches: bool,
/// Target device of this build
#[clap(skip)]
pub target_device: Option<TargetDevice>,
}

impl From<Options> for BuildOptions {
Expand All @@ -104,7 +107,15 @@ impl From<Options> for BuildOptions {
}
}

pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
pub struct BuiltApplication {
pub config: AndroidConfig,
pub interface: AppInterface,
// prevent drop
#[allow(dead_code)]
options_handle: OptionsHandle,
}

pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplication> {
crate::helpers::app_paths::resolve();

delete_codegen_vars();
Expand Down Expand Up @@ -188,8 +199,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
.context("failed to build Android app")?;

let open = options.open;
let _handle = run_build(
interface,
let options_handle = run_build(
&interface,
options,
build_options,
tauri_config,
Expand All @@ -203,12 +214,16 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
open_and_wait(&config, &env);
}

Ok(())
Ok(BuiltApplication {
config,
interface,
options_handle,
})
}

#[allow(clippy::too_many_arguments)]
fn run_build(
interface: AppInterface,
interface: &AppInterface,
mut options: Options,
build_options: BuildOptions,
tauri_config: ConfigHandle,
Expand All @@ -217,10 +232,10 @@ fn run_build(
env: &mut Env,
noise_level: NoiseLevel,
) -> Result<OptionsHandle> {
if !(options.apk || options.aab) {
if !(options.apk.is_some() || options.aab.is_some()) {
// if the user didn't specify the format to build, we'll do both
options.apk = true;
options.aab = true;
options.apk = Some(true);
options.aab = Some(true);
}

let interface_options = InterfaceOptions {
Expand All @@ -241,13 +256,13 @@ fn run_build(
noise_level,
vars: Default::default(),
config: build_options.config,
target_device: None,
target_device: options.target_device.clone(),
};
let handle = write_options(tauri_config.lock().unwrap().as_ref().unwrap(), cli_options)?;

inject_resources(config, tauri_config.lock().unwrap().as_ref().unwrap())?;

let apk_outputs = if options.apk {
let apk_outputs = if options.apk.unwrap_or_default() {
apk::build(
config,
env,
Expand All @@ -261,7 +276,7 @@ fn run_build(
Vec::new()
};

let aab_outputs = if options.aab {
let aab_outputs = if options.aab.unwrap_or_default() {
aab::build(
config,
env,
Expand All @@ -275,8 +290,12 @@ fn run_build(
Vec::new()
};

log_finished(apk_outputs, "APK");
log_finished(aab_outputs, "AAB");
if !apk_outputs.is_empty() {
log_finished(apk_outputs, "APK");
}
if !aab_outputs.is_empty() {
log_finished(aab_outputs, "AAB");
}

Ok(handle)
}
Expand Down
5 changes: 4 additions & 1 deletion crates/tauri-cli/src/mobile/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod android_studio_script;
mod build;
mod dev;
pub(crate) mod project;
mod run;

const NDK_VERSION: &str = "29.0.13846066";
const SDK_VERSION: u8 = 36;
Expand Down Expand Up @@ -96,6 +97,7 @@ enum Commands {
Init(InitOptions),
Dev(dev::Options),
Build(build::Options),
Run(run::Options),
#[clap(hide(true))]
AndroidStudioScript(android_studio_script::Options),
}
Expand All @@ -114,7 +116,8 @@ pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
)?
}
Commands::Dev(options) => dev::command(options, noise_level)?,
Commands::Build(options) => build::command(options, noise_level)?,
Commands::Build(options) => build::command(options, noise_level).map(|_| ())?,
Commands::Run(options) => run::command(options, noise_level)?,
Commands::AndroidStudioScript(options) => android_studio_script::command(options)?,
}

Expand Down
152 changes: 152 additions & 0 deletions crates/tauri-cli/src/mobile/android/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use cargo_mobile2::{
android::target::Target,
opts::{FilterLevel, NoiseLevel, Profile},
target::TargetTrait,
};
use clap::{ArgAction, Parser};
use std::path::PathBuf;

use super::{configure_cargo, device_prompt, env};
use crate::{
error::Context,
interface::{DevProcess, Interface, WatcherOptions},
mobile::{DevChild, TargetDevice},
ConfigValue, Result,
};

#[derive(Debug, Clone, Parser)]
#[clap(
about = "Run your app in production mode on Android",
long_about = "Run your app in production mode on Android. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`."
)]
pub struct Options {
/// Run the app in release mode
#[clap(short, long)]
pub release: bool,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// 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>,
/// Disable the file watcher
#[clap(long)]
pub no_watch: bool,
/// Additional paths to watch for changes.
#[clap(long)]
pub additional_watch_folders: Vec<PathBuf>,
/// Open Android Studio
#[clap(short, long)]
pub open: bool,
/// Runs on the given device name
pub device: Option<String>,
/// Command line arguments passed to the runner.
/// Use `--` to explicitly mark the start of the arguments.
/// e.g. `tauri android build -- [runnerArgs]`.
#[clap(last(true))]
pub args: Vec<String>,
/// Do not error out if a version mismatch is detected on a Tauri package.
///
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
#[clap(long)]
pub ignore_version_mismatches: bool,
}

pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let mut env = env(false)?;

let device = if options.open {
None
} else {
match device_prompt(&env, options.device.as_deref()) {
Ok(d) => Some(d),
Err(e) => {
log::error!("{e}");
None
}
}
};

let mut built_application = super::build::command(
super::build::Options {
debug: !options.release,
targets: device.as_ref().map(|d| {
vec![Target::all()
.iter()
.find(|(_key, t)| t.arch == d.target().arch)
.map(|(key, _t)| key.to_string())
.expect("Target not found")]
}),
features: options.features,
config: options.config.clone(),
split_per_abi: true,
apk: Some(false),
aab: Some(false),
open: options.open,
ci: false,
args: options.args,
ignore_version_mismatches: options.ignore_version_mismatches,
target_device: device.as_ref().map(|d| TargetDevice {
id: d.serial_no().to_string(),
name: d.name().to_string(),
}),
},
noise_level,
)?;

configure_cargo(&mut env, &built_application.config)?;

// options.open is handled by the build command
// so all we need to do here is run the app on the selected device
if let Some(device) = device {
let config = built_application.config.clone();
let release = options.release;
let runner = move || {
device
.run(
&config,
&env,
noise_level,
if !release {
Profile::Debug
} else {
Profile::Release
},
Some(match noise_level {
NoiseLevel::Polite => FilterLevel::Info,
NoiseLevel::LoudAndProud => FilterLevel::Debug,
NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
}),
false,
false,
".MainActivity".into(),
)
.map(|c| Box::new(DevChild::new(c)) as Box<dyn DevProcess + Send>)
.context("failed to run Android app")
};

if options.no_watch {
runner()?;
} else {
built_application.interface.watch(
WatcherOptions {
config: options.config,
additional_watch_folders: options.additional_watch_folders,
},
runner,
)?;
}
}

Ok(())
}
Loading