Skip to content

Commit

Permalink
feat(cli): added support for automatically updating tools from lockfile
Browse files Browse the repository at this point in the history
This should prevent future inconveniences with updated to tools like
`wasm-bindgen`.
Addresses reccomendations from #169.
  • Loading branch information
arctic-hen7 committed Aug 19, 2022
1 parent 5d75f9d commit 52ab3f1
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 38 deletions.
2 changes: 2 additions & 0 deletions packages/perseus-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions packages/perseus-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
153 changes: 117 additions & 36 deletions packages/perseus-cli/src/install.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<Self, InstallError> {
// 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
Expand All @@ -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)) =
Expand Down Expand Up @@ -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<ToolStatus, InstallError> {
pub fn get_status(
&self,
target: &Path,
lockfile: &Lockfile,
) -> Result<ToolStatus, InstallError> {
// 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.
Expand All @@ -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<String> = Vec::new();
Expand All @@ -307,22 +347,50 @@ impl Tool {
// Now order those from most recent to least recent
versions.sort();
let versions = versions.into_iter().rev().collect::<Vec<String>>();
// 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)
}
}
}
}
}
Expand Down Expand Up @@ -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<Option<String>, 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.
Expand Down
6 changes: 4 additions & 2 deletions packages/perseus-cli/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// 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<String>,
/// Disables the system-wide tools cache in `~/.cargo/perseus_tools/` (you
Expand Down

0 comments on commit 52ab3f1

Please sign in to comment.