refactor(cli): simplify features: Option<Vec<String>> to Vec<String> (#14607)

* refactor: use empty vector for features instead of None

* refactor: reorder

* add change file

* comment: highlight places where serialization is used

* refactor: simplify serialization

* Update .changes/empty-vec-instead-of-none.md

* Update crates/tauri-cli/src/mobile/ios/mod.rs

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
This commit is contained in:
sftse
2025-12-29 06:54:51 +01:00
committed by GitHub
parent 51f0fcb69c
commit a2abe2e6bc
19 changed files with 57 additions and 70 deletions

View File

@@ -0,0 +1,6 @@
---
"@tauri-apps/cli": patch:enhance
"tauri-cli": patch:enhance
---
Simplified internal representation of `features: Option<Vec<String>>` with `Vec<String>`, no user facing changes

View File

@@ -40,7 +40,7 @@ pub struct Options {
pub target: Option<String>,
/// Space or comma separated list of features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: Vec<String>,
/// Space or comma separated list of bundles to package.
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub bundles: Option<Vec<BundleFormat>>,
@@ -252,8 +252,7 @@ pub fn setup(
options
.features
.get_or_insert(Vec::new())
.extend(config.build.features.clone().unwrap_or_default());
.extend_from_slice(config.build.features.as_deref().unwrap_or_default());
interface.build_options(&mut options.args, &mut options.features, mobile);
Ok(())

View File

@@ -71,7 +71,7 @@ pub struct Options {
pub config: Vec<ConfigValue>,
/// Space or comma separated list of features, should be the same features passed to `tauri build` if any.
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: Vec<String>,
/// Target triple to build against.
///
/// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.

View File

@@ -57,7 +57,7 @@ pub struct Options {
pub target: Option<String>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: Vec<String>,
/// Exit on panic
#[clap(short, long)]
pub exit_on_panic: bool,
@@ -254,9 +254,7 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
.features
.clone()
.unwrap_or_default();
if let Some(features) = &options.features {
cargo_features.extend(features.clone());
}
cargo_features.extend(options.features.clone());
let mut dev_url = config
.lock()

View File

@@ -46,7 +46,7 @@ pub trait AppSettings {
package_types: Vec<PackageType>,
) -> crate::Result<Settings> {
let no_default_features = options.args.contains(&"--no-default-features".into());
let mut enabled_features = options.features.clone().unwrap_or_default();
let mut enabled_features = options.features.clone();
if !no_default_features {
enabled_features.push("default".into());
}

View File

@@ -51,7 +51,7 @@ pub struct Options {
pub runner: Option<RunnerConfig>,
pub debug: bool,
pub target: Option<String>,
pub features: Option<Vec<String>>,
pub features: Vec<String>,
pub args: Vec<String>,
pub config: Vec<ConfigValue>,
pub no_watch: bool,
@@ -108,7 +108,7 @@ impl From<crate::dev::Options> for Options {
#[derive(Debug, Clone)]
pub struct MobileOptions {
pub debug: bool,
pub features: Option<Vec<String>>,
pub features: Vec<String>,
pub args: Vec<String>,
pub config: Vec<ConfigValue>,
pub no_watch: bool,
@@ -393,7 +393,7 @@ fn dev_options(
mobile: bool,
args: &mut Vec<String>,
run_args: &mut Vec<String>,
features: &mut Option<Vec<String>>,
features: &mut Vec<String>,
app_settings: &RustAppSettings,
) {
let mut dev_args = Vec::new();
@@ -429,9 +429,7 @@ fn dev_options(
})
.collect();
args.push("--no-default-features".into());
if !enable_features.is_empty() {
features.get_or_insert(Vec::new()).extend(enable_features);
}
features.extend(enable_features);
}
}
@@ -498,15 +496,8 @@ fn get_watch_folders(additional_watch_folders: &[PathBuf]) -> crate::Result<Vec<
}
impl Rust {
pub fn build_options(
&self,
args: &mut Vec<String>,
features: &mut Option<Vec<String>>,
mobile: bool,
) {
features
.get_or_insert(Vec::new())
.push("tauri/custom-protocol".into());
pub fn build_options(&self, args: &mut Vec<String>, features: &mut Vec<String>, mobile: bool) {
features.push("tauri/custom-protocol".into());
if mobile {
args.push("--lib".into());
} else {
@@ -957,11 +948,12 @@ impl AppSettings for RustAppSettings {
.clone()
.unwrap_or_default();
for bin in bins {
if let (Some(req_features), Some(opt_features)) =
(&bin.required_features, &options.features)
{
if let Some(req_features) = &bin.required_features {
// Check if all required features are enabled.
if !req_features.iter().all(|feat| opt_features.contains(feat)) {
if !req_features
.iter()
.all(|feat| options.features.contains(feat))
{
continue;
}
}

View File

@@ -262,9 +262,7 @@ fn cargo_command(
build_cmd.args(&options.args);
let mut features = config_features;
if let Some(f) = options.features {
features.extend(f);
}
features.extend(options.features);
if !features.is_empty() {
build_cmd.arg("--features");
build_cmd.arg(features.join(","));

View File

@@ -80,7 +80,7 @@ pub fn command(options: Options) -> Result<()> {
&AppInterface::new(tauri_config_, None)?,
),
tauri_config_,
None,
&[],
&cli_options,
);
(config, metadata)

View File

@@ -48,7 +48,7 @@ pub struct Options {
pub targets: Option<Vec<String>>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: 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.
@@ -152,7 +152,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplica
let (config, metadata) = get_config(
&app,
tauri_config_,
build_options.features.as_ref(),
&build_options.features,
&Default::default(),
);
(interface, config, metadata)

View File

@@ -45,7 +45,7 @@ use std::{env::set_current_dir, net::Ipv4Addr, path::PathBuf};
pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: Vec<String>,
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,

View File

@@ -128,19 +128,14 @@ pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
pub fn get_config(
app: &App,
config: &TauriConfig,
features: Option<&Vec<String>>,
features: &[String],
cli_options: &CliOptions,
) -> (AndroidConfig, AndroidMetadata) {
let mut android_options = cli_options.clone();
if let Some(features) = features {
android_options
.features
.get_or_insert(Vec::new())
.extend_from_slice(features);
}
android_options.features.extend_from_slice(features);
let raw = RawAndroidConfig {
features: android_options.features.clone(),
features: Some(android_options.features.clone()),
logcat_filter_specs: vec![
"RustStdoutStderr".into(),
format!(
@@ -161,7 +156,7 @@ pub fn get_config(
let metadata = AndroidMetadata {
supported: true,
cargo_args: Some(android_options.args),
features: android_options.features,
features: Some(android_options.features),
..Default::default()
};
@@ -257,8 +252,8 @@ fn ensure_java() -> Result<()> {
fn ensure_sdk(non_interactive: bool) -> Result<()> {
let android_home = std::env::var_os("ANDROID_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from));
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT"))
.map(PathBuf::from);
if !android_home.as_ref().is_some_and(|v| v.exists()) {
log::info!(
"ANDROID_HOME {}, trying to locate Android SDK...",
@@ -354,8 +349,8 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> {
fn ensure_ndk(non_interactive: bool) -> Result<()> {
// re-evaluate ANDROID_HOME
let android_home = std::env::var_os("ANDROID_HOME")
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT"))
.map(PathBuf::from)
.or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from))
.context("Failed to locate Android SDK")?;
let mut installed_ndks = read_dir(android_home.join("ndk"))
.map(|dir| {

View File

@@ -29,7 +29,7 @@ pub struct Options {
pub release: bool,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: 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.

View File

@@ -135,7 +135,7 @@ pub fn exec(
Target::Android => {
let _env = super::android::env(non_interactive)?;
let (config, metadata) =
super::android::get_config(&app, tauri_config_, None, &Default::default());
super::android::get_config(&app, tauri_config_, &[], &Default::default());
map.insert("android", &config);
super::android::project::gen(
&config,
@@ -150,7 +150,7 @@ pub fn exec(
// Generate Xcode project
Target::Ios => {
let (config, metadata) =
super::ios::get_config(&app, tauri_config_, None, &Default::default())?;
super::ios::get_config(&app, tauri_config_, &[], &Default::default())?;
map.insert("apple", &config);
super::ios::project::gen(
tauri_config_,

View File

@@ -60,7 +60,7 @@ pub struct Options {
pub targets: Option<Vec<String>>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: 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.
@@ -201,7 +201,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplica
let (config, _metadata) = get_config(
&app,
tauri_config_,
build_options.features.as_ref(),
&build_options.features,
&Default::default(),
)?;
(interface, config)

View File

@@ -54,7 +54,7 @@ environment variable to determine whether the public network should be used or n
pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: Vec<String>,
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
@@ -197,7 +197,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let (config, _metadata) = get_config(
&app,
tauri_config_,
dev_options.features.as_ref(),
&dev_options.features,
&Default::default(),
)?;

View File

@@ -40,7 +40,7 @@ use crate::{
use std::{
env::{set_var, var_os},
fs::create_dir_all,
path::PathBuf,
path::Path,
str::FromStr,
thread::sleep,
time::Duration,
@@ -126,16 +126,11 @@ pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
pub fn get_config(
app: &App,
tauri_config: &TauriConfig,
features: Option<&Vec<String>>,
features: &[String],
cli_options: &CliOptions,
) -> Result<(AppleConfig, AppleMetadata)> {
let mut ios_options = cli_options.clone();
if let Some(features) = features {
ios_options
.features
.get_or_insert(Vec::new())
.extend_from_slice(features);
}
ios_options.features.extend_from_slice(features);
let bundle_version = if let Some(bundle_version) = tauri_config
.bundle
@@ -232,7 +227,7 @@ pub fn get_config(
}
}
}),
ios_features: ios_options.features.clone(),
ios_features: Some(ios_options.features.clone()),
bundle_version,
bundle_version_short,
ios_version: Some(tauri_config.bundle.ios.minimum_system_version.clone()),
@@ -252,7 +247,7 @@ pub fn get_config(
.clone()
.unwrap_or_default()
{
let framework_path = PathBuf::from(&framework);
let framework_path = Path::new(&framework);
let ext = framework_path.extension().unwrap_or_default();
if ext.is_empty() {
frameworks.push(framework);
@@ -277,7 +272,11 @@ pub fn get_config(
supported: true,
ios: ApplePlatform {
cargo_args: Some(ios_options.args),
features: ios_options.features,
features: if ios_options.features.is_empty() {
None
} else {
Some(ios_options.features)
},
frameworks: Some(frameworks),
vendor_frameworks: Some(vendor_frameworks),
..Default::default()

View File

@@ -26,7 +26,7 @@ pub struct Options {
pub release: bool,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
pub features: 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.
@@ -77,7 +77,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
super::build::Options {
debug: !options.release,
targets: Some(vec![]), /* skips IPA build since there's no target */
features: None,
features: Vec::new(),
config: options.config.clone(),
build_number: None,
open: options.open,

View File

@@ -128,7 +128,7 @@ pub fn command(options: Options) -> Result<()> {
&AppInterface::new(tauri_config_, None)?,
),
tauri_config_,
None,
&[],
&cli_options,
)?;
(config, metadata)

View File

@@ -181,7 +181,7 @@ impl Default for DevHost {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliOptions {
pub dev: bool,
pub features: Option<Vec<String>>,
pub features: Vec<String>,
pub args: Vec<String>,
pub noise_level: NoiseLevel,
pub vars: HashMap<String, OsString>,
@@ -193,7 +193,7 @@ impl Default for CliOptions {
fn default() -> Self {
Self {
dev: false,
features: None,
features: Vec::new(),
args: vec!["--lib".into()],
noise_level: Default::default(),
vars: Default::default(),