فهرست منبع

feat(cli): add migration from 2.0.0-beta to 2.0.0-rc (#10395)

* refactor(cli): check tauri version on migration

* rc migration

* license headers

* fix tests

* add path

* update schema
Lucas Fernandes Nogueira 1 سال پیش
والد
کامیت
d5511c3117

+ 6 - 0
.changes/rc-migration.md

@@ -0,0 +1,6 @@
+---
+"tauri-cli": patch:feat
+"@tauri-apps/cli": patch:feat
+---
+
+Added migration from `2.0.0-beta` to `2.0.0-rc`.

+ 9 - 9
core/tauri-config-schema/schema.json

@@ -2551,7 +2551,7 @@
           ]
         },
         "changelog": {
-          "description": "Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See\n https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes",
+          "description": "Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See\n <https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes>",
           "type": [
             "string",
             "null"
@@ -2565,28 +2565,28 @@
           ]
         },
         "preInstallScript": {
-          "description": "Path to script that will be executed before the package is unpacked. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed before the package is unpacked. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postInstallScript": {
-          "description": "Path to script that will be executed after the package is unpacked. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed after the package is unpacked. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "preRemoveScript": {
-          "description": "Path to script that will be executed before the package is removed. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed before the package is removed. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postRemoveScript": {
-          "description": "Path to script that will be executed after the package is removed. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed after the package is removed. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
@@ -2667,28 +2667,28 @@
           ]
         },
         "preInstallScript": {
-          "description": "Path to script that will be executed before the package is unpacked. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed before the package is unpacked. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postInstallScript": {
-          "description": "Path to script that will be executed after the package is unpacked. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed after the package is unpacked. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "preRemoveScript": {
-          "description": "Path to script that will be executed before the package is removed. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed before the package is removed. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postRemoveScript": {
-          "description": "Path to script that will be executed after the package is removed. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed after the package is removed. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"

+ 1 - 0
tooling/cli/Cargo.lock

@@ -5181,6 +5181,7 @@ dependencies = [
  "toml_edit 0.22.6",
  "ureq",
  "url",
+ "walkdir",
  "windows-sys 0.52.0",
 ]
 

+ 1 - 0
tooling/cli/Cargo.toml

@@ -98,6 +98,7 @@ oxc_allocator = "0.16"
 oxc_ast = "0.16"
 magic_string = "0.3"
 phf = { version = "0.11", features = ["macros"] }
+walkdir = "2"
 
 [target."cfg(windows)".dependencies.windows-sys]
 version = "0.52"

+ 9 - 9
tooling/cli/schema.json

@@ -2551,7 +2551,7 @@
           ]
         },
         "changelog": {
-          "description": "Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See\n https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes",
+          "description": "Path of the uncompressed Changelog file, to be stored at /usr/share/doc/package-name/changelog.gz. See\n <https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes>",
           "type": [
             "string",
             "null"
@@ -2565,28 +2565,28 @@
           ]
         },
         "preInstallScript": {
-          "description": "Path to script that will be executed before the package is unpacked. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed before the package is unpacked. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postInstallScript": {
-          "description": "Path to script that will be executed after the package is unpacked. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed after the package is unpacked. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "preRemoveScript": {
-          "description": "Path to script that will be executed before the package is removed. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed before the package is removed. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postRemoveScript": {
-          "description": "Path to script that will be executed after the package is removed. See\n https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html",
+          "description": "Path to script that will be executed after the package is removed. See\n <https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html>",
           "type": [
             "string",
             "null"
@@ -2667,28 +2667,28 @@
           ]
         },
         "preInstallScript": {
-          "description": "Path to script that will be executed before the package is unpacked. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed before the package is unpacked. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postInstallScript": {
-          "description": "Path to script that will be executed after the package is unpacked. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed after the package is unpacked. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "preRemoveScript": {
-          "description": "Path to script that will be executed before the package is removed. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed before the package is removed. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"
           ]
         },
         "postRemoveScript": {
-          "description": "Path to script that will be executed after the package is removed. See\n http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html",
+          "description": "Path to script that will be executed after the package is removed. See\n <http://ftp.rpm.org/max-rpm/s1-rpm-inside-scripts.html>",
           "type": [
             "string",
             "null"

+ 1 - 1
tooling/cli/src/add.rs

@@ -146,7 +146,7 @@ pub fn command(options: Options) -> Result<()> {
         (None, None, None, None) => npm_name,
         _ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
       };
-      manager.install(&[npm_spec])?;
+      manager.install(&[npm_spec], &tauri_dir)?;
     }
 
     let _ = acl::permission::add::command(acl::permission::add::Options {

+ 172 - 0
tooling/cli/src/helpers/cargo_manifest.rs

@@ -0,0 +1,172 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use serde::Deserialize;
+
+use std::{
+  collections::HashMap,
+  fmt::Write,
+  fs::read_to_string,
+  path::{Path, PathBuf},
+};
+
+#[derive(Clone, Deserialize)]
+pub struct CargoLockPackage {
+  pub name: String,
+  pub version: String,
+  pub source: Option<String>,
+}
+
+#[derive(Deserialize)]
+pub struct CargoLock {
+  pub package: Vec<CargoLockPackage>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct CargoManifestDependencyPackage {
+  pub version: Option<String>,
+  pub git: Option<String>,
+  pub branch: Option<String>,
+  pub rev: Option<String>,
+  pub path: Option<PathBuf>,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(untagged)]
+pub enum CargoManifestDependency {
+  Version(String),
+  Package(CargoManifestDependencyPackage),
+}
+
+#[derive(Deserialize)]
+pub struct CargoManifestPackage {
+  pub version: String,
+}
+
+#[derive(Deserialize)]
+pub struct CargoManifest {
+  pub package: CargoManifestPackage,
+  pub dependencies: HashMap<String, CargoManifestDependency>,
+}
+
+pub struct CrateVersion {
+  pub version: String,
+  pub found_crate_versions: Vec<String>,
+}
+
+pub fn crate_version(
+  tauri_dir: &Path,
+  manifest: Option<&CargoManifest>,
+  lock: Option<&CargoLock>,
+  name: &str,
+) -> CrateVersion {
+  let crate_lock_packages: Vec<CargoLockPackage> = lock
+    .as_ref()
+    .map(|lock| {
+      lock
+        .package
+        .iter()
+        .filter(|p| p.name == name)
+        .cloned()
+        .collect()
+    })
+    .unwrap_or_default();
+  let (crate_version_string, found_crate_versions) =
+    match (&manifest, &lock, crate_lock_packages.len()) {
+      (Some(_manifest), Some(_lock), 1) => {
+        let crate_lock_package = crate_lock_packages.first().unwrap();
+        let version_string = if let Some(s) = &crate_lock_package.source {
+          if s.starts_with("git") {
+            format!("{} ({})", s, crate_lock_package.version)
+          } else {
+            crate_lock_package.version.clone()
+          }
+        } else {
+          crate_lock_package.version.clone()
+        };
+        (version_string, vec![crate_lock_package.version.clone()])
+      }
+      (None, Some(_lock), 1) => {
+        let crate_lock_package = crate_lock_packages.first().unwrap();
+        let version_string = if let Some(s) = &crate_lock_package.source {
+          if s.starts_with("git") {
+            format!("{} ({})", s, crate_lock_package.version)
+          } else {
+            crate_lock_package.version.clone()
+          }
+        } else {
+          crate_lock_package.version.clone()
+        };
+        (
+          format!("{version_string} (no manifest)"),
+          vec![crate_lock_package.version.clone()],
+        )
+      }
+      _ => {
+        let mut found_crate_versions = Vec::new();
+        let mut is_git = false;
+        let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
+          Some(tauri) => match tauri {
+            CargoManifestDependency::Version(v) => {
+              found_crate_versions.push(v.clone());
+              v
+            }
+            CargoManifestDependency::Package(p) => {
+              if let Some(v) = p.version {
+                found_crate_versions.push(v.clone());
+                v
+              } else if let Some(p) = p.path {
+                let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
+                let v = match read_to_string(manifest_path)
+                  .map_err(|_| ())
+                  .and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
+                {
+                  Ok(manifest) => manifest.package.version,
+                  Err(_) => "unknown version".to_string(),
+                };
+                format!("path:{p:?} [{v}]")
+              } else if let Some(g) = p.git {
+                is_git = true;
+                let mut v = format!("git:{g}");
+                if let Some(branch) = p.branch {
+                  let _ = write!(v, "&branch={branch}");
+                } else if let Some(rev) = p.rev {
+                  let _ = write!(v, "#{rev}");
+                }
+                v
+              } else {
+                "unknown manifest".to_string()
+              }
+            }
+          },
+          None => "no manifest".to_string(),
+        };
+
+        let lock_version = match (lock, crate_lock_packages.is_empty()) {
+          (Some(_lock), false) => crate_lock_packages
+            .iter()
+            .map(|p| p.version.clone())
+            .collect::<Vec<String>>()
+            .join(", "),
+          (Some(_lock), true) => "unknown lockfile".to_string(),
+          _ => "no lockfile".to_string(),
+        };
+
+        (
+          format!(
+            "{} {}({})",
+            manifest_version,
+            if is_git { "(git manifest)" } else { "" },
+            lock_version
+          ),
+          found_crate_versions,
+        )
+      }
+    };
+
+  CrateVersion {
+    found_crate_versions,
+    version: crate_version_string,
+  }
+}

+ 1 - 0
tooling/cli/src/helpers/mod.rs

@@ -4,6 +4,7 @@
 
 pub mod app_paths;
 pub mod cargo;
+pub mod cargo_manifest;
 pub mod config;
 pub mod flock;
 pub mod framework;

+ 69 - 1
tooling/cli/src/helpers/npm.rs

@@ -81,7 +81,7 @@ impl PackageManager {
     }
   }
 
-  pub fn install(&self, dependencies: &[String]) -> crate::Result<()> {
+  pub fn install<P: AsRef<Path>>(&self, dependencies: &[String], app_dir: P) -> crate::Result<()> {
     let dependencies_str = if dependencies.len() > 1 {
       "dependencies"
     } else {
@@ -100,6 +100,7 @@ impl PackageManager {
       .cross_command()
       .arg("add")
       .args(dependencies)
+      .current_dir(app_dir)
       .status()
       .with_context(|| format!("failed to run {self}"))?;
 
@@ -109,4 +110,71 @@ impl PackageManager {
 
     Ok(())
   }
+
+  pub fn current_package_version<P: AsRef<Path>>(
+    &self,
+    name: &str,
+    app_dir: P,
+  ) -> crate::Result<Option<String>> {
+    let (output, regex) = match self {
+      PackageManager::Yarn => (
+        cross_command("yarn")
+          .args(["list", "--pattern"])
+          .arg(name)
+          .args(["--depth", "0"])
+          .current_dir(app_dir)
+          .output()?,
+        None,
+      ),
+      PackageManager::YarnBerry => (
+        cross_command("yarn")
+          .arg("info")
+          .arg(name)
+          .arg("--json")
+          .current_dir(app_dir)
+          .output()?,
+        Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
+      ),
+      PackageManager::Npm => (
+        cross_command("npm")
+          .arg("list")
+          .arg(name)
+          .args(["version", "--depth", "0"])
+          .current_dir(app_dir)
+          .output()?,
+        None,
+      ),
+      PackageManager::Pnpm => (
+        cross_command("pnpm")
+          .arg("list")
+          .arg(name)
+          .args(["--parseable", "--depth", "0"])
+          .current_dir(app_dir)
+          .output()?,
+        None,
+      ),
+      // Bun doesn't support `list` command
+      PackageManager::Bun => (
+        cross_command("npm")
+          .arg("list")
+          .arg(name)
+          .args(["version", "--depth", "0"])
+          .current_dir(app_dir)
+          .output()?,
+        None,
+      ),
+    };
+    if output.status.success() {
+      let stdout = String::from_utf8_lossy(&output.stdout);
+      let regex = regex.unwrap_or_else(|| regex::Regex::new("@(\\d[\\da-zA-Z\\-\\.]+)").unwrap());
+      Ok(
+        regex
+          .captures_iter(&stdout)
+          .last()
+          .and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
+      )
+    } else {
+      Ok(None)
+    }
+  }
 }

+ 3 - 69
tooling/cli/src/info/packages_nodejs.rs

@@ -6,7 +6,7 @@ use super::SectionItem;
 use super::{env_nodejs::manager_version, VersionMetadata};
 use colored::Colorize;
 use serde::Deserialize;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 
 use crate::helpers::{cross_command, npm::PackageManager};
 
@@ -87,73 +87,6 @@ fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<S
   }
 }
 
-fn npm_package_version<P: AsRef<Path>>(
-  pm: &PackageManager,
-  name: &str,
-  app_dir: P,
-) -> crate::Result<Option<String>> {
-  let (output, regex) = match pm {
-    PackageManager::Yarn => (
-      cross_command("yarn")
-        .args(["list", "--pattern"])
-        .arg(name)
-        .args(["--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-    PackageManager::YarnBerry => (
-      cross_command("yarn")
-        .arg("info")
-        .arg(name)
-        .arg("--json")
-        .current_dir(app_dir)
-        .output()?,
-      Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
-    ),
-    PackageManager::Npm => (
-      cross_command("npm")
-        .arg("list")
-        .arg(name)
-        .args(["version", "--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-    PackageManager::Pnpm => (
-      cross_command("pnpm")
-        .arg("list")
-        .arg(name)
-        .args(["--parseable", "--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-    // Bun doesn't support `list` command
-    PackageManager::Bun => (
-      cross_command("npm")
-        .arg("list")
-        .arg(name)
-        .args(["version", "--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-  };
-  if output.status.success() {
-    let stdout = String::from_utf8_lossy(&output.stdout);
-    let regex = regex.unwrap_or_else(|| regex::Regex::new("@(\\d[\\da-zA-Z\\-\\.]+)").unwrap());
-    Ok(
-      regex
-        .captures_iter(&stdout)
-        .last()
-        .and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
-    )
-  } else {
-    Ok(None)
-  }
-}
-
 fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
   let mut use_npm = false;
   let mut use_pnpm = false;
@@ -244,7 +177,8 @@ pub fn items(app_dir: Option<&PathBuf>, metadata: &VersionMetadata) -> Vec<Secti
       let app_dir = app_dir.clone();
       let item = SectionItem::new().action(move || {
         let version = version.clone().unwrap_or_else(|| {
-          npm_package_version(&package_manager, package, &app_dir)
+          package_manager
+            .current_package_version(package, &app_dir)
             .unwrap_or_default()
             .unwrap_or_default()
         });

+ 28 - 178
tooling/cli/src/info/packages_rust.rs

@@ -3,53 +3,14 @@
 // SPDX-License-Identifier: MIT
 
 use super::{ActionResult, SectionItem};
-use crate::interface::rust::get_workspace_dir;
+use crate::{
+  helpers::cargo_manifest::{crate_version, CargoLock, CargoManifest},
+  interface::rust::get_workspace_dir,
+};
 use colored::Colorize;
-use serde::Deserialize;
-use std::collections::HashMap;
-use std::fmt::Write;
 use std::fs::read_to_string;
 use std::path::{Path, PathBuf};
 
-#[derive(Clone, Deserialize)]
-struct CargoLockPackage {
-  name: String,
-  version: String,
-  source: Option<String>,
-}
-
-#[derive(Deserialize)]
-struct CargoLock {
-  package: Vec<CargoLockPackage>,
-}
-
-#[derive(Clone, Deserialize)]
-struct CargoManifestDependencyPackage {
-  version: Option<String>,
-  git: Option<String>,
-  branch: Option<String>,
-  rev: Option<String>,
-  path: Option<PathBuf>,
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(untagged)]
-enum CargoManifestDependency {
-  Version(String),
-  Package(CargoManifestDependencyPackage),
-}
-
-#[derive(Deserialize)]
-struct CargoManifestPackage {
-  version: String,
-}
-
-#[derive(Deserialize)]
-struct CargoManifest {
-  package: CargoManifestPackage,
-  dependencies: HashMap<String, CargoManifestDependency>,
-}
-
 fn crate_latest_version(name: &str) -> Option<String> {
   let url = format!("https://docs.rs/crate/{name}/");
   match ureq::get(&url).call() {
@@ -61,138 +22,6 @@ fn crate_latest_version(name: &str) -> Option<String> {
   }
 }
 
-fn crate_version(
-  tauri_dir: &Path,
-  manifest: Option<&CargoManifest>,
-  lock: Option<&CargoLock>,
-  name: &str,
-) -> (String, Option<String>) {
-  let crate_lock_packages: Vec<CargoLockPackage> = lock
-    .as_ref()
-    .map(|lock| {
-      lock
-        .package
-        .iter()
-        .filter(|p| p.name == name)
-        .cloned()
-        .collect()
-    })
-    .unwrap_or_default();
-  let (crate_version_string, found_crate_versions) =
-    match (&manifest, &lock, crate_lock_packages.len()) {
-      (Some(_manifest), Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
-          }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (version_string, vec![crate_lock_package.version.clone()])
-      }
-      (None, Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
-          }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (
-          format!("{version_string} (no manifest)"),
-          vec![crate_lock_package.version.clone()],
-        )
-      }
-      _ => {
-        let mut found_crate_versions = Vec::new();
-        let mut is_git = false;
-        let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
-          Some(tauri) => match tauri {
-            CargoManifestDependency::Version(v) => {
-              found_crate_versions.push(v.clone());
-              v
-            }
-            CargoManifestDependency::Package(p) => {
-              if let Some(v) = p.version {
-                found_crate_versions.push(v.clone());
-                v
-              } else if let Some(p) = p.path {
-                let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
-                let v = match read_to_string(manifest_path)
-                  .map_err(|_| ())
-                  .and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
-                {
-                  Ok(manifest) => manifest.package.version,
-                  Err(_) => "unknown version".to_string(),
-                };
-                format!("path:{p:?} [{v}]")
-              } else if let Some(g) = p.git {
-                is_git = true;
-                let mut v = format!("git:{g}");
-                if let Some(branch) = p.branch {
-                  let _ = write!(v, "&branch={branch}");
-                } else if let Some(rev) = p.rev {
-                  let _ = write!(v, "#{rev}");
-                }
-                v
-              } else {
-                "unknown manifest".to_string()
-              }
-            }
-          },
-          None => "no manifest".to_string(),
-        };
-
-        let lock_version = match (lock, crate_lock_packages.is_empty()) {
-          (Some(_lock), false) => crate_lock_packages
-            .iter()
-            .map(|p| p.version.clone())
-            .collect::<Vec<String>>()
-            .join(", "),
-          (Some(_lock), true) => "unknown lockfile".to_string(),
-          _ => "no lockfile".to_string(),
-        };
-
-        (
-          format!(
-            "{} {}({})",
-            manifest_version,
-            if is_git { "(git manifest)" } else { "" },
-            lock_version
-          ),
-          found_crate_versions,
-        )
-      }
-    };
-
-  let crate_version = found_crate_versions
-    .into_iter()
-    .map(|v| semver::Version::parse(&v).ok())
-    .max();
-  let suffix = match (crate_version, crate_latest_version(name)) {
-    (Some(Some(version)), Some(target_version)) => {
-      let target_version = semver::Version::parse(&target_version).unwrap();
-      if version < target_version {
-        Some(format!(
-          " ({}, latest: {})",
-          "outdated".yellow(),
-          target_version.to_string().green()
-        ))
-      } else {
-        None
-      }
-    }
-    _ => None,
-  };
-  (crate_version_string, suffix)
-}
-
 pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
   let mut items = Vec::new();
 
@@ -210,13 +39,34 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Section
         .and_then(|s| toml::from_str(&s).ok());
 
       for dep in ["tauri", "tauri-build", "wry", "tao"] {
-        let (version_string, version_suffix) =
-          crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
+        let version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
+        let crate_version = version
+          .found_crate_versions
+          .into_iter()
+          .map(|v| semver::Version::parse(&v).ok())
+          .max();
+
+        let version_suffix = match (crate_version, crate_latest_version(dep)) {
+          (Some(Some(version)), Some(target_version)) => {
+            let target_version = semver::Version::parse(&target_version).unwrap();
+            if version < target_version {
+              Some(format!(
+                " ({}, latest: {})",
+                "outdated".yellow(),
+                target_version.to_string().green()
+              ))
+            } else {
+              None
+            }
+          }
+          _ => None,
+        };
+
         let item = SectionItem::new().description(format!(
           "{} {}: {}{}",
           dep,
           "[RUST]".dimmed(),
-          version_string,
+          version.version,
           version_suffix
             .clone()
             .map(|s| format!(",{s}"))

+ 13 - 9
tooling/cli/src/interface/rust/manifest.rs

@@ -96,6 +96,18 @@ pub fn read_manifest(manifest_path: &Path) -> crate::Result<(Document, String)>
   Ok((manifest, manifest_str))
 }
 
+pub fn serialize_manifest(manifest: &Document) -> String {
+  manifest
+    .to_string()
+    // apply some formatting fixes
+    .replace(r#"" ,features =["#, r#"", features = ["#)
+    .replace(r#"" , features"#, r#"", features"#)
+    .replace("]}", "] }")
+    .replace("={", "= {")
+    .replace("=[", "= [")
+    .replace(r#"",""#, r#"", ""#)
+}
+
 pub fn toml_array(features: &HashSet<String>) -> Array {
   let mut f = Array::default();
   let mut features: Vec<String> = features.iter().map(|f| f.to_string()).collect();
@@ -301,15 +313,7 @@ pub fn rewrite_manifest(config: &Config) -> crate::Result<(Manifest, bool)> {
     .unwrap()
     .features;
 
-  let new_manifest_str = manifest
-    .to_string()
-    // apply some formatting fixes
-    .replace(r#"" ,features =["#, r#"", features = ["#)
-    .replace(r#"" , features"#, r#"", features"#)
-    .replace("]}", "] }")
-    .replace("={", "= {")
-    .replace("=[", "= [")
-    .replace(r#"",""#, r#"", ""#);
+  let new_manifest_str = serialize_manifest(&manifest);
 
   if persist && original_manifest_str != new_manifest_str {
     let mut manifest_file =

+ 6 - 0
tooling/cli/src/migrate/migrations/mod.rs

@@ -0,0 +1,6 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+pub mod v1;
+pub mod v2_rc;

+ 0 - 0
tooling/cli/src/migrate/config.rs → tooling/cli/src/migrate/migrations/v1/config.rs


+ 24 - 1
tooling/cli/src/migrate/frontend.rs → tooling/cli/src/migrate/migrations/v1/frontend.rs

@@ -58,11 +58,34 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
   let mut new_npm_packages = Vec::new();
   let mut new_cargo_packages = Vec::new();
 
+  let pre = env!("CARGO_PKG_VERSION_PRE");
+  let npm_version = if pre.is_empty() {
+    format!("{}.0.0", env!("CARGO_PKG_VERSION_MAJOR"))
+  } else {
+    format!(
+      "{}.{}.{}-{}.0",
+      env!("CARGO_PKG_VERSION_MAJOR"),
+      env!("CARGO_PKG_VERSION_MINOR"),
+      env!("CARGO_PKG_VERSION_PATCH"),
+      pre.split('.').next().unwrap()
+    )
+  };
+
   let pm = PackageManager::from_project(app_dir)
     .into_iter()
     .next()
     .unwrap_or(PackageManager::Npm);
 
+  for pkg in ["@tauri-apps/cli", "@tauri-apps/api"] {
+    let version = pm
+      .current_package_version(pkg, app_dir)
+      .unwrap_or_default()
+      .unwrap_or_default();
+    if version.starts_with("1") {
+      new_npm_packages.push(format!("{pkg}@^{npm_version}"));
+    }
+  }
+
   for entry in walk_builder(app_dir).build().flatten() {
     if entry.file_type().map(|t| t.is_file()).unwrap_or_default() {
       let path = entry.path();
@@ -86,7 +109,7 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
   new_npm_packages.sort();
   new_npm_packages.dedup();
   if !new_npm_packages.is_empty() {
-    pm.install(&new_npm_packages)
+    pm.install(&new_npm_packages, app_dir)
       .context("Error installing new npm packages")?;
   }
 

+ 9 - 18
tooling/cli/src/migrate/manifest.rs → tooling/cli/src/migrate/migrations/v1/manifest.rs

@@ -2,14 +2,17 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use crate::{interface::rust::manifest::read_manifest, Result};
+use crate::{
+  interface::rust::manifest::{read_manifest, serialize_manifest},
+  Result,
+};
 
 use anyhow::Context;
 use itertools::Itertools;
 use tauri_utils_v1::config::Allowlist;
 use toml_edit::{Document, Entry, Item, Table, TableLike, Value};
 
-use std::{fs::File, io::Write, path::Path};
+use std::path::Path;
 
 const CRATE_TYPES: [&str; 3] = ["lib", "staticlib", "cdylib"];
 
@@ -18,20 +21,8 @@ pub fn migrate(tauri_dir: &Path) -> Result<()> {
   let (mut manifest, _) = read_manifest(&manifest_path)?;
   migrate_manifest(&mut manifest)?;
 
-  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("=[", "= [")
-      .as_bytes(),
-  )?;
-  manifest_file.flush()?;
+  std::fs::write(&manifest_path, serialize_manifest(&manifest))
+    .context("failed to rewrite Cargo manifest")?;
 
   Ok(())
 }
@@ -44,7 +35,7 @@ fn migrate_manifest(manifest: &mut Document) -> Result<()> {
     .entry("dependencies")
     .or_insert(Item::Table(Table::new()))
     .as_table_mut()
-    .expect("manifest dependencies isn't a table");
+    .context("manifest dependencies isn't a table")?;
 
   migrate_dependency(dependencies, "tauri", &version, &features_to_remove());
 
@@ -53,7 +44,7 @@ fn migrate_manifest(manifest: &mut Document) -> Result<()> {
     .entry("build-dependencies")
     .or_insert(Item::Table(Table::new()))
     .as_table_mut()
-    .expect("manifest build-dependencies isn't a table");
+    .context("manifest build-dependencies isn't a table")?;
 
   migrate_dependency(build_dependencies, "tauri-build", &version, &[]);
 

+ 36 - 0
tooling/cli/src/migrate/migrations/v1/mod.rs

@@ -0,0 +1,36 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{
+  helpers::app_paths::{app_dir, tauri_dir},
+  Result,
+};
+
+use anyhow::Context;
+
+mod config;
+mod frontend;
+mod manifest;
+
+pub fn run() -> Result<()> {
+  let tauri_dir = tauri_dir();
+  let app_dir = app_dir();
+
+  let migrated = config::migrate(&tauri_dir).context("Could not migrate config")?;
+  manifest::migrate(&tauri_dir).context("Could not migrate manifest")?;
+  frontend::migrate(app_dir, &tauri_dir)?;
+
+  // Add plugins
+  for plugin in migrated.plugins {
+    crate::add::command(crate::add::Options {
+      plugin: plugin.clone(),
+      branch: None,
+      tag: None,
+      rev: None,
+    })
+    .with_context(|| format!("Could not migrate plugin '{plugin}'"))?;
+  }
+
+  Ok(())
+}

+ 196 - 0
tooling/cli/src/migrate/migrations/v2_rc.rs

@@ -0,0 +1,196 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{
+  helpers::{
+    app_paths::{app_dir, tauri_dir},
+    npm::PackageManager,
+  },
+  interface::rust::manifest::{read_manifest, serialize_manifest},
+  Result,
+};
+
+use std::{fs::read_to_string, path::Path};
+
+use anyhow::Context;
+use toml_edit::{Document, Item, Table, TableLike, Value};
+
+pub fn run() -> Result<()> {
+  let app_dir = app_dir();
+  let tauri_dir = tauri_dir();
+
+  let manifest_path = tauri_dir.join("Cargo.toml");
+  let (mut manifest, _) = read_manifest(&manifest_path)?;
+  migrate_manifest(&mut manifest)?;
+
+  migrate_permissions(&tauri_dir)?;
+
+  migrate_npm_dependencies(app_dir)?;
+
+  std::fs::write(&manifest_path, serialize_manifest(&manifest))
+    .context("failed to rewrite Cargo manifest")?;
+
+  Ok(())
+}
+
+fn migrate_npm_dependencies(app_dir: &Path) -> Result<()> {
+  let pm = PackageManager::from_project(app_dir)
+    .into_iter()
+    .next()
+    .unwrap_or(PackageManager::Npm);
+
+  let mut install_deps = Vec::new();
+  for pkg in [
+    "@tauri-apps/cli",
+    "@tauri-apps/api",
+    "@tauri-apps/plugin-authenticator",
+    "@tauri-apps/plugin-autostart",
+    "@tauri-apps/plugin-barcode-scanner",
+    "@tauri-apps/plugin-biometric",
+    "@tauri-apps/plugin-cli",
+    "@tauri-apps/plugin-clipboard-manager",
+    "@tauri-apps/plugin-deep-link",
+    "@tauri-apps/plugin-dialog",
+    "@tauri-apps/plugin-fs",
+    "@tauri-apps/plugin-global-shortcut",
+    "@tauri-apps/plugin-http",
+    "@tauri-apps/plugin-log",
+    "@tauri-apps/plugin-nfc",
+    "@tauri-apps/plugin-notification",
+    "@tauri-apps/plugin-os",
+    "@tauri-apps/plugin-positioner",
+    "@tauri-apps/plugin-process",
+    "@tauri-apps/plugin-shell",
+    "@tauri-apps/plugin-sql",
+    "@tauri-apps/plugin-store",
+    "@tauri-apps/plugin-stronghold",
+    "@tauri-apps/plugin-updater",
+    "@tauri-apps/plugin-upload",
+    "@tauri-apps/plugin-websocket",
+    "@tauri-apps/plugin-window-state",
+  ] {
+    let version = pm
+      .current_package_version(pkg, app_dir)
+      .unwrap_or_default()
+      .unwrap_or_default();
+    if version.starts_with("1") {
+      install_deps.push(format!("{pkg}@^2.0.0-rc.0"));
+    }
+  }
+
+  if !install_deps.is_empty() {
+    pm.install(&install_deps, app_dir)?;
+  }
+
+  Ok(())
+}
+
+fn migrate_permissions(tauri_dir: &Path) -> Result<()> {
+  let core_plugins = [
+    "app",
+    "event",
+    "image",
+    "menu",
+    "path",
+    "resources",
+    "tray",
+    "webview",
+    "window",
+  ];
+
+  for entry in walkdir::WalkDir::new(tauri_dir.join("capabilities")) {
+    let entry = entry?;
+    let path = entry.path();
+    if path.extension().map_or(false, |ext| ext == "json") {
+      let mut capability = read_to_string(path).context("failed to read capability")?;
+      for plugin in core_plugins {
+        capability = capability.replace(&format!("\"{plugin}:"), &format!("\"core:{plugin}:"));
+      }
+      std::fs::write(path, capability).context("failed to rewrite capability")?;
+    }
+  }
+  Ok(())
+}
+
+fn migrate_manifest(manifest: &mut Document) -> Result<()> {
+  let version = "2.0.0-rc.0";
+
+  let dependencies = manifest
+    .as_table_mut()
+    .entry("dependencies")
+    .or_insert(Item::Table(Table::new()))
+    .as_table_mut()
+    .context("manifest dependencies isn't a table")?;
+
+  for dep in [
+    "tauri",
+    "tauri-plugin-authenticator",
+    "tauri-plugin-autostart",
+    "tauri-plugin-barcode-scanner",
+    "tauri-plugin-biometric",
+    "tauri-plugin-cli",
+    "tauri-plugin-clipboard-manager",
+    "tauri-plugin-deep-link",
+    "tauri-plugin-dialog",
+    "tauri-plugin-fs",
+    "tauri-plugin-global-shortcut",
+    "tauri-plugin-http",
+    "tauri-plugin-localhost",
+    "tauri-plugin-log",
+    "tauri-plugin-nfc",
+    "tauri-plugin-notification",
+    "tauri-plugin-os",
+    "tauri-plugin-persisted-scope",
+    "tauri-plugin-positioner",
+    "tauri-plugin-process",
+    "tauri-plugin-shell",
+    "tauri-plugin-single-instance",
+    "tauri-plugin-sql",
+    "tauri-plugin-store",
+    "tauri-plugin-stronghold",
+    "tauri-plugin-updater",
+    "tauri-plugin-upload",
+    "tauri-plugin-websocket",
+    "tauri-plugin-window-state",
+  ] {
+    migrate_dependency(dependencies, dep, version);
+  }
+
+  let build_dependencies = manifest
+    .as_table_mut()
+    .entry("build-dependencies")
+    .or_insert(Item::Table(Table::new()))
+    .as_table_mut()
+    .context("manifest build-dependencies isn't a table")?;
+
+  migrate_dependency(build_dependencies, "tauri-build", version);
+
+  Ok(())
+}
+
+fn migrate_dependency(dependencies: &mut Table, name: &str, version: &str) {
+  let item = dependencies.entry(name).or_insert(Item::None);
+
+  // do not rewrite if dependency uses workspace inheritance
+  if item
+    .get("workspace")
+    .and_then(|v| v.as_bool())
+    .unwrap_or_default()
+  {
+    log::info!("`{name}` dependency has workspace inheritance enabled. The features array won't be automatically rewritten.");
+    return;
+  }
+
+  if let Some(dep) = item.as_table_mut() {
+    migrate_dependency_table(dep, version);
+  } else if let Some(Value::InlineTable(table)) = item.as_value_mut() {
+    migrate_dependency_table(table, version);
+  } else if item.as_str().is_some() {
+    *item = Item::Value(version.into());
+  }
+}
+
+fn migrate_dependency_table<D: TableLike>(dep: &mut D, version: &str) {
+  *dep.entry("version").or_insert(Item::None) = Item::Value(version.into());
+}

+ 37 - 19
tooling/cli/src/migrate/mod.rs

@@ -3,32 +3,50 @@
 // SPDX-License-Identifier: MIT
 
 use crate::{
-  helpers::app_paths::{app_dir, tauri_dir},
+  helpers::{
+    app_paths::tauri_dir,
+    cargo_manifest::{crate_version, CargoLock, CargoManifest},
+  },
+  interface::rust::get_workspace_dir,
   Result,
 };
+
+use std::{fs::read_to_string, str::FromStr};
+
 use anyhow::Context;
 
-mod config;
-mod frontend;
-mod manifest;
+mod migrations;
 
 pub fn command() -> Result<()> {
   let tauri_dir = tauri_dir();
-  let app_dir = app_dir();
-
-  let migrated = config::migrate(&tauri_dir).context("Could not migrate config")?;
-  manifest::migrate(&tauri_dir).context("Could not migrate manifest")?;
-  frontend::migrate(app_dir, &tauri_dir)?;
-
-  // Add plugins
-  for plugin in migrated.plugins {
-    crate::add::command(crate::add::Options {
-      plugin: plugin.clone(),
-      branch: None,
-      tag: None,
-      rev: None,
-    })
-    .with_context(|| format!("Could not migrate plugin '{plugin}'"))?
+
+  let manifest_contents =
+    read_to_string(tauri_dir.join("Cargo.toml")).context("failed to read Cargo manifest")?;
+  let manifest = toml::from_str::<CargoManifest>(&manifest_contents)
+    .context("failed to parse Cargo manifest")?;
+
+  let workspace_dir = get_workspace_dir()?;
+  let lock_path = workspace_dir.join("Cargo.lock");
+  let lock = if lock_path.exists() {
+    let lockfile_contents = read_to_string(lock_path).context("failed to read Cargo lockfile")?;
+    let lock =
+      toml::from_str::<CargoLock>(&lockfile_contents).context("failed to parse Cargo lockfile")?;
+    Some(lock)
+  } else {
+    None
+  };
+
+  let tauri_version = crate_version(&tauri_dir, Some(&manifest), lock.as_ref(), "tauri").version;
+  let tauri_version = semver::Version::from_str(&tauri_version)?;
+
+  if tauri_version.major == 1 {
+    migrations::v1::run().context("failed to migrate from v1")?;
+  } else if tauri_version.major == 2 {
+    if let Some((pre, _number)) = tauri_version.pre.as_str().split_once('.') {
+      if pre == "beta" {
+        migrations::v2_rc::run().context("failed to migrate from v2 beta to rc")?;
+      }
+    }
   }
 
   Ok(())