perseus_cli/install.rs
1use crate::cmd::{cfg_spinner, fail_spinner, run_stage, succeed_spinner};
2use crate::errors::*;
3use crate::parse::Opts;
4use cargo_lock::Lockfile;
5use cargo_metadata::MetadataCommand;
6use console::Emoji;
7use directories::ProjectDirs;
8use flate2::read::GzDecoder;
9use futures::future::try_join;
10use indicatif::ProgressBar;
11use reqwest::Client;
12use std::borrow::BorrowMut;
13use std::fs;
14use std::fs::File;
15use std::{
16 path::{Path, PathBuf},
17 process::Command,
18};
19use tar::Archive;
20use tokio::io::AsyncWriteExt;
21
22static INSTALLING: Emoji<'_, '_> = Emoji("📥", "");
23static GENERATING_LOCKFILE: Emoji<'_, '_> = Emoji("🔏", "");
24
25// For each of the tools installed in this file, we preferentially
26// manually download it. If that can't be achieved due to a platform
27// mismatch, then we'll see if the user already has a version installed.
28//
29// Importantly, if the user has specified an environment variable specifying
30// where a tool can be found, we'll use that no matter what.
31
32/// Gets the directory to store tools in. This will preferentially use the
33/// system-wide cache, falling back to a local version.
34///
35/// If the user specifies that we're running on CI, we'll use the local version
36/// regardless.
37pub fn get_tools_dir(project: &Path, no_system_cache: bool) -> Result<PathBuf, InstallError> {
38 match ProjectDirs::from("", "perseus", "perseus_cli") {
39 Some(dirs) if !no_system_cache => {
40 let target = dirs.cache_dir().join("tools");
41 if target.exists() {
42 Ok(target)
43 } else {
44 // Try to create the system-wide cache
45 if fs::create_dir_all(&target).is_ok() {
46 Ok(target)
47 } else {
48 // Failed, so we'll resort to the local cache
49 let target = project.join("dist/tools");
50 if !target.exists() {
51 // If this fails, we have no recourse, so we'll have to fail
52 fs::create_dir_all(&target)
53 .map_err(|err| InstallError::CreateToolsDirFailed { source: err })?;
54 }
55 // It either already existed or we've just created it
56 Ok(target)
57 }
58 }
59 }
60 _ => {
61 let target = project.join("dist/tools");
62 if !target.exists() {
63 // If this fails, we have no recourse, so we'll have to fail
64 fs::create_dir_all(&target)
65 .map_err(|err| InstallError::CreateToolsDirFailed { source: err })?;
66 }
67 // It either already existed or we've just created it
68 Ok(target)
69 }
70 }
71}
72
73/// A representation of the paths to all the external tools we need.
74/// This includes `cargo`, simply for convenience, even though it's not
75/// actually independently installed.
76///
77/// This does not contain metadata for the installation process, but is rather
78/// intended to be passed around through the rest of the CLI.
79#[derive(Clone)]
80pub struct Tools {
81 /// The path to `cargo` on the engine-side.
82 pub cargo_engine: String,
83 /// The path to `cargo` on the browser-side.
84 pub cargo_browser: String,
85 /// The path to `wasm-bindgen`.
86 pub wasm_bindgen: String,
87 /// The path to `wasm-opt`.
88 pub wasm_opt: String,
89}
90impl Tools {
91 /// Gets a new instance of `Tools` by installing the tools if necessary.
92 ///
93 /// If tools are installed, this will create a CLI spinner automatically.
94 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 .cargo_path(&global_opts.cargo_engine_path)
100 .exec()
101 .map_err(|err| InstallError::MetadataFailed { source: err })?;
102 let workspace_root = metadata.workspace_root.into_std_path_buf();
103 let lockfile_path = workspace_root.join("Cargo.lock");
104 if !lockfile_path.exists() {
105 let lf_msg = format!("{} Generating Cargo lockfile", GENERATING_LOCKFILE);
106 let lf_spinner = cfg_spinner(ProgressBar::new_spinner(), &lf_msg);
107 let (_stdout, _stderr, exit_code) = run_stage(
108 vec![&format!(
109 "{} generate-lockfile",
110 global_opts.cargo_engine_path
111 )],
112 &workspace_root,
113 &lf_spinner,
114 &lf_msg,
115 vec![
116 ("RUSTFLAGS", "--cfg=engine"),
117 ("CARGO_TERM_COLOR", "always"),
118 ],
119 // Sure, the user *might* want logs on this process (but this will only be run the
120 // first time)
121 global_opts.verbose,
122 )
123 .map_err(|err| InstallError::LockfileGenerationFailed { source: err })?;
124 if exit_code != 0 {
125 // The output has already been handled, just terminate
126 return Err(InstallError::LockfileGenerationNonZero { code: exit_code });
127 }
128 }
129 let lockfile = Lockfile::load(lockfile_path)
130 .map_err(|err| InstallError::LockfileLoadFailed { source: err })?;
131
132 let target = get_tools_dir(dir, global_opts.no_system_tools_cache)?;
133
134 // Instantiate the tools
135 let wasm_bindgen = Tool::new(
136 ToolType::WasmBindgen,
137 &global_opts.wasm_bindgen_path,
138 &global_opts.wasm_bindgen_version,
139 );
140 let wasm_opt = Tool::new(
141 ToolType::WasmOpt,
142 &global_opts.wasm_opt_path,
143 &global_opts.wasm_opt_version,
144 );
145
146 // Get the statuses of all the tools
147 let wb_status = wasm_bindgen.get_status(&target, &lockfile)?;
148 let wo_status = wasm_opt.get_status(&target, &lockfile)?;
149 // Figure out if everything is present
150 // This is the only case in which we don't have to start the spinner
151 if let (ToolStatus::Available(wb_path), ToolStatus::Available(wo_path)) =
152 (&wb_status, &wo_status)
153 {
154 #[cfg(unix)]
155 let (wb_path, wo_path) = (wb_path.to_string(), wo_path.to_string());
156 #[cfg(windows)]
157 let (wb_path, wo_path) = (format!("& \'{}\'", wb_path), format!("& \'{}\'", wo_path));
158 Ok(Tools {
159 cargo_engine: global_opts.cargo_engine_path.clone(),
160 cargo_browser: global_opts.cargo_browser_path.clone(),
161 wasm_bindgen: wb_path,
162 wasm_opt: wo_path,
163 })
164 } else {
165 // We need to install some things, which may take some time
166 let spinner_msg = format!("{} Installing external tools", INSTALLING);
167 let spinner = cfg_spinner(ProgressBar::new_spinner(), &spinner_msg);
168
169 // Install all the tools in parallel
170 // These functions sanity-check their statuses, so we don't need to worry about
171 // installing unnecessarily
172 let res = try_join(
173 wasm_bindgen.install(wb_status, &target),
174 wasm_opt.install(wo_status, &target),
175 )
176 .await;
177 if let Err(err) = res {
178 fail_spinner(&spinner, &spinner_msg);
179 return Err(err);
180 }
181 // If we're here, we have the paths
182 succeed_spinner(&spinner, &spinner_msg);
183 let paths = res.unwrap();
184 #[cfg(unix)]
185 let (wb_path, wo_path) = paths;
186 #[cfg(windows)]
187 let (wb_path, wo_path) = (format!("& \'{}\'", paths.0), format!("& \'{}\'", paths.1));
188
189 Ok(Tools {
190 cargo_engine: global_opts.cargo_engine_path.clone(),
191 cargo_browser: global_opts.cargo_browser_path.clone(),
192 wasm_bindgen: wb_path,
193 wasm_opt: wo_path,
194 })
195 }
196 }
197}
198
199/// The data we need about an external tool to be able to install it.
200///
201/// This does not contain data about arguments to these tools, that's passed
202/// through the global options to the CLI directly (with defaults hardcoded).
203pub struct Tool {
204 /// The name of the tool. This will also be used for the name of the
205 /// directory in `dist/tools/` in which the tool is stored (with the
206 /// version number added on).
207 pub name: String,
208 /// A path provided by the user to the tool. If this is present, then the
209 /// tool won't be installed, we'll use what the user has given us
210 /// instead.
211 pub user_given_path: Option<String>,
212 /// A specific version number provided by the user. By default, the latest
213 /// version is used.
214 pub user_given_version: Option<String>,
215 /// The path to the binary within the directory that is extracted from the
216 /// downloaded archive.
217 pub final_path: String,
218 /// The name of the GitHub repo from which the tool can be downloaded.
219 pub gh_repo: String,
220 /// The name of the directory that will be extracted. This should contain
221 /// `%version` to be replaced with the actual version and/or
222 /// `%artifact_name` to be replaced with that.
223 pub extracted_dir_name: String,
224 /// The actual type of the tool. (All tools have the same data.)
225 pub tool_type: ToolType,
226}
227impl Tool {
228 /// Creates a new instance of this `struct`.
229 pub fn new(
230 tool_type: ToolType,
231 user_given_path: &Option<String>,
232 user_given_version: &Option<String>,
233 ) -> Self {
234 // Correct the final path for Windows (on which it'll have a `.exe` extension)
235 #[cfg(unix)]
236 let final_path = tool_type.final_path();
237 #[cfg(windows)]
238 let final_path = tool_type.final_path() + ".exe";
239
240 Self {
241 name: tool_type.name(),
242 user_given_path: user_given_path.to_owned(),
243 user_given_version: user_given_version.to_owned(),
244 final_path,
245 gh_repo: tool_type.gh_repo(),
246 extracted_dir_name: tool_type.extracted_dir_name(),
247 tool_type,
248 }
249 }
250 /// Gets the name of the artifact to download based on the tool data and the
251 /// version to download. Note that the version provided here entirely
252 /// overrides anything the user might have provided.
253 ///
254 /// If no precompiled binary is expected to be available for the current
255 /// platform, this will return `None`.
256 fn get_artifact_name(&self, version: &str) -> Option<String> {
257 match &self.tool_type {
258 // --- `wasm-bindgen` ---
259 // Linux
260 ToolType::WasmBindgen if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") => {
261 Some("wasm-bindgen-%version-x86_64-unknown-linux-musl")
262 }
263 // MacOS (Intel)
264 ToolType::WasmBindgen if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") => {
265 Some("wasm-bindgen-%version-x86_64-apple-darwin")
266 }
267 // MacOS (Apple Silicon)
268 ToolType::WasmBindgen if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") => {
269 Some("wasm-bindgen-%version-aarch64-apple-darwin")
270 }
271 // Windows
272 ToolType::WasmBindgen
273 if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") =>
274 {
275 Some("wasm-bindgen-%version-x86_64-pc-windows-msvc")
276 }
277 ToolType::WasmBindgen => None,
278 // --- `wasm-opt` ---
279 // Linux
280 ToolType::WasmOpt if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") => {
281 Some("binaryen-%version-x86_64-linux")
282 }
283 // MacOS (Intel)
284 ToolType::WasmOpt if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") => {
285 Some("binaryen-%version-x86_64-macos")
286 }
287 // MacOS (Apple Silicon)
288 ToolType::WasmOpt if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") => {
289 Some("binaryen-%version-arm64-macos")
290 }
291 // Windows
292 ToolType::WasmOpt if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") => {
293 Some("binaryen-%version-x86_64-windows")
294 }
295 ToolType::WasmOpt => None,
296 }
297 .map(|s| s.replace("%version", version))
298 }
299 /// Gets the path to the already-installed version of the tool to use. This
300 /// should take the full path to `dist/tools/`. This will automatically
301 /// handle whether or not to install a new version, use a version already
302 /// installed globally on the user's system, etc. If this returns
303 /// `ToolStatus::NeedsInstall`, we can be sure that there are binaries
304 /// available, and the same if it returns `ToolStatus::NeedsLatestInstall`.
305 pub fn get_status(
306 &self,
307 target: &Path,
308 lockfile: &Lockfile,
309 ) -> Result<ToolStatus, InstallError> {
310 // The status information will be incomplete from this first pass
311 let initial_status = {
312 // If there's a directory that matches with a given user version, we'll use it.
313 // If not, we'll use the latest version. Only if there are no
314 // installed versions available will this return `None`, or if the user wants a
315 // specific one that doesn't exist.
316
317 // If the user has given us a path, that overrides everything
318 if let Some(path) = &self.user_given_path {
319 Ok(ToolStatus::Available(path.to_string()))
320 } else {
321 // If they've given us a version, we'll check if that directory exists (we don't
322 // care about any others)
323 if let Some(version) = &self.user_given_version {
324 // If the user wants the latest version, just force an update
325 if version == "latest" {
326 Ok(ToolStatus::NeedsLatestInstall)
327 } else {
328 let expected_path = target.join(format!("{}-{}", self.name, version));
329 Ok(if fs::metadata(&expected_path).is_ok() {
330 ToolStatus::Available(
331 expected_path
332 .join(&self.final_path)
333 .to_string_lossy()
334 .to_string(),
335 )
336 } else {
337 ToolStatus::NeedsInstall {
338 version: version.to_string(),
339 // This will be filled in on the second pass-through
340 artifact_name: String::new(),
341 }
342 })
343 }
344 } else {
345 // We have no further information from the user, so we'll use whatever matches
346 // the user's `Cargo.lock`, or, if they haven't specified anything, we'll try
347 // the latest version.
348 // Either way, we need to know what we've got installed already by walking the
349 // directory.
350 let mut versions: Vec<String> = Vec::new();
351 for entry in fs::read_dir(target)
352 .map_err(|err| InstallError::ReadToolsDirFailed { source: err })?
353 {
354 let entry = entry
355 .map_err(|err| InstallError::ReadToolsDirFailed { source: err })?;
356 let dir_name = entry.file_name().to_string_lossy().to_string();
357 if dir_name.starts_with(&self.name) {
358 let dir_name_ref = dir_name.to_string();
359 // Valid directory names are of the form `<tool-name>-<tool-version>`
360 let version = dir_name
361 .strip_prefix(&format!("{}-", self.name))
362 .ok_or(InstallError::InvalidToolsDirName { name: dir_name_ref })?;
363 versions.push(version.to_string());
364 }
365 }
366 // Now order those from most recent to least recent
367 versions.sort();
368 let versions = versions.into_iter().rev().collect::<Vec<String>>();
369
370 // Now figure out what would match the current setup by checking `Cargo.lock`
371 // (it's entirely possible that there are multiple versions
372 // of `wasm-bindgen` in here, but that would be the user's problem).
373 // It doesn't matter that we do this erroneously for other tools, since they'll
374 // just return `None`.
375 match self.get_pkg_version_from_lockfile(lockfile)? {
376 Some(version) => {
377 if versions.contains(&version) {
378 let path_to_version = target
379 .join(format!("{}-{}/{}", self.name, version, self.final_path));
380 Ok(ToolStatus::Available(
381 path_to_version.to_string_lossy().to_string(),
382 ))
383 } else {
384 Ok(ToolStatus::NeedsInstall {
385 version,
386 // This will be filled in on the second pass-through
387 artifact_name: String::new(),
388 })
389 }
390 }
391 // There's nothing in the lockfile, so we'll go with the latest we have
392 // installed
393 None => {
394 // If there are any at all, pick the first one
395 if !versions.is_empty() {
396 let latest_available_version = &versions[0];
397 // We know the directory for this version had a valid name, so we
398 // can determine exactly where it
399 // was
400 let path_to_latest_version = target.join(format!(
401 "{}-{}/{}",
402 self.name, latest_available_version, self.final_path
403 ));
404 Ok(ToolStatus::Available(
405 path_to_latest_version.to_string_lossy().to_string(),
406 ))
407 } else {
408 // We don't check the latest version here because we haven't started
409 // the spinner yet
410 Ok(ToolStatus::NeedsLatestInstall)
411 }
412 }
413 }
414 }
415 }
416 }?;
417 // If we're considering installing something, we should make sure that there are
418 // actually precompiled binaries available for this platform (if there
419 // aren't, then we'll try to fall back on anything the user has installed
420 // locally, and if they have nothing, an error will be returned)
421 match initial_status {
422 ToolStatus::Available(path) => Ok(ToolStatus::Available(path)),
423 ToolStatus::NeedsInstall { version, .. } => {
424 // This will be `None` if there are no precompiled binaries available
425 let artifact_name = self.get_artifact_name(&version);
426 if let Some(artifact_name) = artifact_name {
427 // There are precompiled binaries available, which we prefer to preinstalled
428 // global ones
429 Ok(ToolStatus::NeedsInstall {
430 version,
431 artifact_name,
432 })
433 } else {
434 // If the user has something, we're good, but, if not, we have to fail
435 let preinstalled_path = self.get_path_to_preinstalled()?;
436 // We've got something, but it might not be the right version, so if the user
437 // told us to use a specific version, we should warn them
438 if self.user_given_version.is_some() {
439 eprintln!("[WARNING]: You requested a specific version, but no precompiled binaries of '{}' are available for your platform, so the version already installed on your system is being used. This may not correspond to the requested version!", self.name);
440 }
441 Ok(ToolStatus::Available(preinstalled_path))
442 }
443 }
444 ToolStatus::NeedsLatestInstall => {
445 // To get the proper artifact name for this, we would have to request the latest
446 // version, but we don't want to do that until a CLI spinner has
447 // been started, which is after the execution of this function (so we just use a
448 // dummy version to check if there would be binaries)
449 // This will be `None` if there are no precompiled binaries available
450 let artifact_name = self.get_artifact_name("dummy");
451 if artifact_name.is_some() {
452 // There are precompiled binaries available, which we prefer to preinstalled
453 // global ones
454 Ok(ToolStatus::NeedsLatestInstall)
455 } else {
456 // If the user has something, we're good, but, if not, we have to fail
457 let preinstalled_path = self.get_path_to_preinstalled()?;
458 // The user can't have requested a specific version if we're in the market for
459 // the latest one, so we can wrap up here
460 Ok(ToolStatus::Available(preinstalled_path))
461 }
462 }
463 }
464 }
465 /// Gets the latest version for this tool from its GitHub repository. One
466 /// should only bother executing this if we know there are precompiled
467 /// binaries for this platform.
468 pub async fn get_latest_version(&self) -> Result<String, InstallError> {
469 let json = Client::new()
470 .get(&format!(
471 "https://api.github.com/repos/{}/releases/latest",
472 self.gh_repo
473 ))
474 // This needs to display the name of the app for GH
475 .header("User-Agent", "perseus-cli")
476 .send()
477 .await
478 .map_err(|err| InstallError::GetLatestToolVersionFailed {
479 source: err,
480 tool: self.name.to_string(),
481 })?
482 .json::<serde_json::Value>()
483 .await
484 .map_err(|err| InstallError::GetLatestToolVersionFailed {
485 source: err,
486 tool: self.name.to_string(),
487 })?;
488 let latest_version =
489 json.get("name")
490 .ok_or_else(|| InstallError::ParseToolVersionFailed {
491 tool: self.name.to_string(),
492 })?;
493
494 Ok(latest_version
495 .as_str()
496 .ok_or_else(|| InstallError::ParseToolVersionFailed {
497 tool: self.name.to_string(),
498 })?
499 .to_string())
500 }
501 /// Installs the tool, taking the predetermined status as an argument to
502 /// avoid installing if the tool is actually already available, since
503 /// this method will be called on all tools if even one
504 /// is not available.
505 pub async fn install(&self, status: ToolStatus, target: &Path) -> Result<String, InstallError> {
506 // Do a sanity check to prevent installing something that already exists
507 match status {
508 ToolStatus::Available(path) => Ok(path),
509 ToolStatus::NeedsInstall {
510 version,
511 artifact_name,
512 } => self.install_version(&version, &artifact_name, target).await,
513 ToolStatus::NeedsLatestInstall => {
514 let latest_version = self.get_latest_version().await?;
515 // We *do* know at this point that there do exist precompiled binaries
516 let artifact_name = self.get_artifact_name(&latest_version).unwrap();
517 self.install_version(&latest_version, &artifact_name, target)
518 .await
519 }
520 }
521 }
522 /// Checks if the user already has this tool installed. This should only be
523 /// called if there are no precompiled binaries available of this tool
524 /// for the user's platform. In that case, this will also warn the user
525 /// if they've asked for a specific version that their own version might not
526 /// be that (we don't bother trying to parse the version of their
527 /// installed program).
528 ///
529 /// If there's nothing the user has installed, then this will return an
530 /// error, and hence it should only be called after all other options
531 /// have been exhausted.
532 fn get_path_to_preinstalled(&self) -> Result<String, InstallError> {
533 #[cfg(unix)]
534 let shell_exec = "sh";
535 #[cfg(windows)]
536 let shell_exec = "powershell";
537 #[cfg(unix)]
538 let shell_param = "-c";
539 #[cfg(windows)]
540 let shell_param = "-command";
541
542 let check_cmd = format!("{} --version", self.name); // Not exactly bulletproof, but it works!
543 let res = Command::new(shell_exec)
544 .args([shell_param, &check_cmd])
545 .output();
546 if let Err(err) = res {
547 // Unlike `wasm-pack`, we don't try to install with `cargo install`, because
548 // that's a little pointless to me (the user will still have to get
549 // `wasm-opt` somehow...)
550 //
551 // TODO Installation script that can build manually on any platform
552 Err(InstallError::ExternalToolUnavailable {
553 tool: self.name.to_string(),
554 source: err,
555 })
556 } else {
557 // It works, so we don't need to install anything
558 Ok(self.name.to_string())
559 }
560 }
561 /// Installs the given version of the tool, returning the path to the final
562 /// binary.
563 async fn install_version(
564 &self,
565 version: &str,
566 artifact_name: &str,
567 target: &Path,
568 ) -> Result<String, InstallError> {
569 let url = format!(
570 "https://github.com/{gh_repo}/releases/download/{version}/{artifact_name}.tar.gz",
571 artifact_name = artifact_name,
572 version = version,
573 gh_repo = self.gh_repo
574 );
575 let dir_name = format!("{}-{}", self.name, version);
576 let tar_name = format!("{}.tar.gz", &dir_name);
577 let dir_path = target.join(&dir_name);
578 let tar_path = target.join(&tar_name);
579
580 // Deal with placeholders in the name to expect from the extracted directory
581 let extracted_dir_name = self
582 .extracted_dir_name
583 .replace("%artifact_name", artifact_name)
584 .replace("%version", version);
585
586 // Download the tarball (source https://github.com/seanmonstar/reqwest/issues/1266#issuecomment-1106187437)
587 // We do this by chunking to minimize memory usage (we're downloading fairly
588 // large files!)
589 let mut res = Client::new().get(url).send().await.map_err(|err| {
590 InstallError::BinaryDownloadRequestFailed {
591 source: err,
592 tool: self.name.to_string(),
593 }
594 })?;
595 let mut file = tokio::fs::File::create(&tar_path)
596 .await
597 .map_err(|err| InstallError::CreateToolDownloadDestFailed { source: err })?;
598 while let Some(mut item) = res
599 .chunk()
600 .await
601 .map_err(|err| InstallError::ChunkBinaryDownloadFailed { source: err })?
602 {
603 file.write_all_buf(item.borrow_mut())
604 .await
605 .map_err(|err| InstallError::WriteBinaryDownloadChunkFailed { source: err })?;
606 }
607 // Now unzip the tarball
608 // TODO Async?
609 let tar_gz = File::open(&tar_path)
610 .map_err(|err| InstallError::CreateToolExtractDestFailed { source: err })?;
611 let mut archive = Archive::new(GzDecoder::new(tar_gz));
612 // We'll extract straight into `dist/tools/` and then rename the resulting
613 // directory
614 archive
615 .unpack(target)
616 .map_err(|err| InstallError::ToolExtractFailed {
617 source: err,
618 tool: self.name.to_string(),
619 })?;
620
621 // Now delete the original archive file
622 fs::remove_file(&tar_path)
623 .map_err(|err| InstallError::ArchiveDeletionFailed { source: err })?;
624 // Finally, rename the extracted directory
625 fs::rename(
626 target.join(extracted_dir_name), // We extracted into the root of the target
627 &dir_path,
628 )
629 .map_err(|err| InstallError::DirRenameFailed { source: err })?;
630
631 // Return the path inside the directory we extracted
632 Ok(dir_path
633 .join(&self.final_path)
634 .to_str()
635 .unwrap()
636 .to_string())
637 }
638 /// Gets the version of a specific package in `Cargo.lock`, assuming it has
639 /// already been generated.
640 fn get_pkg_version_from_lockfile(
641 &self,
642 lockfile: &Lockfile,
643 ) -> Result<Option<String>, InstallError> {
644 let version = lockfile
645 .packages
646 .iter()
647 .find(|p| p.name.as_str() == self.name)
648 .map(|p| p.version.to_string());
649 Ok(version)
650 }
651}
652
653/// A tool's status on-system.
654pub enum ToolStatus {
655 /// The tool needs to be installed.
656 NeedsInstall {
657 version: String,
658 artifact_name: String,
659 },
660 /// The latest version of the tool needs to be determined from its repo and
661 /// then installed.
662 NeedsLatestInstall,
663 /// The tool is already available at the attached path.
664 Available(String),
665}
666
667/// The types of tools we can install.
668pub enum ToolType {
669 /// The `wasm-bindgen` CLI, used for producing final Wasm and JS artifacts.
670 WasmBindgen,
671 /// Binaryen's `wasm-opt` CLI, used for optimizing Wasm in release builds
672 /// to achieve significant savings in bundle sizes.
673 WasmOpt,
674}
675impl ToolType {
676 /// Gets the tool's name.
677 pub fn name(&self) -> String {
678 match &self {
679 Self::WasmBindgen => "wasm-bindgen",
680 Self::WasmOpt => "wasm-opt",
681 }
682 .to_string()
683 }
684 /// Get's the path to the tool's binary inside the extracted directory
685 /// from the downloaded archive.
686 ///
687 /// Note that the return value of this function is uncorrected for Windows.
688 pub fn final_path(&self) -> String {
689 match &self {
690 Self::WasmBindgen => "wasm-bindgen",
691 Self::WasmOpt => "bin/wasm-opt",
692 }
693 .to_string()
694 }
695 /// Gets the GitHub repo to install this tool from.
696 pub fn gh_repo(&self) -> String {
697 match &self {
698 Self::WasmBindgen => "rustwasm/wasm-bindgen",
699 Self::WasmOpt => "WebAssembly/binaryen",
700 }
701 .to_string()
702 }
703 /// Gets the name of the directory that will be extracted from the
704 /// downloaded archive for this tool.
705 ///
706 /// This will return a `String` that might include placeholders for the
707 /// version and downloaded artifact name.
708 pub fn extracted_dir_name(&self) -> String {
709 match &self {
710 Self::WasmBindgen => "%artifact_name",
711 Self::WasmOpt => "binaryen-%version",
712 }
713 .to_string()
714 }
715}