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