Make the cargo-run tool auto-install its wasm-opt build dependency (#4256)

* Allow auto downloading wasm-opt

* Fix for mac

* Review
This commit is contained in:
Timon
2026-06-21 20:17:54 +00:00
committed by GitHub
parent a72ac39059
commit d753ce26f7
9 changed files with 327 additions and 69 deletions
+16
View File
@@ -84,6 +84,14 @@ pub enum TerminalColor {
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Reset,
}
impl TerminalColor {
@@ -97,6 +105,14 @@ impl TerminalColor {
Self::Magenta => "\x1b[35m",
Self::Cyan => "\x1b[36m",
Self::White => "\x1b[37m",
Self::BrightBlack => "\x1b[90m",
Self::BrightRed => "\x1b[91m",
Self::BrightGreen => "\x1b[92m",
Self::BrightYellow => "\x1b[93m",
Self::BrightBlue => "\x1b[94m",
Self::BrightMagenta => "\x1b[95m",
Self::BrightCyan => "\x1b[96m",
Self::BrightWhite => "\x1b[97m",
Self::Reset => "\x1b[0m",
}
}
+8
View File
@@ -77,6 +77,14 @@ pub fn target_dir() -> PathBuf {
}
}
pub fn install_dir() -> PathBuf {
target_dir().join("cargo-run")
}
pub fn bin_dir() -> PathBuf {
install_dir().join("bin")
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("One or more requirements were not met")]
+1 -1
View File
@@ -140,7 +140,7 @@ fn run_task(task: &Task) -> Result<(), Error> {
}
fn prepend_path() {
let mut paths = vec![target_dir().join("cargo-run").join("bin")];
let mut paths = vec![bin_dir()];
if let Some(path) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&path));
}
+66 -64
View File
@@ -4,6 +4,8 @@ use std::io::IsTerminal;
use crate::cmd::prelude::*;
use crate::*;
mod wasm_opt;
#[derive(Default, Clone)]
pub struct Requirement {
command: &'static str,
@@ -27,24 +29,17 @@ fn requirements(task: &Task) -> Vec<Requirement> {
command: "rustc",
args: &["--print", "target-libdir", "--target", "wasm32-unknown-unknown"],
check: Check::Matches(&|out| std::path::Path::new(out.trim()).is_dir()),
name: "Rust Wasm Toolchain",
name: "Rust (Wasm Target)",
install: "rustup target add wasm32-unknown-unknown".into(),
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
Requirement {
command: "cargo-about",
args: &["--version"],
name: "Cargo About",
install: "cargo install cargo-about".into(),
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
Requirement {
command: "wasm-opt",
args: &["--version"],
name: "Wasm Opt",
version: Some(">=130"),
install: wasm_opt::install_action(),
skip: Some(&|task| {
matches!(task.target, Target::Cli)
|| match task.profile {
@@ -64,6 +59,14 @@ fn requirements(task: &Task) -> Vec<Requirement> {
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
Requirement {
command: "cargo-about",
args: &["--version"],
name: "Cargo About",
install: "cargo install cargo-about".into(),
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
Requirement {
command: "node",
args: &["--version"],
@@ -75,6 +78,7 @@ fn requirements(task: &Task) -> Vec<Requirement> {
command: "cmake",
args: &["--version"],
name: "CMake",
install: InstallAction::ManualInstructions("https://cmake.org/download/ or find it in your system's package manager"),
skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")),
..Default::default()
},
@@ -82,6 +86,7 @@ fn requirements(task: &Task) -> Vec<Requirement> {
command: "ninja",
args: &["--version"],
name: "Ninja",
install: InstallAction::ManualInstructions("https://github.com/ninja-build/ninja/releases or find it in your system's package manager"),
skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")),
..Default::default()
},
@@ -97,7 +102,7 @@ pub fn check(task: &Task) -> Result<(), Error> {
eprintln!("Checking Requirements:");
let mut installable: Vec<Requirement> = Vec::new();
let mut failures: Vec<String> = Vec::new();
let mut manual: Vec<(Requirement, String)> = Vec::new();
for dep in requirements(task) {
match cmd(dep.command, dep.args.iter().copied()).output_unchecked() {
@@ -114,15 +119,15 @@ pub fn check(task: &Task) -> Result<(), Error> {
Some(version) if req.matches(&version) => eprintln!(" ✓ {} ({version})", dep.name),
Some(version) => {
eprintln!(" ✗ {} (found {version}, requires {req_str})", dep.name);
if dep.install.is_some() {
if dep.install.is_auto_installable() {
installable.push(dep);
} else {
failures.push(format!("{}: version mismatch (found {version}, requires {req_str})", dep.name));
manual.push((dep, format!("version mismatch (found {version}, requires {req_str})")));
}
}
None => {
eprintln!(" ✗ {} (could not parse version from '{line}')", dep.name);
failures.push(format!("{}: could not parse version from '{line}'", dep.name));
manual.push((dep, format!("could not parse version from '{line}'")));
}
}
}
@@ -131,8 +136,10 @@ pub fn check(task: &Task) -> Result<(), Error> {
Check::Matches(check) => {
if !check(stdout.to_string()) {
eprintln!(" ✗ {} - check failed", dep.name);
if dep.install.is_some() {
if dep.install.is_auto_installable() {
installable.push(dep);
} else {
manual.push((dep, "check failed".into()));
}
} else {
eprintln!(" ✓ {}", dep.name);
@@ -143,18 +150,18 @@ pub fn check(task: &Task) -> Result<(), Error> {
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!(" ✗ {} - command failed: {}", dep.name, stderr.trim());
if dep.install.is_some() {
if dep.install.is_auto_installable() {
installable.push(dep);
} else {
failures.push(format!("{}: not installed or not working", dep.name));
manual.push((dep.clone(), format!("`{}` not installed or not working", dep.command)));
}
}
Err(_) => {
eprintln!(" ✗ {} - not found", dep.name);
if dep.install.is_some() {
if dep.install.is_auto_installable() {
installable.push(dep);
} else {
failures.push(format!("{}: not found in PATH", dep.name));
manual.push((dep.clone(), format!("`{}` not found in PATH", dep.command)));
}
}
}
@@ -162,20 +169,20 @@ pub fn check(task: &Task) -> Result<(), Error> {
eprintln!();
if installable.is_empty() && failures.is_empty() {
if installable.is_empty() && manual.is_empty() {
return Ok(());
}
let total = installable.len() + failures.len();
let total = installable.len() + manual.len();
eprintln!("{total} requirement{} not met:", if total > 1 { "s" } else { "" });
for dep in &installable {
eprintln!(" - {}", dep.name);
}
for msg in &failures {
eprintln!(" - {msg}");
for (dep, msg) in &manual {
eprintln!(" - {}: {msg}", dep.name);
}
if !failures.is_empty() {
if !manual.is_empty() {
eprintln!();
eprintln!("See: https://graphite.art/volunteer/guide/project-setup/");
}
@@ -187,7 +194,7 @@ pub fn check(task: &Task) -> Result<(), Error> {
return Ok(());
}
if installable.is_empty() && failures.is_empty() {
if installable.is_empty() && manual.is_empty() {
return Ok(());
}
@@ -195,13 +202,17 @@ pub fn check(task: &Task) -> Result<(), Error> {
eprintln!();
eprintln!("The following can be installed automatically:");
for dep in &installable {
eprintln!(" {}: {}", dep.name, dep.install.description());
match &dep.install {
InstallAction::Command(cmd) => eprintln!(" - {}: {}", dep.name, cmd),
InstallAction::Expression { description, .. } => eprintln!(" - {description}"),
InstallAction::None | InstallAction::ManualInstructions(_) => unreachable!(),
}
}
eprintln!();
if installable.len() == 1 {
eprint!("Install it now? [Y/n] ");
eprint!("Install it now? [y/N] ");
} else {
eprint!("Install them now? [Y/n] ");
eprint!("Install them now? [y/N] ");
}
let mut input = String::new();
@@ -209,55 +220,54 @@ pub fn check(task: &Task) -> Result<(), Error> {
let input = input.trim();
if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
let mut successfully_installed = Vec::new();
for (i, dep) in installable.iter().enumerate() {
for dep in installable.into_iter() {
eprintln!("Installing {}...", dep.name);
match &dep.install {
InstallAction::Command(install_cmd) => {
let parts: Vec<&str> = install_cmd.split_whitespace().collect();
let expr = cmd(parts[0], parts[1..].iter().copied()).unchecked();
match Expression::run(&expr) {
Ok(output) if output.status.success() => successfully_installed.push(i),
Ok(_) => eprintln!("Failed to install {}", dep.name),
Ok(output) if !output.status.success() => {
let stderr = String::from_utf8_lossy(&output.stderr);
manual.push((dep, format!("installation command failed: {}", stderr.trim())));
}
Err(e) => return Err(Error::Command(e)),
_ => {}
}
}
InstallAction::Function { function, .. } => {
if let Err(e) = function() {
InstallAction::Expression { expression, .. } => {
if let Err(e) = expression.clone().run() {
eprintln!("{e}");
eprintln!("Failed to install {}", dep.name);
} else {
successfully_installed.push(i);
manual.push((dep, format!("failed to install ({e})")));
}
}
InstallAction::None => unreachable!(),
InstallAction::None | InstallAction::ManualInstructions(_) => unreachable!(),
}
}
}
}
for i in successfully_installed.into_iter().rev() {
installable.remove(i);
if !manual.is_empty() {
eprintln!();
eprintln!("Please install the following dependenc{}:", if manual.len() == 1 { "y" } else { "ies" });
for (dep, msg) in &manual {
match dep.install {
InstallAction::ManualInstructions(instructions) => eprintln!(" - {}: {}", dep.name, instructions),
_ => eprintln!(" - {}: {}", dep.name, msg),
}
}
}
if !failures.is_empty() {
if (!manual.is_empty()) && is_interactive {
eprintln!();
eprintln!("The following requirements must be resolved manually:");
for msg in &failures {
eprintln!(" - {msg}");
}
}
if (!installable.is_empty() || !failures.is_empty()) && is_interactive {
eprintln!();
eprintln!("Continue without resolving these requirements? [y/N]");
eprintln!("Attempt to continue regardless of unmet requirements? [y/N]");
let mut input = String::new();
std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?;
let input = input.trim();
if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
if !input.eq_ignore_ascii_case("y") && !input.eq_ignore_ascii_case("yes") {
return Err(Error::RequirementsNotMet);
}
}
@@ -300,24 +310,16 @@ enum InstallAction {
#[default]
None,
Command(&'static str),
#[expect(dead_code)] // TODO: Remove after followup pr landed
Function {
description: &'static str,
function: &'static dyn Fn() -> Result<(), Error>,
Expression {
description: String,
expression: Expression,
},
ManualInstructions(&'static str),
}
impl InstallAction {
fn is_some(&self) -> bool {
!matches!(self, InstallAction::None)
}
fn description(&self) -> &'static str {
match self {
InstallAction::None => "",
InstallAction::Command(cmd) => cmd,
InstallAction::Function { description, .. } => description,
}
fn is_auto_installable(&self) -> bool {
!matches!(self, InstallAction::None | InstallAction::ManualInstructions(_))
}
}
@@ -0,0 +1,46 @@
use super::InstallAction;
use crate::cmd::prelude::*;
use crate::{install_dir, workspace_dir};
/// Pinned Binaryen release used by [`install_action`].
/// NOTICE: keep in sync with the `BINARYEN_VERSION` pinned across the CI workflows, and update [`SHA256`] below.
const VERSION: &str = "130";
const SHA256: &[(&str, &str)] = &[
("x86_64-windows", "cc09c874f4332d00aa32ab72745a9b98c9a172f795762f21d03e70638a3f7f4c"),
("arm64-windows", "b18c9cbe000562b1ee5d9cb60146616a949aca504903ad63f27fd9fd679898a7"),
("arm64-macos", "79d3ab9f417d9e215f15f598f523d001a7d9ac1e59367e5c869fbdabd1cba72e"),
("x86_64-macos", "d3e2d1235b70c93c54b52eabc1625ea960965152218754f1f4eeb0f873c48e03"),
("x86_64-linux", "0a18362361ad05465118cd8eeb72edaeec89de6894bc283576ef4e07aa3babcc"),
("aarch64-linux", "e6ae6e09ac40f4e14bc5be6f687c58e2995c84170013975fa641809dd3b480a0"),
];
fn url_for(platform: &str) -> String {
format!("https://github.com/WebAssembly/binaryen/releases/download/version_{VERSION}/binaryen-version_{VERSION}-{platform}.tar.gz")
}
pub fn install_action() -> InstallAction {
let platform = match (std::env::consts::OS, std::env::consts::ARCH) {
("windows", "x86_64") => "x86_64-windows",
("windows", "aarch64") => "arm64-windows",
("macos", "aarch64") => "arm64-macos",
("macos", "x86_64") => "x86_64-macos",
("linux", "x86_64") => "x86_64-linux",
("linux", "aarch64") => "aarch64-linux",
_ => return InstallAction::None,
};
let url = url_for(platform);
let Some(sha256) = SHA256.iter().find_map(|(p, s)| (*p == platform).then_some(*s)) else {
return InstallAction::None;
};
let out = install_dir().to_string_lossy().into_owned();
let description = format!("Download wasm-opt {VERSION} from {url} (sha256 {sha256})");
let args = [&url, sha256, &out, "--extract", "--strip", "1", "--include", "bin/wasm-opt"];
#[cfg(target_os = "macos")]
let args = args.into_iter().chain(["--include", "lib/libbinaryen.dylib"]).collect::<Vec<_>>();
let expression = utils::internal("download").args(args).dir(workspace_dir());
InstallAction::Expression { description, expression }
}