Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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/check-plugin-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
---

Check installed plugin NPM/crate versions for incompatible releases.
19 changes: 18 additions & 1 deletion crates/tauri-cli/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use crate::{
bundle::BundleFormat,
helpers::{
self,
app_paths::tauri_dir,
app_paths::{frontend_dir, tauri_dir},
config::{get as get_config, ConfigHandle, FrontendDist},
},
info::plugins::check_mismatched_packages,
interface::{rust::get_cargo_target_dir, AppInterface, Interface},
ConfigValue, Result,
};
Expand Down Expand Up @@ -70,6 +71,11 @@ pub struct Options {
/// On subsequent runs, it's recommended to disable this setting again.
#[clap(long)]
pub skip_stapling: bool,
/// 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(mut options: Options, verbosity: u8) -> Result<()> {
Expand Down Expand Up @@ -131,6 +137,17 @@ pub fn setup(
mobile: bool,
) -> Result<()> {
let tauri_path = tauri_dir();

let ignore_version_mismatches = options.ignore_version_mismatches;
std::thread::spawn(move || {
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
log::error!("{error}");
if !ignore_version_mismatches {
std::process::exit(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to stop the spawned processes (e.g. vite, cargo) though, feels like a pain to deal with 😂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right.. no easy way to deal with this unless we go with tokio tasks, easier to have graceful shutdown :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's either that or get back to sync (which is ok imo, build isn't run as often - and let's not forget cargo takes 500s while our check takes just 1 😂 )

Copy link
Contributor

@Legend-Master Legend-Master Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's either that or get back to sync (which is ok imo, build isn't run as often - and let's not forget cargo takes 500s while our check takes just 1 😂 )

Yeah, ~1 second is acceptable, let's use the sync version for now, sigh, I wish the cargo build doesn't take that long 😂

Just for the reference time with release build, it took around 500ms to check in plugins-workspace and ~700ms to check in my own project (with npm list taking up almost all the time) on my machine with an 11th gen i5 11400

I honestly don't know why those js package managers are so slow (maybe because the js file sizes are too big so parsing them could take a while?), pnpm --version takes 300-400ms, similar numbers for npm, with our cli, tauri --version takes only 20-50ms

}
}
});

set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;

let config_guard = config.lock().unwrap();
Expand Down
8 changes: 8 additions & 0 deletions crates/tauri-cli/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
},
},
info::plugins::check_mismatched_packages,
interface::{AppInterface, ExitReason, Interface},
CommandExt, ConfigValue, Result,
};
Expand Down Expand Up @@ -135,6 +136,13 @@ fn command_internal(mut options: Options) -> Result<()> {

pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHandle) -> Result<()> {
let tauri_path = tauri_dir();

std::thread::spawn(|| {
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
log::error!("{error}");
}
});

set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;

if let Some(before_dev) = config
Expand Down
157 changes: 156 additions & 1 deletion crates/tauri-cli/src/helpers/npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
// SPDX-License-Identifier: MIT

use anyhow::Context;
use serde::Deserialize;

use crate::helpers::cross_command;
use std::{fmt::Display, path::Path, process::Command};
use std::{collections::HashMap, fmt::Display, path::Path, process::Command};

pub fn manager_version(package_manager: &str) -> Option<String> {
cross_command(package_manager)
Expand Down Expand Up @@ -197,6 +198,7 @@ impl PackageManager {
Ok(())
}

// TODO: Use `current_package_versions` as much as possible for better speed
pub fn current_package_version<P: AsRef<Path>>(
&self,
name: &str,
Expand Down Expand Up @@ -254,4 +256,157 @@ impl PackageManager {
Ok(None)
}
}

pub fn current_package_versions(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to migrate the other ones to use this one as much as possible to speed things up, could push that to the future though

&self,
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = match self {
PackageManager::Yarn => return yarn_package_versions(packages, frontend_dir),
PackageManager::YarnBerry => return yarn_berry_package_versions(packages, frontend_dir),
PackageManager::Pnpm => cross_command("pnpm")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()?,
// Bun and Deno don't support `list` command
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()?,
};

let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListOutput {
#[serde(default)]
dependencies: HashMap<String, ListDependency>,
#[serde(default)]
dev_dependencies: HashMap<String, ListDependency>,
}

#[derive(Deserialize)]
struct ListDependency {
version: String,
}

let json: ListOutput = serde_json::from_str(&stdout)?;
for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) {
let version = dependency.version;
if let Ok(version) = semver::Version::parse(&version) {
versions.insert(package, version);
} else {
log::error!("Failed to parse version `{version}` for NPM package `{package}`");
}
}
Ok(versions)
}
}

fn yarn_package_versions(
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = cross_command("yarn")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()?;

let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}

#[derive(Deserialize)]
struct YarnListOutput {
data: YarnListOutputData,
}

#[derive(Deserialize)]
struct YarnListOutputData {
trees: Vec<YarnListOutputDataTree>,
}

#[derive(Deserialize)]
struct YarnListOutputDataTree {
name: String,
}

for line in stdout.lines() {
if let Ok(tree) = serde_json::from_str::<YarnListOutput>(line) {
for tree in tree.data.trees {
let Some((name, version)) = tree.name.rsplit_once('@') else {
continue;
};
if let Ok(version) = semver::Version::parse(version) {
versions.insert(name.to_owned(), version);
} else {
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
}
}
return Ok(versions);
}
}

Ok(versions)
}

fn yarn_berry_package_versions(
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = cross_command("yarn")
.args(["info", "--json"])
.current_dir(frontend_dir)
.output()?;

let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}

#[derive(Deserialize)]
struct YarnBerryInfoOutput {
value: String,
children: YarnBerryInfoOutputChildren,
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct YarnBerryInfoOutputChildren {
version: String,
}

for line in stdout.lines() {
if let Ok(info) = serde_json::from_str::<YarnBerryInfoOutput>(line) {
let Some((name, _)) = info.value.rsplit_once('@') else {
continue;
};
if !packages.iter().any(|package| package == name) {
continue;
}
let version = info.children.version;
if let Ok(version) = semver::Version::parse(&version) {
versions.insert(name.to_owned(), version);
} else {
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
}
}
}

Ok(versions)
}
2 changes: 1 addition & 1 deletion crates/tauri-cli/src/info/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mod env_system;
mod ios;
mod packages_nodejs;
mod packages_rust;
mod plugins;
pub mod plugins;

#[derive(Deserialize)]
struct JsCliVersionMetadata {
Expand Down
121 changes: 120 additions & 1 deletion crates/tauri-cli/src/info/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// SPDX-License-Identifier: MIT

use std::{
fs,
collections::HashMap,
fs, iter,
path::{Path, PathBuf},
};

Expand All @@ -17,6 +18,99 @@ use crate::{
};

use super::{packages_nodejs, packages_rust, SectionItem};
use anyhow::anyhow;

#[derive(Debug)]
pub struct InstalledPackage {
pub crate_name: String,
pub npm_name: String,
pub crate_version: semver::Version,
pub npm_version: semver::Version,
}

#[derive(Debug)]
pub struct InstalledPackages(Vec<InstalledPackage>);

impl InstalledPackages {
pub fn mismatched(&self) -> Vec<&InstalledPackage> {
self
.0
.iter()
.filter(|p| {
p.crate_version.major != p.npm_version.major || p.crate_version.minor != p.npm_version.minor
})
.collect()
}
}

pub fn installed_tauri_packages(
frontend_dir: &Path,
tauri_dir: &Path,
package_manager: PackageManager,
) -> InstalledPackages {
let manifest: Option<CargoManifest> =
if let Ok(manifest_contents) = fs::read_to_string(tauri_dir.join("Cargo.toml")) {
toml::from_str(&manifest_contents).ok()
} else {
None
};

let lock: Option<CargoLock> = get_workspace_dir()
.ok()
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
.and_then(|s| toml::from_str(&s).ok());

let know_plugins = helpers::plugins::known_plugins();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to also include @tauri-app/api here? And maybe also @tauri-app/cli

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well we do not have a good way to match those..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd need to agree on whether to match the tauri package versions and which packages and patch or minor and then actually do that 🙃

sooner or later we'll probably have to tackle this.

Copy link
Contributor

@Legend-Master Legend-Master Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tauri-app/api and tauri will need to match in minor versions at least

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except that we failed with that multiple times (especially in v1 at the end). again, i'm not against it but if we add the check here we must start to take this serious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think before we actually run the check, we should sync our versions for a while, otherwise we suddenly break most builds

let's sync tauri, api and cli
and maybe pin minors for inner dependencies too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could at least add a check that checks the version of tauri and @tauri-app/api here?

let crate_names: Vec<String> = iter::once("tauri".to_owned())
.chain(
know_plugins
.keys()
.map(|plugin_name| format!("tauri-plugin-{plugin_name}")),
)
.collect();
let npm_names: Vec<String> = iter::once("@tauri-apps/api".to_owned())
.chain(
know_plugins
.keys()
.map(|plugin_name| format!("@tauri-apps/plugin-{plugin_name}")),
)
.collect();

let mut rust_plugins: HashMap<String, semver::Version> = crate_names
.iter()
.filter_map(|crate_name| {
let crate_version =
crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), crate_name).version?;
let crate_version = semver::Version::parse(&crate_version)
.inspect_err(|_| {
log::error!("Failed to parse version `{crate_version}` for crate `{crate_name}`");
})
.ok()?;
Some((crate_name.clone(), crate_version))
})
.collect();

let mut npm_plugins = package_manager
.current_package_versions(&npm_names, frontend_dir)
.unwrap_or_default();

let installed_plugins = crate_names
.iter()
.zip(npm_names.iter())
.filter_map(|(crate_name, npm_name)| {
let (crate_name, crate_version) = rust_plugins.remove_entry(crate_name)?;
let (npm_name, npm_version) = npm_plugins.remove_entry(npm_name)?;
Some(InstalledPackage {
npm_name,
npm_version,
crate_name,
crate_version,
})
})
.collect();

InstalledPackages(installed_plugins)
}

pub fn items(
frontend_dir: Option<&PathBuf>,
Expand Down Expand Up @@ -67,3 +161,28 @@ pub fn items(

items
}

pub fn check_mismatched_packages(frontend_dir: &Path, tauri_path: &Path) -> crate::Result<()> {
let installed_packages = installed_tauri_packages(
frontend_dir,
tauri_path,
PackageManager::from_project(frontend_dir),
);
let mismatched_packages = installed_packages.mismatched();
if mismatched_packages.is_empty() {
return Ok(());
}
let mismatched_text = mismatched_packages
.iter()
.map(
|InstalledPackage {
crate_name,
crate_version,
npm_name,
npm_version,
}| format!("{crate_name} (v{crate_version}) : {npm_name} (v{npm_version})"),
)
.collect::<Vec<_>>()
.join("\n");
Err(anyhow!("Found version mismatched Tauri packages. Make sure the NPM and crate versions are on the same major/minor releases:\n{mismatched_text}"))
}
Loading
Loading