Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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.
34 changes: 33 additions & 1 deletion crates/tauri-cli/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use crate::{
bundle::BundleFormat,
helpers::{
self,
app_paths::tauri_dir,
app_paths::{frontend_dir, tauri_dir},
config::{get as get_config, ConfigHandle, FrontendDist},
npm::PackageManager,
},
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 aversion incompatibility is detected on an installed plugin.
///
/// Only use this when you are sure the mismatch is incorrectly detected as incompatible plugin versions can lead to unknown behavior.
#[clap(long)]
pub ignore_incompatible_plugins: bool,
}

pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
Expand Down Expand Up @@ -131,6 +137,32 @@ pub fn setup(
mobile: bool,
) -> Result<()> {
let tauri_path = tauri_dir();

log::info!("Looking up installed plugins to check incompatible versions...");
let installed_plugins = crate::info::plugins::installed_plugins(
frontend_dir(),
tauri_path,
PackageManager::from_project(frontend_dir()),
);
let incompatible_plugins = installed_plugins.incompatible();
if !incompatible_plugins.is_empty() {
let incompatible_text = incompatible_plugins
.iter()
.map(|p| {
format!(
"{} (v{}) : {} (v{})",
p.crate_name, p.crate_version, p.npm_name, p.npm_version
)
})
.collect::<Vec<_>>()
.join("\n");
if options.ignore_incompatible_plugins {
log::error!("Found incompatible Tauri plugins. Make sure the NPM and crate versions are on the same major/minor releases:\n{}", incompatible_text);
} else {
anyhow::bail!("Found incompatible Tauri plugins. Make sure the NPM and crate versions are on the same major/minor releases:\n{}", incompatible_text);
}
}

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

let config_guard = config.lock().unwrap();
Expand Down
24 changes: 24 additions & 0 deletions crates/tauri-cli/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
config::{
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
},
npm::PackageManager,
},
interface::{AppInterface, ExitReason, Interface},
CommandExt, ConfigValue, Result,
Expand Down Expand Up @@ -135,6 +136,29 @@ 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(|| {
let installed_plugins = crate::info::plugins::installed_plugins(
frontend_dir(),
tauri_path,
PackageManager::from_project(frontend_dir()),
);
let incompatible_plugins = installed_plugins.incompatible();
if !incompatible_plugins.is_empty() {
let incompatible_text = incompatible_plugins
.iter()
.map(|p| {
format!(
"{} (v{}) : {} (v{})",
p.crate_name, p.crate_version, p.npm_name, p.npm_version
)
})
.collect::<Vec<_>>()
.join("\n");
log::warn!("Found incompatible Tauri plugins. Make sure the NPM and crate versions are on the same major/minor releases:\n{}", incompatible_text);
}
});

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

if let Some(before_dev) = config
Expand Down
6 changes: 1 addition & 5 deletions crates/tauri-cli/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ pub fn resolve_tauri_path<P: AsRef<Path>>(path: P, crate_name: &str) -> PathBuf

pub fn cross_command(bin: &str) -> Command {
#[cfg(target_os = "windows")]
let cmd = {
let mut cmd = Command::new("cmd");
cmd.arg("/c").arg(bin);
cmd
};
let cmd = Command::new(format!("{bin}.cmd"));
#[cfg(not(target_os = "windows"))]
let cmd = Command::new(bin);
cmd
Expand Down
102 changes: 101 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 @@ -254,4 +255,103 @@ 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>> {
#[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 output = match self {
PackageManager::Yarn => {
let output = cross_command("yarn")
.args(["list", "--pattern"])
.arg(packages.join("|"))
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(HashMap::new());
}
#[derive(Deserialize)]
struct YarnListOutput {
data: YarnListOutputData,
}
#[derive(Deserialize)]
struct YarnListOutputData {
trees: Vec<YarnListOutputDataTree>,
}
#[derive(Deserialize)]
struct YarnListOutputDataTree {
name: String,
}
let mut versions = HashMap::new();
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);
}
}
return Ok(HashMap::new());
}
PackageManager::YarnBerry => cross_command("yarn")
.arg("info")
.args(packages)
.arg("--json")
.current_dir(frontend_dir)
.output()?,
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 stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(HashMap::new());
}
let json: ListOutput = serde_json::from_str(&stdout)?;
let mut versions = HashMap::new();
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)
}
}
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
87 changes: 87 additions & 0 deletions crates/tauri-cli/src/info/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT

use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
Expand All @@ -18,6 +19,92 @@ use crate::{

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

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

#[derive(Debug)]
pub struct InstalledPlugins(Vec<InstalledPlugin>);

impl InstalledPlugins {
pub fn incompatible(&self) -> Vec<&InstalledPlugin> {
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_plugins(
frontend_dir: &Path,
tauri_dir: &Path,
package_manager: PackageManager,
) -> InstalledPlugins {
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> = know_plugins
.keys()
.map(|plugin_name| format!("tauri-plugin-{plugin_name}"))
.collect();
let npm_names: Vec<String> = 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(InstalledPlugin {
npm_name,
npm_version,
crate_name,
crate_version,
})
})
.collect();

InstalledPlugins(installed_plugins)
}

pub fn items(
frontend_dir: Option<&PathBuf>,
tauri_dir: Option<&Path>,
Expand Down
6 changes: 6 additions & 0 deletions crates/tauri-cli/src/mobile/android/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ pub struct Options {
/// e.g. `tauri android build -- [runnerArgs]`.
#[clap(last(true))]
pub args: Vec<String>,
/// Do not error out if aversion incompatibility is detected on an installed plugin.
///
/// Only use this when you are sure the mismatch is incorrectly detected as incompatible plugin versions can lead to unknown behavior.
#[clap(long)]
pub ignore_incompatible_plugins: bool,
}

impl From<Options> for BuildOptions {
Expand All @@ -93,6 +98,7 @@ impl From<Options> for BuildOptions {
args: options.args,
ci: options.ci,
skip_stapling: false,
ignore_incompatible_plugins: options.ignore_incompatible_plugins,
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/tauri-cli/src/mobile/ios/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ pub struct Options {
/// e.g. `tauri ios build -- [runnerArgs]`.
#[clap(last(true))]
pub args: Vec<String>,
/// Do not error out if aversion incompatibility is detected on an installed plugin.
///
/// Only use this when you are sure the mismatch is incorrectly detected as incompatible plugin versions can lead to unknown behavior.
#[clap(long)]
pub ignore_incompatible_plugins: bool,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down Expand Up @@ -133,6 +138,7 @@ impl From<Options> for BuildOptions {
args: options.args,
ci: options.ci,
skip_stapling: false,
ignore_incompatible_plugins: options.ignore_incompatible_plugins,
}
}
}
Expand Down
Loading