Skip to main content

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/wasm-bindgen/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, Tool::WasmBindgen) if wasm_bindgen_since(version, 104) => {
178            "aarch64-unknown-linux-musl"
179        }
180        (Os::Linux, Arch::AArch64, Tool::CargoGenerate) => "aarch64-unknown-linux-musl",
181        (Os::Linux, Arch::AArch64, _) => "aarch64-unknown-linux-gnu",
182        (Os::Linux, Arch::Riscv64, Tool::WasmBindgen) if wasm_bindgen_since(version, 127) => {
183            "riscv64gc-unknown-linux-gnu"
184        }
185        (Os::Linux, Arch::X86_64, Tool::WasmOpt) => "x86_64-linux",
186        (Os::Linux, Arch::X86_64, Tool::CargoGenerate) => "x86_64-unknown-linux-gnu",
187        (Os::Linux, Arch::X86_64, _) => "x86_64-unknown-linux-musl",
188        (Os::MacOS, Arch::X86_64, Tool::WasmOpt) => "x86_64-macos",
189        (Os::MacOS, Arch::X86_64, _) => "x86_64-apple-darwin",
190        (Os::MacOS, Arch::AArch64, Tool::WasmOpt) => "arm64-macos",
191        (Os::MacOS, Arch::AArch64, _) => "aarch64-apple-darwin",
192        (Os::Windows, Arch::X86_64, Tool::WasmOpt) => "x86_64-windows",
193        (Os::Windows, Arch::X86_64, _) => "x86_64-pc-windows-msvc",
194        (Os::Windows, Arch::AArch64, Tool::WasmOpt) => "arm64-windows",
195        // wasm-bindgen and cargo-generate publish no Windows ARM64 builds, so
196        // use the x64 ones, which Windows 11 on ARM runs under emulation.
197        (Os::Windows, Arch::AArch64, _) => "x86_64-pc-windows-msvc",
198        _ => bail!("Unrecognized target!"),
199    };
200    match tool {
201        Tool::WasmBindgen => {
202            Ok(format!(
203                "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/{0}/wasm-bindgen-{0}-{1}.tar.gz",
204                version,
205                target
206            ))
207        },
208        Tool::CargoGenerate => {
209            Ok(format!(
210                "https://github.com/cargo-generate/cargo-generate/releases/download/v{0}/cargo-generate-v{0}-{1}.tar.gz",
211                "0.23.7",
212                target
213            ))
214        },
215        Tool::WasmOpt => {
216            Ok(format!(
217        "https://github.com/WebAssembly/binaryen/releases/download/{vers}/binaryen-{vers}-{target}.tar.gz",
218        vers = "version_130", // Make sure to update the version in docs/src/cargo-toml-configuration.md as well
219        target = target,
220            ))
221        }
222    }
223}
224
225/// Whether `version` is a wasm-bindgen release at or after 0.2.`patch`, for
226/// release targets that older versions don't publish.
227fn wasm_bindgen_since(version: &str, patch: u64) -> bool {
228    semver::Version::parse(version).is_ok_and(|v| v >= semver::Version::new(0, 2, patch))
229}
230
231/// Use `cargo install` to install the tool locally into the given
232/// crate.
233pub fn cargo_install(
234    tool: Tool,
235    cache: &Cache,
236    version: &str,
237    install_permitted: bool,
238) -> Result<Status> {
239    debug!(
240        "Attempting to use a `cargo install`ed version of `{}={}`",
241        tool, version,
242    );
243
244    let dirname = format!("{}-cargo-install-{}", tool, version);
245    let destination = cache.join(dirname.as_ref());
246    if destination.exists() {
247        debug!(
248            "`cargo install`ed `{}={}` already exists at {}",
249            tool,
250            version,
251            destination.display()
252        );
253        let download = Download::at(&destination);
254        return Ok(Status::Found(download));
255    }
256
257    if !install_permitted {
258        return Ok(Status::CannotInstall);
259    }
260
261    // Run `cargo install` to a temporary location to handle ctrl-c gracefully
262    // and ensure we don't accidentally use stale files in the future
263    let tmp = cache.join(format!(".{}-pid{}", dirname, std::process::id(),).as_ref());
264    drop(fs::remove_dir_all(&tmp));
265    debug!("cargo installing {} to tempdir: {}", tool, tmp.display(),);
266
267    let context = format!("failed to create temp dir for `cargo install {}`", tool);
268    fs::create_dir_all(&tmp).context(context)?;
269
270    let crate_name = match tool {
271        Tool::WasmBindgen => "wasm-bindgen-cli".to_string(),
272        _ => tool.to_string(),
273    };
274    let mut cmd = Command::new("cargo");
275
276    cmd.arg("install")
277        .arg("--force")
278        .arg(crate_name)
279        .arg("--root")
280        .arg(&tmp)
281        .arg("--target-dir")
282        .arg(tmp.join("build"));
283
284    if version != "latest" {
285        cmd.arg("--version").arg(version);
286    }
287
288    let context = format!("Installing {} with cargo", tool);
289    child::run(cmd, "cargo install").context(context)?;
290
291    // `cargo install` will put the installed binaries in `$root/bin/*`, but we
292    // just want them in `$root/*` directly (which matches how the tarballs are
293    // laid out, and where the rest of our code expects them to be). So we do a
294    // little renaming here.
295    let binaries: Result<Vec<&str>> = match tool {
296        Tool::WasmBindgen => Ok(vec!["wasm-bindgen", "wasm-bindgen-test-runner"]),
297        Tool::CargoGenerate => Ok(vec!["cargo-generate"]),
298        Tool::WasmOpt => bail!("Cannot install wasm-opt with cargo."),
299    };
300
301    for b in binaries?.iter().cloned() {
302        let from = tmp
303            .join("bin")
304            .join(b)
305            .with_extension(env::consts::EXE_EXTENSION);
306        let to = tmp.join(from.file_name().unwrap());
307        fs::rename(&from, &to).with_context(|| {
308            anyhow!(
309                "failed to move {} to {} for `cargo install`ed `{}`",
310                from.display(),
311                to.display(),
312                b
313            )
314        })?;
315    }
316
317    // Finally, move the `tmp` directory into our binary cache.
318    fs::rename(&tmp, &destination)?;
319
320    let download = Download::at(&destination);
321    Ok(Status::Found(download))
322}