Skip to content

Commit 52ab3f1

Browse files
committed
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.
1 parent 5d75f9d commit 52ab3f1

File tree

4 files changed

+140
-38
lines changed

4 files changed

+140
-38
lines changed

packages/perseus-cli/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ reqwest = { version = "0.11", features = [ "json", "stream" ] }
4141
tar = "0.4"
4242
flate2 = "1"
4343
directories = "4"
44+
cargo_metadata = "0.15"
45+
cargo-lock = "8"
4446

4547
[dev-dependencies]
4648
assert_cmd = "2"

packages/perseus-cli/src/errors.rs

+17
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,21 @@ pub enum InstallError {
309309
},
310310
#[error("directory found in `dist/tools/` with invalid name (running `perseus clean` should resolve this)")]
311311
InvalidToolsDirName { name: String },
312+
#[error("generating `Cargo.lock` returned non-zero exit code")]
313+
LockfileGenerationNonZero { code: i32 },
314+
#[error("couldn't generate `Cargo.lock`")]
315+
LockfileGenerationFailed {
316+
#[source]
317+
source: ExecutionError,
318+
},
319+
#[error("couldn't fetch metadata for current crate (have you run `perseus init` yet?)")]
320+
MetadataFailed {
321+
#[source]
322+
source: cargo_metadata::Error,
323+
},
324+
#[error("couldn't load `Cargo.lock` from workspace root")]
325+
LockfileLoadFailed {
326+
#[source]
327+
source: cargo_lock::Error,
328+
},
312329
}

packages/perseus-cli/src/install.rs

+117-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use crate::cmd::{cfg_spinner, fail_spinner, succeed_spinner};
1+
use crate::cmd::{cfg_spinner, fail_spinner, run_stage, succeed_spinner};
22
use crate::errors::*;
33
use crate::parse::Opts;
4+
use cargo_lock::Lockfile;
5+
use cargo_metadata::MetadataCommand;
46
use console::Emoji;
57
use directories::ProjectDirs;
68
use flate2::read::GzDecoder;
@@ -18,6 +20,7 @@ use tar::Archive;
1820
use tokio::io::AsyncWriteExt;
1921

2022
static INSTALLING: Emoji<'_, '_> = Emoji("📥", "");
23+
static GENERATING_LOCKFILE: Emoji<'_, '_> = Emoji("🔏", "");
2124

2225
// For each of the tools installed in this file, we preferentially
2326
// manually download it. If that can't be achieved due to a platform
@@ -89,6 +92,33 @@ impl Tools {
8992
///
9093
/// If tools are installed, this will create a CLI spinner automatically.
9194
pub async fn new(dir: &Path, global_opts: &Opts) -> Result<Self, InstallError> {
95+
// First, make sure `Cargo.lock` exists, since we'll need it for determining the
96+
// right version of `wasm-bindgen`
97+
let metadata = MetadataCommand::new()
98+
.no_deps()
99+
.exec()
100+
.map_err(|err| InstallError::MetadataFailed { source: err })?;
101+
let workspace_root = metadata.workspace_root.into_std_path_buf();
102+
let lockfile_path = workspace_root.join("Cargo.lock");
103+
if !lockfile_path.exists() {
104+
let lf_msg = format!("{} Generating Cargo lockfile", GENERATING_LOCKFILE);
105+
let lf_spinner = cfg_spinner(ProgressBar::new_spinner(), &lf_msg);
106+
let (_stdout, _stderr, exit_code) = run_stage(
107+
vec!["cargo generate-lockfile"],
108+
&workspace_root,
109+
&lf_spinner,
110+
&lf_msg,
111+
Vec::new(),
112+
)
113+
.map_err(|err| InstallError::LockfileGenerationFailed { source: err })?;
114+
if exit_code != 0 {
115+
// The output has already been handled, just terminate
116+
return Err(InstallError::LockfileGenerationNonZero { code: exit_code });
117+
}
118+
}
119+
let lockfile = Lockfile::load(lockfile_path)
120+
.map_err(|err| InstallError::LockfileLoadFailed { source: err })?;
121+
92122
let target = get_tools_dir(dir, global_opts.no_system_tools_cache)?;
93123

94124
// Instantiate the tools
@@ -104,8 +134,8 @@ impl Tools {
104134
);
105135

106136
// Get the statuses of all the tools
107-
let wb_status = wasm_bindgen.get_status(&target)?;
108-
let wo_status = wasm_opt.get_status(&target)?;
137+
let wb_status = wasm_bindgen.get_status(&target, &lockfile)?;
138+
let wo_status = wasm_opt.get_status(&target, &lockfile)?;
109139
// Figure out if everything is present
110140
// This is the only case in which we don't have to start the spinner
111141
if let (ToolStatus::Available(wb_path), ToolStatus::Available(wo_path)) =
@@ -253,7 +283,11 @@ impl Tool {
253283
/// installed globally on the user's system, etc. If this returns
254284
/// `ToolStatus::NeedsInstall`, we can be sure that there are binaries
255285
/// available, and the same if it returns `ToolStatus::NeedsLatestInstall`.
256-
pub fn get_status(&self, target: &Path) -> Result<ToolStatus, InstallError> {
286+
pub fn get_status(
287+
&self,
288+
target: &Path,
289+
lockfile: &Lockfile,
290+
) -> Result<ToolStatus, InstallError> {
257291
// The status information will be incomplete from this first pass
258292
let initial_status = {
259293
// If there's a directory that matches with a given user version, we'll use it.
@@ -268,24 +302,30 @@ impl Tool {
268302
// If they've given us a version, we'll check if that directory exists (we don't
269303
// care about any others)
270304
if let Some(version) = &self.user_given_version {
271-
let expected_path = target.join(format!("{}-{}", self.name, version));
272-
Ok(if fs::metadata(&expected_path).is_ok() {
273-
ToolStatus::Available(
274-
expected_path
275-
.join(&self.final_path)
276-
.to_string_lossy()
277-
.to_string(),
278-
)
305+
// If the user wants the latets version, just force an update
306+
if version == "latest" {
307+
Ok(ToolStatus::NeedsLatestInstall)
279308
} else {
280-
ToolStatus::NeedsInstall {
281-
version: version.to_string(),
282-
// This will be filled in on the second pass-through
283-
artifact_name: String::new(),
284-
}
285-
})
309+
let expected_path = target.join(format!("{}-{}", self.name, version));
310+
Ok(if fs::metadata(&expected_path).is_ok() {
311+
ToolStatus::Available(
312+
expected_path
313+
.join(&self.final_path)
314+
.to_string_lossy()
315+
.to_string(),
316+
)
317+
} else {
318+
ToolStatus::NeedsInstall {
319+
version: version.to_string(),
320+
// This will be filled in on the second pass-through
321+
artifact_name: String::new(),
322+
}
323+
})
324+
}
286325
} else {
287-
// We have no further information from the user, so we'll use the latest version
288-
// that's installed, or we'll install the latest version.
326+
// We have no further information from the user, so we'll use whatever matches
327+
// the user's `Cargo.lock`, or, if they haven't specified anything, we'll try
328+
// the latest version.
289329
// Either way, we need to know what we've got installed already by walking the
290330
// directory.
291331
let mut versions: Vec<String> = Vec::new();
@@ -307,22 +347,50 @@ impl Tool {
307347
// Now order those from most recent to least recent
308348
versions.sort();
309349
let versions = versions.into_iter().rev().collect::<Vec<String>>();
310-
// If there are any at all, pick the first one
311-
if !versions.is_empty() {
312-
let latest_available_version = &versions[0];
313-
// We know the directory for this version had a valid name, so we can
314-
// determine exactly where it was
315-
let path_to_latest_version = target.join(format!(
316-
"{}-{}/{}",
317-
self.name, latest_available_version, self.final_path
318-
));
319-
Ok(ToolStatus::Available(
320-
path_to_latest_version.to_string_lossy().to_string(),
321-
))
322-
} else {
323-
// We don't check the latest version here because we haven't started the
324-
// spinner yet
325-
Ok(ToolStatus::NeedsLatestInstall)
350+
351+
// Now figure out what would match the current setup by checking `Cargo.lock`
352+
// (it's entirely possible that there are multiple versions
353+
// of `wasm-bindgen` in here, but that would be the user's problem).
354+
// It doesn't matter that we do this erroneously for other tools, since they'll
355+
// just return `None`.
356+
match self.get_pkg_version_from_lockfile(lockfile)? {
357+
Some(version) => {
358+
if versions.contains(&version) {
359+
let path_to_version = target
360+
.join(format!("{}-{}/{}", self.name, version, self.final_path));
361+
Ok(ToolStatus::Available(
362+
path_to_version.to_string_lossy().to_string(),
363+
))
364+
} else {
365+
Ok(ToolStatus::NeedsInstall {
366+
version,
367+
// This will be filled in on the second pass-through
368+
artifact_name: String::new(),
369+
})
370+
}
371+
}
372+
// There's nothing in the lockfile, so we'll go with the latest we have
373+
// installed
374+
None => {
375+
// If there are any at all, pick the first one
376+
if !versions.is_empty() {
377+
let latest_available_version = &versions[0];
378+
// We know the directory for this version had a valid name, so we
379+
// can determine exactly where it
380+
// was
381+
let path_to_latest_version = target.join(format!(
382+
"{}-{}/{}",
383+
self.name, latest_available_version, self.final_path
384+
));
385+
Ok(ToolStatus::Available(
386+
path_to_latest_version.to_string_lossy().to_string(),
387+
))
388+
} else {
389+
// We don't check the latest version here because we haven't started
390+
// the spinner yet
391+
Ok(ToolStatus::NeedsLatestInstall)
392+
}
393+
}
326394
}
327395
}
328396
}
@@ -548,6 +616,19 @@ impl Tool {
548616
.unwrap()
549617
.to_string())
550618
}
619+
/// Gets the version of a specific package in `Cargo.lock`, assuming it has
620+
/// already been generated.
621+
fn get_pkg_version_from_lockfile(
622+
&self,
623+
lockfile: &Lockfile,
624+
) -> Result<Option<String>, InstallError> {
625+
let version = lockfile
626+
.packages
627+
.iter()
628+
.find(|p| p.name.as_str() == self.name)
629+
.map(|p| p.version.to_string());
630+
Ok(version)
631+
}
551632
}
552633

553634
/// A tool's status on-system.

packages/perseus-cli/src/parse.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ pub struct Opts {
6868
#[clap(long, global = true)]
6969
pub no_browser_reload: bool,
7070
/// A custom version of `wasm-bindgen` to use (defaults to the latest
71-
/// installed version, and after that the latest available from GitHub)
71+
/// installed version, and after that the latest available from GitHub;
72+
/// update to latest can be forced with `latest`)
7273
#[clap(long, global = true)]
7374
pub wasm_bindgen_version: Option<String>,
7475
/// A custom version of `wasm-opt` to use (defaults to the latest installed
75-
/// version, and after that the latest available from GitHub)
76+
/// version, and after that the latest available from GitHub; update to
77+
/// latest can be forced with `latest`)
7678
#[clap(long, global = true)]
7779
pub wasm_opt_version: Option<String>,
7880
/// Disables the system-wide tools cache in `~/.cargo/perseus_tools/` (you

0 commit comments

Comments
 (0)