Skip to content

Commit bc4afe7

Browse files
feat(cli): check plugin versions for incompatibilities (#13993)
* feat(cli): check plugin versions for incompatibilities check core plugin versions for incompatibilities between Cargo and NPM releases a plugin NPM/cargo version is considered "incompatible" if their major or minor versions are not equal on dev we show an warning on build we error out (with a `--ignore-incompatible-plugins` flag to prevent that) this is an idea from @oscartbeaumont we've seen several plugin changes that require updates for both the cargo and the NPM releases of a plugin, and if they are not in sync, the functionality does not work e.g. tauri-apps/plugins-workspace#2573 where the change actually breaks the app updater if you miss the NPM update * Use list to get multiple package versions at once * Fix for older rust versions * Clippy * Support yarn classic * Support yarn berry * Use `.cmd` only for `npm`, `yarn`, `pnpm` * Use yarn list without --pattern * rename * Extract function `check_incompatible_packages` * Check `tauri` <-> `@tauri-apps/api` * incompatible -> mismatched * run build check in parallel * rename struct * Switch back to use sync check and add todo * Extract to function `cargo_manifest_and_lock` --------- Co-authored-by: Tony <[email protected]>
1 parent 7c2eb31 commit bc4afe7

File tree

10 files changed

+337
-45
lines changed

10 files changed

+337
-45
lines changed

.changes/check-plugin-versions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-cli": minor:feat
3+
"@tauri-apps/cli": minor:feat
4+
---
5+
6+
Check installed plugin NPM/crate versions for incompatible releases.

crates/tauri-cli/src/build.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ use crate::{
66
bundle::BundleFormat,
77
helpers::{
88
self,
9-
app_paths::tauri_dir,
9+
app_paths::{frontend_dir, tauri_dir},
1010
config::{get as get_config, ConfigHandle, FrontendDist},
1111
},
12+
info::plugins::check_mismatched_packages,
1213
interface::{rust::get_cargo_target_dir, AppInterface, Interface},
1314
ConfigValue, Result,
1415
};
@@ -70,6 +71,11 @@ pub struct Options {
7071
/// On subsequent runs, it's recommended to disable this setting again.
7172
#[clap(long)]
7273
pub skip_stapling: bool,
74+
/// Do not error out if a version mismatch is detected on a Tauri package.
75+
///
76+
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
77+
#[clap(long)]
78+
pub ignore_version_mismatches: bool,
7379
}
7480

7581
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
@@ -131,6 +137,18 @@ pub fn setup(
131137
mobile: bool,
132138
) -> Result<()> {
133139
let tauri_path = tauri_dir();
140+
141+
// TODO: Maybe optimize this to run in parallel in the future
142+
// see https://github.com/tauri-apps/tauri/pull/13993#discussion_r2280697117
143+
log::info!("Looking up installed tauri packages to check mismatched versions...");
144+
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
145+
if options.ignore_version_mismatches {
146+
log::error!("{error}");
147+
} else {
148+
return Err(error);
149+
}
150+
}
151+
134152
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
135153

136154
let config_guard = config.lock().unwrap();
@@ -141,24 +159,21 @@ pub fn setup(
141159
.unwrap_or_else(|| "tauri.conf.json".into());
142160

143161
if config_.identifier == "com.tauri.dev" {
144-
log::error!(
145-
"You must change the bundle identifier in `{} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
146-
bundle_identifier_source
162+
anyhow::bail!(
163+
"You must change the bundle identifier in `{bundle_identifier_source} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
147164
);
148-
std::process::exit(1);
149165
}
150166

151167
if config_
152168
.identifier
153169
.chars()
154170
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
155171
{
156-
log::error!(
172+
anyhow::bail!(
157173
"The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
158174
config_.identifier,
159175
bundle_identifier_source
160176
);
161-
std::process::exit(1);
162177
}
163178

164179
if config_.identifier.ends_with(".app") {

crates/tauri-cli/src/dev.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
1111
},
1212
},
13+
info::plugins::check_mismatched_packages,
1314
interface::{AppInterface, ExitReason, Interface},
1415
CommandExt, ConfigValue, Result,
1516
};
@@ -135,6 +136,13 @@ fn command_internal(mut options: Options) -> Result<()> {
135136

136137
pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHandle) -> Result<()> {
137138
let tauri_path = tauri_dir();
139+
140+
std::thread::spawn(|| {
141+
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
142+
log::error!("{error}");
143+
}
144+
});
145+
138146
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
139147

140148
if let Some(before_dev) = config

crates/tauri-cli/src/helpers/cargo_manifest.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use std::{
1010
path::{Path, PathBuf},
1111
};
1212

13+
use crate::interface::rust::get_workspace_dir;
14+
1315
#[derive(Clone, Deserialize)]
1416
pub struct CargoLockPackage {
1517
pub name: String,
@@ -49,6 +51,18 @@ pub struct CargoManifest {
4951
pub dependencies: HashMap<String, CargoManifestDependency>,
5052
}
5153

54+
pub fn cargo_manifest_and_lock(tauri_dir: &Path) -> (Option<CargoManifest>, Option<CargoLock>) {
55+
let manifest: Option<CargoManifest> = fs::read_to_string(tauri_dir.join("Cargo.toml"))
56+
.ok()
57+
.and_then(|manifest_contents| toml::from_str(&manifest_contents).ok());
58+
59+
let lock: Option<CargoLock> = get_workspace_dir()
60+
.ok()
61+
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
62+
.and_then(|s| toml::from_str(&s).ok());
63+
64+
(manifest, lock)
65+
}
5266
#[derive(Default)]
5367
pub struct CrateVersion {
5468
pub version: Option<String>,

crates/tauri-cli/src/helpers/npm.rs

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
// SPDX-License-Identifier: MIT
44

55
use anyhow::Context;
6+
use serde::Deserialize;
67

78
use crate::helpers::cross_command;
8-
use std::{fmt::Display, path::Path, process::Command};
9+
use std::{collections::HashMap, fmt::Display, path::Path, process::Command};
910

1011
pub fn manager_version(package_manager: &str) -> Option<String> {
1112
cross_command(package_manager)
@@ -197,6 +198,7 @@ impl PackageManager {
197198
Ok(())
198199
}
199200

201+
// TODO: Use `current_package_versions` as much as possible for better speed
200202
pub fn current_package_version<P: AsRef<Path>>(
201203
&self,
202204
name: &str,
@@ -254,4 +256,157 @@ impl PackageManager {
254256
Ok(None)
255257
}
256258
}
259+
260+
pub fn current_package_versions(
261+
&self,
262+
packages: &[String],
263+
frontend_dir: &Path,
264+
) -> crate::Result<HashMap<String, semver::Version>> {
265+
let output = match self {
266+
PackageManager::Yarn => return yarn_package_versions(packages, frontend_dir),
267+
PackageManager::YarnBerry => return yarn_berry_package_versions(packages, frontend_dir),
268+
PackageManager::Pnpm => cross_command("pnpm")
269+
.arg("list")
270+
.args(packages)
271+
.args(["--json", "--depth", "0"])
272+
.current_dir(frontend_dir)
273+
.output()?,
274+
// Bun and Deno don't support `list` command
275+
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm")
276+
.arg("list")
277+
.args(packages)
278+
.args(["--json", "--depth", "0"])
279+
.current_dir(frontend_dir)
280+
.output()?,
281+
};
282+
283+
let mut versions = HashMap::new();
284+
let stdout = String::from_utf8_lossy(&output.stdout);
285+
if !output.status.success() {
286+
return Ok(versions);
287+
}
288+
289+
#[derive(Deserialize)]
290+
#[serde(rename_all = "camelCase")]
291+
struct ListOutput {
292+
#[serde(default)]
293+
dependencies: HashMap<String, ListDependency>,
294+
#[serde(default)]
295+
dev_dependencies: HashMap<String, ListDependency>,
296+
}
297+
298+
#[derive(Deserialize)]
299+
struct ListDependency {
300+
version: String,
301+
}
302+
303+
let json: ListOutput = serde_json::from_str(&stdout)?;
304+
for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) {
305+
let version = dependency.version;
306+
if let Ok(version) = semver::Version::parse(&version) {
307+
versions.insert(package, version);
308+
} else {
309+
log::error!("Failed to parse version `{version}` for NPM package `{package}`");
310+
}
311+
}
312+
Ok(versions)
313+
}
314+
}
315+
316+
fn yarn_package_versions(
317+
packages: &[String],
318+
frontend_dir: &Path,
319+
) -> crate::Result<HashMap<String, semver::Version>> {
320+
let output = cross_command("yarn")
321+
.arg("list")
322+
.args(packages)
323+
.args(["--json", "--depth", "0"])
324+
.current_dir(frontend_dir)
325+
.output()?;
326+
327+
let mut versions = HashMap::new();
328+
let stdout = String::from_utf8_lossy(&output.stdout);
329+
if !output.status.success() {
330+
return Ok(versions);
331+
}
332+
333+
#[derive(Deserialize)]
334+
struct YarnListOutput {
335+
data: YarnListOutputData,
336+
}
337+
338+
#[derive(Deserialize)]
339+
struct YarnListOutputData {
340+
trees: Vec<YarnListOutputDataTree>,
341+
}
342+
343+
#[derive(Deserialize)]
344+
struct YarnListOutputDataTree {
345+
name: String,
346+
}
347+
348+
for line in stdout.lines() {
349+
if let Ok(tree) = serde_json::from_str::<YarnListOutput>(line) {
350+
for tree in tree.data.trees {
351+
let Some((name, version)) = tree.name.rsplit_once('@') else {
352+
continue;
353+
};
354+
if let Ok(version) = semver::Version::parse(version) {
355+
versions.insert(name.to_owned(), version);
356+
} else {
357+
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
358+
}
359+
}
360+
return Ok(versions);
361+
}
362+
}
363+
364+
Ok(versions)
365+
}
366+
367+
fn yarn_berry_package_versions(
368+
packages: &[String],
369+
frontend_dir: &Path,
370+
) -> crate::Result<HashMap<String, semver::Version>> {
371+
let output = cross_command("yarn")
372+
.args(["info", "--json"])
373+
.current_dir(frontend_dir)
374+
.output()?;
375+
376+
let mut versions = HashMap::new();
377+
let stdout = String::from_utf8_lossy(&output.stdout);
378+
if !output.status.success() {
379+
return Ok(versions);
380+
}
381+
382+
#[derive(Deserialize)]
383+
struct YarnBerryInfoOutput {
384+
value: String,
385+
children: YarnBerryInfoOutputChildren,
386+
}
387+
388+
#[derive(Deserialize)]
389+
#[serde(rename_all = "PascalCase")]
390+
struct YarnBerryInfoOutputChildren {
391+
version: String,
392+
}
393+
394+
for line in stdout.lines() {
395+
if let Ok(info) = serde_json::from_str::<YarnBerryInfoOutput>(line) {
396+
let Some((name, _)) = info.value.rsplit_once('@') else {
397+
continue;
398+
};
399+
if !packages.iter().any(|package| package == name) {
400+
continue;
401+
}
402+
let version = info.children.version;
403+
if let Ok(version) = semver::Version::parse(&version) {
404+
versions.insert(name.to_owned(), version);
405+
} else {
406+
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
407+
}
408+
}
409+
}
410+
411+
Ok(versions)
257412
}

crates/tauri-cli/src/info/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ mod env_system;
2020
mod ios;
2121
mod packages_nodejs;
2222
mod packages_rust;
23-
mod plugins;
23+
pub mod plugins;
2424

2525
#[derive(Deserialize)]
2626
struct JsCliVersionMetadata {

crates/tauri-cli/src/info/packages_rust.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,18 @@
33
// SPDX-License-Identifier: MIT
44

55
use super::{ActionResult, SectionItem};
6-
use crate::{
7-
helpers::cargo_manifest::{
8-
crate_latest_version, crate_version, CargoLock, CargoManifest, CrateVersion,
9-
},
10-
interface::rust::get_workspace_dir,
6+
use crate::helpers::cargo_manifest::{
7+
cargo_manifest_and_lock, crate_latest_version, crate_version, CrateVersion,
118
};
129
use colored::Colorize;
13-
use std::fs::read_to_string;
1410
use std::path::{Path, PathBuf};
1511

1612
pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
1713
let mut items = Vec::new();
1814

1915
if tauri_dir.is_some() || frontend_dir.is_some() {
2016
if let Some(tauri_dir) = tauri_dir {
21-
let manifest: Option<CargoManifest> =
22-
if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
23-
toml::from_str(&manifest_contents).ok()
24-
} else {
25-
None
26-
};
27-
let lock: Option<CargoLock> = get_workspace_dir()
28-
.ok()
29-
.and_then(|p| read_to_string(p.join("Cargo.lock")).ok())
30-
.and_then(|s| toml::from_str(&s).ok());
31-
17+
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
3218
for dep in ["tauri", "tauri-build", "wry", "tao"] {
3319
let crate_version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
3420
let item = rust_section_item(dep, crate_version);

0 commit comments

Comments
 (0)