wasm_pack/install/
mod.rs

1//! Functionality related to installing prebuilt binaries and/or running cargo install.
2
3use self::krate::Krate;
4use crate::child;
5use crate::emoji;
6use crate::install;
7use crate::PBAR;
8use anyhow::{anyhow, bail, Context, Result};
9use binary_install::{Cache, Download};
10use log::debug;
11use log::{info, warn};
12use std::env;
13use std::fs;
14use std::path::Path;
15use std::process::Command;
16use which::which;
17
18mod arch;
19mod krate;
20mod mode;
21mod os;
22mod tool;
23pub use self::arch::Arch;
24pub use self::mode::InstallMode;
25pub use self::os::Os;
26pub use self::tool::Tool;
27
28/// Possible outcomes of attempting to find/install a tool
29pub enum Status {
30    /// Couldn't install tool because downloads are forbidden by user
31    CannotInstall,
32    /// The current platform doesn't support precompiled binaries for this tool
33    PlatformNotSupported,
34    /// We found the tool at the specified path
35    Found(Download),
36}
37
38/// Handles possible installs status and returns the download or a error message
39pub fn get_tool_path(status: &Status, tool: Tool) -> Result<&Download> {
40    match status {
41        Status::Found(download) => Ok(download),
42        Status::CannotInstall => bail!("Not able to find or install a local {}.", tool),
43        install::Status::PlatformNotSupported => {
44            bail!("{} does not currently support your platform.", tool)
45        }
46    }
47}
48
49/// Install a cargo CLI tool
50///
51/// Prefers an existing local install, if any exists. Then checks if there is a
52/// global install on `$PATH` that fits the bill. Then attempts to download a
53/// tarball from the GitHub releases page, if this target has prebuilt
54/// binaries. Finally, falls back to `cargo install`.
55pub fn download_prebuilt_or_cargo_install(
56    tool: Tool,
57    cache: &Cache,
58    version: &str,
59    install_permitted: bool,
60) -> Result<Status> {
61    // If the tool is installed globally and it has the right version, use
62    // that. Assume that other tools are installed next to it.
63    //
64    // This situation can arise if the tool is already installed via
65    // `cargo install`, for example.
66    if let Ok(path) = which(tool.to_string()) {
67        debug!("found global {} binary at: {}", tool, path.display());
68        if check_version(&tool, &path, version)? {
69            let download = Download::at(path.parent().unwrap());
70            return Ok(Status::Found(download));
71        }
72    }
73
74    let msg = format!("{}Installing {}...", emoji::DOWN_ARROW, tool);
75    PBAR.info(&msg);
76
77    let dl = download_prebuilt(&tool, cache, version, install_permitted);
78    match dl {
79        Ok(dl) => return Ok(dl),
80        Err(e) => {
81            warn!(
82                "could not download pre-built `{}`: {}. Falling back to `cargo install`.",
83                tool, e
84            );
85        }
86    }
87
88    cargo_install(tool, cache, version, install_permitted)
89}
90
91/// Check if the tool dependency is locally satisfied.
92pub fn check_version(tool: &Tool, path: &Path, expected_version: &str) -> Result<bool> {
93    let expected_version = if expected_version == "latest" {
94        let krate = Krate::new(tool)?;
95        krate.max_version
96    } else {
97        expected_version.to_string()
98    };
99
100    let v = get_cli_version(tool, path)?;
101    info!(
102        "Checking installed `{}` version == expected version: {} == {}",
103        tool, v, &expected_version
104    );
105    Ok(v == expected_version)
106}
107
108/// Fetches the version of a CLI tool
109pub fn get_cli_version(tool: &Tool, path: &Path) -> Result<String> {
110    let mut cmd = Command::new(path);
111    cmd.arg("--version");
112    let stdout = child::run_capture_stdout(cmd, tool)?;
113    let version = stdout.trim().split_whitespace().nth(1);
114    match version {
115        Some(v) => Ok(v.to_string()),
116        None => bail!("Something went wrong! We couldn't determine your version of the wasm-bindgen CLI. We were supposed to set that up for you, so it's likely not your fault! You should file an issue: https://github.com/rustwasm/wasm-pack/issues/new?template=bug_report.md.")
117    }
118}
119
120/// Downloads a precompiled copy of the tool, if available.
121pub fn download_prebuilt(
122    tool: &Tool,
123    cache: &Cache,
124    version: &str,
125    install_permitted: bool,
126) -> Result<Status> {
127    let url = match prebuilt_url(tool, version) {
128        Ok(url) => url,
129        Err(e) => bail!(
130            "no prebuilt {} binaries are available for this platform: {}",
131            tool,
132            e,
133        ),
134    };
135    match tool {
136        Tool::WasmBindgen => {
137            let binaries = &["wasm-bindgen", "wasm-bindgen-test-runner"];
138            match cache.download(install_permitted, "wasm-bindgen", binaries, &url)? {
139                Some(download) => Ok(Status::Found(download)),
140                None => bail!("wasm-bindgen v{} is not installed!", version),
141            }
142        }
143        Tool::CargoGenerate => {
144            let binaries = &["cargo-generate"];
145            match cache.download(install_permitted, "cargo-generate", binaries, &url)? {
146                Some(download) => Ok(Status::Found(download)),
147                None => bail!("cargo-generate v{} is not installed!", version),
148            }
149        }
150        Tool::WasmOpt => {
151            let binaries: &[&str] = match Os::get()? {
152                Os::MacOS => &["bin/wasm-opt", "lib/libbinaryen.dylib"],
153                Os::Linux => &["bin/wasm-opt"],
154                Os::Windows => &["bin/wasm-opt.exe"],
155            };
156            match cache.download(install_permitted, "wasm-opt", binaries, &url)? {
157                Some(download) => Ok(Status::Found(download)),
158                // TODO(ag_dubs): why is this different? i forget...
159                None => Ok(Status::CannotInstall),
160            }
161        }
162    }
163}
164
165/// Returns the URL of a precompiled version of wasm-bindgen, if we have one
166/// available for our host platform.
167fn prebuilt_url(tool: &Tool, version: &str) -> Result<String> {
168    let os = Os::get()?;
169    let arch = Arch::get()?;
170    prebuilt_url_for(tool, version, &arch, &os)
171}
172
173/// Get the download URL for some tool at some version, architecture and operating system
174pub fn prebuilt_url_for(tool: &Tool, version: &str, arch: &Arch, os: &Os) -> Result<String> {
175    let target = match (os, arch, tool) {
176        (Os::Linux, Arch::AArch64, Tool::WasmOpt) => "aarch64-linux",
177        (Os::Linux, Arch::AArch64, _) => "aarch64-unknown-linux-gnu",
178        (Os::Linux, Arch::X86_64, Tool::WasmOpt) => "x86_64-linux",
179        (Os::Linux, Arch::X86_64, _) => "x86_64-unknown-linux-musl",
180        (Os::MacOS, Arch::X86_64, Tool::WasmOpt) => "x86_64-macos",
181        (Os::MacOS, Arch::X86_64, _) => "x86_64-apple-darwin",
182        (Os::MacOS, Arch::AArch64, Tool::CargoGenerate) => "aarch64-apple-darwin",
183        (Os::MacOS, Arch::AArch64, Tool::WasmOpt) => "arm64-macos",
184        (Os::Windows, Arch::X86_64, Tool::WasmOpt) => "x86_64-windows",
185        (Os::Windows, Arch::X86_64, _) => "x86_64-pc-windows-msvc",
186        _ => bail!("Unrecognized target!"),
187    };
188    match tool {
189        Tool::WasmBindgen => {
190            Ok(format!(
191                "https://github.com/rustwasm/wasm-bindgen/releases/download/{0}/wasm-bindgen-{0}-{1}.tar.gz",
192                version,
193                target
194            ))
195        },
196        Tool::CargoGenerate => {
197            Ok(format!(
198                "https://github.com/cargo-generate/cargo-generate/releases/download/v{0}/cargo-generate-v{0}-{1}.tar.gz",
199                "0.18.2",
200                target
201            ))
202        },
203        Tool::WasmOpt => {
204            Ok(format!(
205        "https://github.com/WebAssembly/binaryen/releases/download/{vers}/binaryen-{vers}-{target}.tar.gz",
206        vers = "version_117",
207        target = target,
208            ))
209        }
210    }
211}
212
213/// Use `cargo install` to install the tool locally into the given
214/// crate.
215pub fn cargo_install(
216    tool: Tool,
217    cache: &Cache,
218    version: &str,
219    install_permitted: bool,
220) -> Result<Status> {
221    debug!(
222        "Attempting to use a `cargo install`ed version of `{}={}`",
223        tool, version,
224    );
225
226    let dirname = format!("{}-cargo-install-{}", tool, version);
227    let destination = cache.join(dirname.as_ref());
228    if destination.exists() {
229        debug!(
230            "`cargo install`ed `{}={}` already exists at {}",
231            tool,
232            version,
233            destination.display()
234        );
235        let download = Download::at(&destination);
236        return Ok(Status::Found(download));
237    }
238
239    if !install_permitted {
240        return Ok(Status::CannotInstall);
241    }
242
243    // Run `cargo install` to a temporary location to handle ctrl-c gracefully
244    // and ensure we don't accidentally use stale files in the future
245    let tmp = cache.join(format!(".{}", dirname).as_ref());
246    drop(fs::remove_dir_all(&tmp));
247    debug!("cargo installing {} to tempdir: {}", tool, tmp.display(),);
248
249    let context = format!("failed to create temp dir for `cargo install {}`", tool);
250    fs::create_dir_all(&tmp).context(context)?;
251
252    let crate_name = match tool {
253        Tool::WasmBindgen => "wasm-bindgen-cli".to_string(),
254        _ => tool.to_string(),
255    };
256    let mut cmd = Command::new("cargo");
257
258    cmd.arg("install")
259        .arg("--force")
260        .arg(crate_name)
261        .arg("--root")
262        .arg(&tmp);
263
264    if version != "latest" {
265        cmd.arg("--version").arg(version);
266    }
267
268    let context = format!("Installing {} with cargo", tool);
269    child::run(cmd, "cargo install").context(context)?;
270
271    // `cargo install` will put the installed binaries in `$root/bin/*`, but we
272    // just want them in `$root/*` directly (which matches how the tarballs are
273    // laid out, and where the rest of our code expects them to be). So we do a
274    // little renaming here.
275    let binaries: Result<Vec<&str>> = match tool {
276        Tool::WasmBindgen => Ok(vec!["wasm-bindgen", "wasm-bindgen-test-runner"]),
277        Tool::CargoGenerate => Ok(vec!["cargo-generate"]),
278        Tool::WasmOpt => bail!("Cannot install wasm-opt with cargo."),
279    };
280
281    for b in binaries?.iter().cloned() {
282        let from = tmp
283            .join("bin")
284            .join(b)
285            .with_extension(env::consts::EXE_EXTENSION);
286        let to = tmp.join(from.file_name().unwrap());
287        fs::rename(&from, &to).with_context(|| {
288            anyhow!(
289                "failed to move {} to {} for `cargo install`ed `{}`",
290                from.display(),
291                to.display(),
292                b
293            )
294        })?;
295    }
296
297    // Finally, move the `tmp` directory into our binary cache.
298    fs::rename(&tmp, &destination)?;
299
300    let download = Download::at(&destination);
301    Ok(Status::Found(download))
302}