feat(cli): enhance Cargo features injection, add tests (#7141)

This commit is contained in:
Lucas Fernandes Nogueira 2023-06-06 09:29:28 -07:00 committed by GitHub
parent b41b57ebb2
commit 52474e479d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 541 additions and 224 deletions

View File

@ -0,0 +1,5 @@
---
"tauri-build": patch
---
Enhance Cargo features check.

View File

@ -0,0 +1,6 @@
---
"tauri-cli": patch
"@tauri-apps/cli": patch
---
Enhance injection of Cargo features.

View File

@ -0,0 +1,211 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use anyhow::{anyhow, Result};
use cargo_toml::{Dependency, Manifest};
use tauri_utils::config::{Config, PatternKind, TauriConfig};
#[derive(Debug, Default, PartialEq, Eq)]
struct Diff {
remove: Vec<String>,
add: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
enum DependencyKind {
Build,
Normal,
}
#[derive(Debug)]
struct AllowlistedDependency {
name: String,
alias: Option<String>,
kind: DependencyKind,
all_cli_managed_features: Option<Vec<&'static str>>,
expected_features: Vec<String>,
}
pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
let dependencies = vec![
AllowlistedDependency {
name: "tauri-build".into(),
alias: None,
kind: DependencyKind::Build,
all_cli_managed_features: Some(vec!["isolation"]),
expected_features: match config.tauri.pattern {
PatternKind::Isolation { .. } => vec!["isolation".to_string()],
_ => vec![],
},
},
AllowlistedDependency {
name: "tauri".into(),
alias: None,
kind: DependencyKind::Normal,
all_cli_managed_features: Some(TauriConfig::all_features()),
expected_features: config
.tauri
.features()
.into_iter()
.map(|f| f.to_string())
.collect::<Vec<String>>(),
},
];
for metadata in dependencies {
let mut name = metadata.name.clone();
let mut deps = find_dependency(manifest, &metadata.name, metadata.kind);
if deps.is_empty() {
if let Some(alias) = &metadata.alias {
deps = find_dependency(manifest, alias, metadata.kind);
name = alias.clone();
}
}
for dep in deps {
if let Err(error) = check_features(dep, &metadata) {
return Err(anyhow!("
The `{}` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
Please run `tauri dev` or `tauri build` or {}.
", name, error));
}
}
}
Ok(())
}
fn find_dependency(manifest: &mut Manifest, name: &str, kind: DependencyKind) -> Vec<Dependency> {
let dep = match kind {
DependencyKind::Build => manifest.build_dependencies.remove(name),
DependencyKind::Normal => manifest.dependencies.remove(name),
};
if let Some(dep) = dep {
vec![dep]
} else {
let mut deps = Vec::new();
for target in manifest.target.values_mut() {
if let Some(dep) = match kind {
DependencyKind::Build => target.build_dependencies.remove(name),
DependencyKind::Normal => target.dependencies.remove(name),
} {
deps.push(dep);
}
}
deps
}
}
fn features_diff(current: &[String], expected: &[String]) -> Diff {
let mut remove = Vec::new();
let mut add = Vec::new();
for feature in current {
if !expected.contains(feature) {
remove.push(feature.clone());
}
}
for feature in expected {
if !current.contains(feature) {
add.push(feature.clone());
}
}
Diff { remove, add }
}
fn check_features(dependency: Dependency, metadata: &AllowlistedDependency) -> Result<(), String> {
let features = match dependency {
Dependency::Simple(_) => Vec::new(),
Dependency::Detailed(dep) => dep.features,
Dependency::Inherited(dep) => dep.features,
};
let diff = if let Some(all_cli_managed_features) = &metadata.all_cli_managed_features {
features_diff(
&features
.into_iter()
.filter(|f| all_cli_managed_features.contains(&f.as_str()))
.collect::<Vec<String>>(),
&metadata.expected_features,
)
} else {
features_diff(
&features
.into_iter()
.filter(|f| f.starts_with("allow-"))
.collect::<Vec<String>>(),
&metadata.expected_features,
)
};
let mut error_message = String::new();
if !diff.remove.is_empty() {
error_message.push_str("remove the `");
error_message.push_str(&diff.remove.join(", "));
error_message.push_str(if diff.remove.len() == 1 {
"` feature"
} else {
"` features"
});
if !diff.add.is_empty() {
error_message.push_str(" and ");
}
}
if !diff.add.is_empty() {
error_message.push_str("add the `");
error_message.push_str(&diff.add.join(", "));
error_message.push_str(if diff.add.len() == 1 {
"` feature"
} else {
"` features"
});
}
if error_message.is_empty() {
Ok(())
} else {
Err(error_message)
}
}
#[cfg(test)]
mod tests {
use super::Diff;
#[test]
fn array_diff() {
for (current, expected, result) in [
(vec![], vec![], Default::default()),
(
vec!["a".into()],
vec![],
Diff {
remove: vec!["a".into()],
add: vec![],
},
),
(vec!["a".into()], vec!["a".into()], Default::default()),
(
vec!["a".into(), "b".into()],
vec!["a".into()],
Diff {
remove: vec!["b".into()],
add: vec![],
},
),
(
vec!["a".into(), "b".into()],
vec!["a".into(), "c".into()],
Diff {
remove: vec!["b".into()],
add: vec!["c".into()],
},
),
] {
assert_eq!(super::features_diff(&current, &expected), result);
}
}
}

View File

@ -5,7 +5,7 @@
#![cfg_attr(doc_cfg, feature(doc_cfg))]
pub use anyhow::Result;
use cargo_toml::{Dependency, Manifest};
use cargo_toml::Manifest;
use heck::AsShoutySnakeCase;
use tauri_utils::{
@ -15,6 +15,7 @@ use tauri_utils::{
use std::path::{Path, PathBuf};
mod allowlist;
#[cfg(feature = "codegen")]
mod codegen;
mod static_vcruntime;
@ -320,27 +321,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
}
if let Some(tauri_build) = manifest.build_dependencies.remove("tauri-build") {
let error_message = check_features(&config, tauri_build, true);
if !error_message.is_empty() {
return Err(anyhow!("
The `tauri-build` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
Please run `tauri dev` or `tauri build` or {}.
", error_message));
}
}
if let Some(tauri) = manifest.dependencies.remove("tauri") {
let error_message = check_features(&config, tauri, false);
if !error_message.is_empty() {
return Err(anyhow!("
The `tauri` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
Please run `tauri dev` or `tauri build` or {}.
", error_message));
}
}
allowlist::check(&config, &mut manifest)?;
let target_triple = std::env::var("TARGET").unwrap();
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
@ -487,93 +468,6 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
Ok(())
}
#[derive(Debug, Default, PartialEq, Eq)]
struct Diff {
remove: Vec<String>,
add: Vec<String>,
}
fn features_diff(current: &[String], expected: &[String]) -> Diff {
let mut remove = Vec::new();
let mut add = Vec::new();
for feature in current {
if !expected.contains(feature) {
remove.push(feature.clone());
}
}
for feature in expected {
if !current.contains(feature) {
add.push(feature.clone());
}
}
Diff { remove, add }
}
fn check_features(config: &Config, dependency: Dependency, is_tauri_build: bool) -> String {
use tauri_utils::config::{PatternKind, TauriConfig};
let features = match dependency {
Dependency::Simple(_) => Vec::new(),
Dependency::Detailed(dep) => dep.features,
Dependency::Inherited(dep) => dep.features,
};
let all_cli_managed_features = if is_tauri_build {
vec!["isolation"]
} else {
TauriConfig::all_features()
};
let expected = if is_tauri_build {
match config.tauri.pattern {
PatternKind::Isolation { .. } => vec!["isolation".to_string()],
_ => vec![],
}
} else {
config
.tauri
.features()
.into_iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
};
let diff = features_diff(
&features
.into_iter()
.filter(|f| all_cli_managed_features.contains(&f.as_str()))
.collect::<Vec<String>>(),
&expected,
);
let mut error_message = String::new();
if !diff.remove.is_empty() {
error_message.push_str("remove the `");
error_message.push_str(&diff.remove.join(", "));
error_message.push_str(if diff.remove.len() == 1 {
"` feature"
} else {
"` features"
});
if !diff.add.is_empty() {
error_message.push_str(" and ");
}
}
if !diff.add.is_empty() {
error_message.push_str("add the `");
error_message.push_str(&diff.add.join(", "));
error_message.push_str(if diff.add.len() == 1 {
"` feature"
} else {
"` features"
});
}
error_message
}
#[derive(serde::Deserialize)]
struct CargoMetadata {
workspace_root: PathBuf,
@ -593,42 +487,3 @@ fn get_workspace_dir() -> Result<PathBuf> {
Ok(serde_json::from_slice::<CargoMetadata>(&output.stdout)?.workspace_root)
}
#[cfg(test)]
mod tests {
use super::Diff;
#[test]
fn array_diff() {
for (current, expected, result) in [
(vec![], vec![], Default::default()),
(
vec!["a".into()],
vec![],
Diff {
remove: vec!["a".into()],
add: vec![],
},
),
(vec!["a".into()], vec!["a".into()], Default::default()),
(
vec!["a".into(), "b".into()],
vec!["a".into()],
Diff {
remove: vec!["b".into()],
add: vec![],
},
),
(
vec!["a".into(), "b".into()],
vec!["a".into(), "c".into()],
Diff {
remove: vec!["b".into()],
add: vec!["c".into()],
},
),
] {
assert_eq!(super::features_diff(&current, &expected), result);
}
}
}

View File

@ -2434,7 +2434,6 @@ pub struct TauriConfig {
impl TauriConfig {
/// Returns all Cargo features.
#[allow(dead_code)]
pub fn all_features() -> Vec<&'static str> {
let mut features = AllowlistConfig::all_features();
features.extend(vec![
@ -2448,7 +2447,6 @@ impl TauriConfig {
}
/// Returns the enabled Cargo features.
#[allow(dead_code)]
pub fn features(&self) -> Vec<&str> {
let mut features = self.allowlist.to_features();
if self.cli.is_some() {

View File

@ -10,7 +10,7 @@ use crate::helpers::{
use anyhow::Context;
use itertools::Itertools;
use log::info;
use toml_edit::{Array, Document, InlineTable, Item, Table, TableLike, Value};
use toml_edit::{Array, Document, InlineTable, Item, TableLike, Value};
use std::{
collections::{HashMap, HashSet},
@ -108,31 +108,55 @@ fn toml_array(features: &HashSet<String>) -> Array {
f
}
fn write_features(
dependencies: &mut Table,
dependency_name: &str,
all_features: Vec<&str>,
features: &mut HashSet<String>,
) -> crate::Result<bool> {
let item = dependencies.entry(dependency_name).or_insert(Item::None);
fn find_dependency<'a>(
manifest: &'a mut Document,
name: &'a str,
kind: DependencyKind,
) -> Vec<&'a mut Item> {
let table = match kind {
DependencyKind::Build => "build-dependencies",
DependencyKind::Normal => "dependencies",
};
// do not rewrite if dependency uses workspace inheritance
if item
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or_default()
{
info!("`{dependency_name}` dependency has workspace inheritance enabled. The features array won't be automatically rewritten. Expected features: [{}]", features.iter().join(", "));
return Ok(false);
let m = manifest.as_table_mut();
for (k, v) in m.iter_mut() {
if let Some(t) = v.as_table_mut() {
if k == table {
if let Some(item) = t.get_mut(name) {
return vec![item];
}
} else if k == "target" {
let mut matching_deps = Vec::new();
for (_, target_value) in t.iter_mut() {
if let Some(target_table) = target_value.as_table_mut() {
if let Some(deps) = target_table.get_mut(table) {
if let Some(item) = deps.as_table_mut().and_then(|t| t.get_mut(name)) {
matching_deps.push(item);
}
}
}
}
return matching_deps;
}
}
}
Vec::new()
}
fn write_features<F: Fn(&str) -> bool>(
dependency_name: &str,
item: &mut Item,
is_managed_feature: F,
features: &mut HashSet<String>,
) -> crate::Result<bool> {
if let Some(dep) = item.as_table_mut() {
inject_features_table(dep, &all_features, features);
inject_features_table(dep, is_managed_feature, features);
Ok(true)
} else if let Some(dep) = item.as_value_mut() {
match dep {
Value::InlineTable(table) => {
inject_features_table(table, &all_features, features);
inject_features_table(table, is_managed_feature, features);
}
Value::String(version) => {
let mut def = InlineTable::default();
@ -153,16 +177,30 @@ fn write_features(
}
}
fn inject_features_table<D: TableLike>(
#[derive(Debug, Clone, Copy)]
enum DependencyKind {
Build,
Normal,
}
#[derive(Debug)]
struct DependencyAllowlist {
name: String,
kind: DependencyKind,
all_cli_managed_features: Vec<&'static str>,
features: HashSet<String>,
}
fn inject_features_table<D: TableLike, F: Fn(&str) -> bool>(
dep: &mut D,
all_features: &[&str],
is_managed_feature: F,
features: &mut HashSet<String>,
) {
let manifest_features = dep.entry("features").or_insert(Item::None);
if let Item::Value(Value::Array(f)) = &manifest_features {
for feat in f.iter() {
if let Value::String(feature) = feat {
if !all_features.contains(&feature.value().as_str()) {
if !is_managed_feature(feature.value().as_str()) {
features.insert(feature.value().to_string());
}
}
@ -192,71 +230,275 @@ fn inject_features_table<D: TableLike>(
}
}
fn inject_features(
manifest: &mut Document,
dependencies: &mut Vec<DependencyAllowlist>,
) -> crate::Result<bool> {
let mut persist = false;
for dependency in dependencies {
let name = dependency.name.clone();
let items = find_dependency(manifest, &dependency.name, dependency.kind);
for item in items {
// do not rewrite if dependency uses workspace inheritance
if item
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or_default()
{
info!("`{name}` dependency has workspace inheritance enabled. The features array won't be automatically rewritten. Expected features: [{}]", dependency.features.iter().join(", "));
} else {
let all_cli_managed_features = dependency.all_cli_managed_features.clone();
let is_managed_feature: Box<dyn Fn(&str) -> bool> =
Box::new(move |feature| all_cli_managed_features.contains(&feature));
let should_write =
write_features(&name, item, is_managed_feature, &mut dependency.features)?;
if !persist {
persist = should_write;
}
}
}
}
Ok(persist)
}
pub fn rewrite_manifest(config: &Config) -> crate::Result<Manifest> {
let manifest_path = tauri_dir().join("Cargo.toml");
let mut manifest = read_manifest(&manifest_path)?;
let mut dependencies = Vec::new();
// tauri-build
let mut tauri_build_features = HashSet::new();
if let PatternKind::Isolation { .. } = config.tauri.pattern {
tauri_build_features.insert("isolation".to_string());
}
let resp = write_features(
manifest
.as_table_mut()
.entry("build-dependencies")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.expect("manifest build-dependencies isn't a table"),
"tauri-build",
vec!["isolation"],
&mut tauri_build_features,
)?;
dependencies.push(DependencyAllowlist {
name: "tauri-build".into(),
kind: DependencyKind::Build,
all_cli_managed_features: vec!["isolation"],
features: tauri_build_features,
});
let mut tauri_features =
// tauri
let tauri_features =
HashSet::from_iter(config.tauri.features().into_iter().map(|f| f.to_string()));
let cli_managed_tauri_features = crate::helpers::config::TauriConfig::all_features();
let res = match write_features(
manifest
.as_table_mut()
.entry("dependencies")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.expect("manifest dependencies isn't a table"),
"tauri",
cli_managed_tauri_features,
&mut tauri_features,
) {
Err(e) => Err(e),
Ok(t) if !resp => Ok(t),
_ => Ok(true),
};
dependencies.push(DependencyAllowlist {
name: "tauri".into(),
kind: DependencyKind::Normal,
all_cli_managed_features: crate::helpers::config::TauriConfig::all_features(),
features: tauri_features,
});
match res {
Ok(true) => {
let mut manifest_file =
File::create(&manifest_path).with_context(|| "failed to open Cargo.toml for rewrite")?;
manifest_file.write_all(
manifest
.to_string()
// apply some formatting fixes
.replace(r#"" ,features =["#, r#"", features = ["#)
.replace(r#"" , features"#, r#"", features"#)
.replace("]}", "] }")
.replace("={", "= {")
.replace("=[", "= [")
.replace(r#"",""#, r#"", ""#)
.as_bytes(),
)?;
manifest_file.flush()?;
Ok(Manifest {
inner: manifest,
tauri_features,
})
}
Ok(false) => Ok(Manifest {
let persist = inject_features(&mut manifest, &mut dependencies)?;
let tauri_features = dependencies
.into_iter()
.find(|d| d.name == "tauri")
.unwrap()
.features;
if persist {
let mut manifest_file =
File::create(&manifest_path).with_context(|| "failed to open Cargo.toml for rewrite")?;
manifest_file.write_all(
manifest
.to_string()
// apply some formatting fixes
.replace(r#"" ,features =["#, r#"", features = ["#)
.replace(r#"" , features"#, r#"", features"#)
.replace("]}", "] }")
.replace("={", "= {")
.replace("=[", "= [")
.replace(r#"",""#, r#"", ""#)
.as_bytes(),
)?;
manifest_file.flush()?;
Ok(Manifest {
inner: manifest,
tauri_features,
}),
Err(e) => Err(e),
})
} else {
Ok(Manifest {
inner: manifest,
tauri_features,
})
}
}
#[cfg(test)]
mod tests {
use super::{DependencyAllowlist, DependencyKind};
use std::collections::{HashMap, HashSet};
fn inject_features(toml: &str, mut dependencies: Vec<DependencyAllowlist>) {
let mut manifest = toml.parse::<toml_edit::Document>().expect("invalid toml");
let mut expected = HashMap::new();
for dep in &dependencies {
let mut features = dep.features.clone();
for item in super::find_dependency(&mut manifest, &dep.name, dep.kind) {
let item_table = if let Some(table) = item.as_table() {
Some(table.clone())
} else if let Some(toml_edit::Value::InlineTable(table)) = item.as_value() {
Some(table.clone().into_table())
} else {
None
};
if let Some(f) = item_table
.and_then(|t| t.get("features").cloned())
.and_then(|f| f.as_array().cloned())
{
for feature in f.iter() {
let feature = feature.as_str().expect("feature is not a string");
if !dep.all_cli_managed_features.contains(&feature) {
features.insert(feature.into());
}
}
}
}
expected.insert(dep.name.clone(), features);
}
super::inject_features(&mut manifest, &mut dependencies).expect("failed to migrate manifest");
for dep in dependencies {
let expected_features = expected.get(&dep.name).unwrap();
for item in super::find_dependency(&mut manifest, &dep.name, dep.kind) {
let item_table = if let Some(table) = item.as_table() {
table.clone()
} else if let Some(toml_edit::Value::InlineTable(table)) = item.as_value() {
table.clone().into_table()
} else {
panic!("unexpected TOML item kind for {}", dep.name);
};
let features_array = item_table
.get("features")
.expect("missing features")
.as_array()
.expect("features must be an array")
.clone();
let mut features = Vec::new();
for feature in features_array.iter() {
let feature = feature.as_str().expect("feature must be a string");
features.push(feature);
}
for expected in expected_features {
assert!(
features.contains(&expected.as_str()),
"feature {expected} should have been injected"
);
}
}
}
}
fn tauri_dependency(features: HashSet<String>) -> DependencyAllowlist {
DependencyAllowlist {
name: "tauri".into(),
kind: DependencyKind::Normal,
all_cli_managed_features: vec!["isolation"],
features,
}
}
fn tauri_build_dependency(features: HashSet<String>) -> DependencyAllowlist {
DependencyAllowlist {
name: "tauri-build".into(),
kind: DependencyKind::Build,
all_cli_managed_features: crate::helpers::config::TauriConfig::all_features(),
features,
}
}
#[test]
fn inject_features_table() {
inject_features(
r#"
[dependencies]
tauri = { version = "1", features = ["dummy"] }
[build-dependencies]
tauri-build = { version = "1" }
"#,
vec![
tauri_dependency(HashSet::from_iter(
crate::helpers::config::TauriConfig::all_features()
.iter()
.map(|f| f.to_string()),
)),
tauri_build_dependency(HashSet::from_iter(vec!["isolation".into()])),
],
);
}
#[test]
fn inject_features_target() {
inject_features(
r#"
[target."cfg(windows)".dependencies]
tauri = { version = "1", features = ["dummy"] }
[target."cfg(target_os = \"macos\")".build-dependencies]
tauri-build = { version = "1" }
[target."cfg(target_os = \"linux\")".dependencies]
tauri = { version = "1", features = ["isolation"] }
[target."cfg(windows)".build-dependencies]
tauri-build = { version = "1" }
"#,
vec![
tauri_dependency(Default::default()),
tauri_build_dependency(HashSet::from_iter(vec!["isolation".into()])),
],
);
}
#[test]
fn inject_features_inline_table() {
inject_features(
r#"
[dependencies.tauri]
version = "1"
features = ["test"]
[build-dependencies.tauri-build]
version = "1"
features = ["config-toml", "codegen", "isolation"]
"#,
vec![
tauri_dependency(HashSet::from_iter(vec![
"isolation".into(),
"native-tls-vendored".into(),
])),
tauri_build_dependency(HashSet::from_iter(vec!["isolation".into()])),
],
);
}
#[test]
fn inject_features_string() {
inject_features(
r#"
[dependencies]
tauri = "1"
[build-dependencies]
tauri-build = "1"
"#,
vec![
tauri_dependency(HashSet::from_iter(vec![
"isolation".into(),
"native-tls-vendored".into(),
])),
tauri_build_dependency(HashSet::from_iter(vec!["isolation".into()])),
],
);
}
}