-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat(cli): check plugin versions for incompatibilities #13993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
c093cb2
c6e63a0
8394508
8967d4e
23e9f62
87f59e4
e3efd13
d0cd407
5b1a540
c473511
958c7cf
6397418
cac22e4
3374328
99eac04
201e7c1
202a782
b8690ef
5b0833a
e66fbb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -254,4 +255,157 @@ impl PackageManager { | |
Ok(None) | ||
} | ||
} | ||
|
||
pub fn current_package_versions( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
// SPDX-License-Identifier: MIT | ||
|
||
use std::{ | ||
collections::HashMap, | ||
fs, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to also include There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well we do not have a good way to match those.. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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>, | ||
|
Uh oh!
There was an error while loading. Please reload this page.