From 52ab3f17dec243fa2759489b7e5116c9c4ac4de9 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sat, 20 Aug 2022 09:57:36 +1000 Subject: [PATCH] feat(cli): added support for automatically updating tools from lockfile This should prevent future inconveniences with updated to tools like `wasm-bindgen`. Addresses reccomendations from #169. --- packages/perseus-cli/Cargo.toml | 2 + packages/perseus-cli/src/errors.rs | 17 ++++ packages/perseus-cli/src/install.rs | 153 +++++++++++++++++++++------- packages/perseus-cli/src/parse.rs | 6 +- 4 files changed, 140 insertions(+), 38 deletions(-) diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index 46882b9971..e91192b7cd 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -41,6 +41,8 @@ reqwest = { version = "0.11", features = [ "json", "stream" ] } tar = "0.4" flate2 = "1" directories = "4" +cargo_metadata = "0.15" +cargo-lock = "8" [dev-dependencies] assert_cmd = "2" diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index 25eefb5d77..bf7a71b38d 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -309,4 +309,21 @@ pub enum InstallError { }, #[error("directory found in `dist/tools/` with invalid name (running `perseus clean` should resolve this)")] InvalidToolsDirName { name: String }, + #[error("generating `Cargo.lock` returned non-zero exit code")] + LockfileGenerationNonZero { code: i32 }, + #[error("couldn't generate `Cargo.lock`")] + LockfileGenerationFailed { + #[source] + source: ExecutionError, + }, + #[error("couldn't fetch metadata for current crate (have you run `perseus init` yet?)")] + MetadataFailed { + #[source] + source: cargo_metadata::Error, + }, + #[error("couldn't load `Cargo.lock` from workspace root")] + LockfileLoadFailed { + #[source] + source: cargo_lock::Error, + }, } diff --git a/packages/perseus-cli/src/install.rs b/packages/perseus-cli/src/install.rs index 724579c4d7..5b33b72dbf 100644 --- a/packages/perseus-cli/src/install.rs +++ b/packages/perseus-cli/src/install.rs @@ -1,6 +1,8 @@ -use crate::cmd::{cfg_spinner, fail_spinner, succeed_spinner}; +use crate::cmd::{cfg_spinner, fail_spinner, run_stage, succeed_spinner}; use crate::errors::*; use crate::parse::Opts; +use cargo_lock::Lockfile; +use cargo_metadata::MetadataCommand; use console::Emoji; use directories::ProjectDirs; use flate2::read::GzDecoder; @@ -18,6 +20,7 @@ use tar::Archive; use tokio::io::AsyncWriteExt; static INSTALLING: Emoji<'_, '_> = Emoji("📥", ""); +static GENERATING_LOCKFILE: Emoji<'_, '_> = Emoji("🔏", ""); // For each of the tools installed in this file, we preferentially // manually download it. If that can't be achieved due to a platform @@ -89,6 +92,33 @@ impl Tools { /// /// If tools are installed, this will create a CLI spinner automatically. pub async fn new(dir: &Path, global_opts: &Opts) -> Result { + // First, make sure `Cargo.lock` exists, since we'll need it for determining the + // right version of `wasm-bindgen` + let metadata = MetadataCommand::new() + .no_deps() + .exec() + .map_err(|err| InstallError::MetadataFailed { source: err })?; + let workspace_root = metadata.workspace_root.into_std_path_buf(); + let lockfile_path = workspace_root.join("Cargo.lock"); + if !lockfile_path.exists() { + let lf_msg = format!("{} Generating Cargo lockfile", GENERATING_LOCKFILE); + let lf_spinner = cfg_spinner(ProgressBar::new_spinner(), &lf_msg); + let (_stdout, _stderr, exit_code) = run_stage( + vec!["cargo generate-lockfile"], + &workspace_root, + &lf_spinner, + &lf_msg, + Vec::new(), + ) + .map_err(|err| InstallError::LockfileGenerationFailed { source: err })?; + if exit_code != 0 { + // The output has already been handled, just terminate + return Err(InstallError::LockfileGenerationNonZero { code: exit_code }); + } + } + let lockfile = Lockfile::load(lockfile_path) + .map_err(|err| InstallError::LockfileLoadFailed { source: err })?; + let target = get_tools_dir(dir, global_opts.no_system_tools_cache)?; // Instantiate the tools @@ -104,8 +134,8 @@ impl Tools { ); // Get the statuses of all the tools - let wb_status = wasm_bindgen.get_status(&target)?; - let wo_status = wasm_opt.get_status(&target)?; + let wb_status = wasm_bindgen.get_status(&target, &lockfile)?; + let wo_status = wasm_opt.get_status(&target, &lockfile)?; // Figure out if everything is present // This is the only case in which we don't have to start the spinner if let (ToolStatus::Available(wb_path), ToolStatus::Available(wo_path)) = @@ -253,7 +283,11 @@ impl Tool { /// installed globally on the user's system, etc. If this returns /// `ToolStatus::NeedsInstall`, we can be sure that there are binaries /// available, and the same if it returns `ToolStatus::NeedsLatestInstall`. - pub fn get_status(&self, target: &Path) -> Result { + pub fn get_status( + &self, + target: &Path, + lockfile: &Lockfile, + ) -> Result { // The status information will be incomplete from this first pass let initial_status = { // If there's a directory that matches with a given user version, we'll use it. @@ -268,24 +302,30 @@ impl Tool { // If they've given us a version, we'll check if that directory exists (we don't // care about any others) if let Some(version) = &self.user_given_version { - let expected_path = target.join(format!("{}-{}", self.name, version)); - Ok(if fs::metadata(&expected_path).is_ok() { - ToolStatus::Available( - expected_path - .join(&self.final_path) - .to_string_lossy() - .to_string(), - ) + // If the user wants the latets version, just force an update + if version == "latest" { + Ok(ToolStatus::NeedsLatestInstall) } else { - ToolStatus::NeedsInstall { - version: version.to_string(), - // This will be filled in on the second pass-through - artifact_name: String::new(), - } - }) + let expected_path = target.join(format!("{}-{}", self.name, version)); + Ok(if fs::metadata(&expected_path).is_ok() { + ToolStatus::Available( + expected_path + .join(&self.final_path) + .to_string_lossy() + .to_string(), + ) + } else { + ToolStatus::NeedsInstall { + version: version.to_string(), + // This will be filled in on the second pass-through + artifact_name: String::new(), + } + }) + } } else { - // We have no further information from the user, so we'll use the latest version - // that's installed, or we'll install the latest version. + // We have no further information from the user, so we'll use whatever matches + // the user's `Cargo.lock`, or, if they haven't specified anything, we'll try + // the latest version. // Either way, we need to know what we've got installed already by walking the // directory. let mut versions: Vec = Vec::new(); @@ -307,22 +347,50 @@ impl Tool { // Now order those from most recent to least recent versions.sort(); let versions = versions.into_iter().rev().collect::>(); - // If there are any at all, pick the first one - if !versions.is_empty() { - let latest_available_version = &versions[0]; - // We know the directory for this version had a valid name, so we can - // determine exactly where it was - let path_to_latest_version = target.join(format!( - "{}-{}/{}", - self.name, latest_available_version, self.final_path - )); - Ok(ToolStatus::Available( - path_to_latest_version.to_string_lossy().to_string(), - )) - } else { - // We don't check the latest version here because we haven't started the - // spinner yet - Ok(ToolStatus::NeedsLatestInstall) + + // Now figure out what would match the current setup by checking `Cargo.lock` + // (it's entirely possible that there are multiple versions + // of `wasm-bindgen` in here, but that would be the user's problem). + // It doesn't matter that we do this erroneously for other tools, since they'll + // just return `None`. + match self.get_pkg_version_from_lockfile(lockfile)? { + Some(version) => { + if versions.contains(&version) { + let path_to_version = target + .join(format!("{}-{}/{}", self.name, version, self.final_path)); + Ok(ToolStatus::Available( + path_to_version.to_string_lossy().to_string(), + )) + } else { + Ok(ToolStatus::NeedsInstall { + version, + // This will be filled in on the second pass-through + artifact_name: String::new(), + }) + } + } + // There's nothing in the lockfile, so we'll go with the latest we have + // installed + None => { + // If there are any at all, pick the first one + if !versions.is_empty() { + let latest_available_version = &versions[0]; + // We know the directory for this version had a valid name, so we + // can determine exactly where it + // was + let path_to_latest_version = target.join(format!( + "{}-{}/{}", + self.name, latest_available_version, self.final_path + )); + Ok(ToolStatus::Available( + path_to_latest_version.to_string_lossy().to_string(), + )) + } else { + // We don't check the latest version here because we haven't started + // the spinner yet + Ok(ToolStatus::NeedsLatestInstall) + } + } } } } @@ -548,6 +616,19 @@ impl Tool { .unwrap() .to_string()) } + /// Gets the version of a specific package in `Cargo.lock`, assuming it has + /// already been generated. + fn get_pkg_version_from_lockfile( + &self, + lockfile: &Lockfile, + ) -> Result, InstallError> { + let version = lockfile + .packages + .iter() + .find(|p| p.name.as_str() == self.name) + .map(|p| p.version.to_string()); + Ok(version) + } } /// A tool's status on-system. diff --git a/packages/perseus-cli/src/parse.rs b/packages/perseus-cli/src/parse.rs index 6ed04037d0..3c78585df6 100644 --- a/packages/perseus-cli/src/parse.rs +++ b/packages/perseus-cli/src/parse.rs @@ -68,11 +68,13 @@ pub struct Opts { #[clap(long, global = true)] pub no_browser_reload: bool, /// A custom version of `wasm-bindgen` to use (defaults to the latest - /// installed version, and after that the latest available from GitHub) + /// installed version, and after that the latest available from GitHub; + /// update to latest can be forced with `latest`) #[clap(long, global = true)] pub wasm_bindgen_version: Option, /// A custom version of `wasm-opt` to use (defaults to the latest installed - /// version, and after that the latest available from GitHub) + /// version, and after that the latest available from GitHub; update to + /// latest can be forced with `latest`) #[clap(long, global = true)] pub wasm_opt_version: Option, /// Disables the system-wide tools cache in `~/.cargo/perseus_tools/` (you