Replace the dev server's wasm-pack, cargo-watch, and concurrently tools with custom cargo-run tooling (#4254)

Replace wasm-pack, cargo-watch and concurrently with custom build tooling

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2026-06-21 12:48:31 -07:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 1f05000238
commit a72ac39059
22 changed files with 977 additions and 478 deletions
+197 -69
View File
@@ -1,15 +1,17 @@
use semver::{Version, VersionReq};
use std::io::IsTerminal;
use std::process::Command;
use crate::cmd::prelude::*;
use crate::*;
#[derive(Default, Clone)]
struct Requirement {
pub struct Requirement {
command: &'static str,
args: &'static [&'static str],
name: &'static str,
check: Check,
version: Option<&'static str>,
install: Option<&'static str>,
install: InstallAction,
skip: Option<&'static dyn Fn(&Task) -> bool>,
}
@@ -22,43 +24,43 @@ fn requirements(task: &Task) -> Vec<Requirement> {
..Default::default()
},
Requirement {
command: "cargo-about",
args: &["--version"],
name: "cargo-about",
install: Some("cargo install cargo-about"),
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",
install: "rustup target add wasm32-unknown-unknown".into(),
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
Requirement {
command: "cargo-watch",
command: "cargo-about",
args: &["--version"],
name: "cargo-watch",
install: Some("cargo install cargo-watch"),
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"),
skip: Some(&|task| {
!matches!(
task,
Task {
target: Target::Web,
action: Action::Run,
..
matches!(task.target, Target::Cli)
|| match task.profile {
Profile::Debug => true,
Profile::Release => false,
Profile::Default => matches!(task.action, Action::Run),
}
)
}),
..Default::default()
},
Requirement {
command: "wasm-bindgen",
args: &["--version"],
name: "wasm-bindgen-cli",
version: Some("0.2.121"),
install: Some("cargo install -f wasm-bindgen-cli@0.2.121"),
skip: Some(&|task| matches!(task.target, Target::Cli)),
},
Requirement {
command: "wasm-pack",
args: &["--version"],
name: "wasm-pack",
install: Some("cargo install wasm-pack"),
name: "Wasm Bindgen",
version: Some("=0.2.121"),
install: "cargo install -f wasm-bindgen-cli@0.2.121".into(),
skip: Some(&|task| matches!(task.target, Target::Cli)),
..Default::default()
},
@@ -98,24 +100,44 @@ pub fn check(task: &Task) -> Result<(), Error> {
let mut failures: Vec<String> = Vec::new();
for dep in requirements(task) {
match Command::new(dep.command).args(dep.args).output() {
match cmd(dep.command, dep.args.iter().copied()).output_unchecked() {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout);
let version = version.lines().next().unwrap_or_default().trim();
if let Some(expected) = dep.version {
if version.contains(expected) {
eprintln!(" ✓ {} ({})", dep.name, version);
} else {
eprintln!(" ✗ {} (found {}, expected {})", dep.name, version, expected);
if dep.install.is_some() {
installable.push(dep);
let stdout = String::from_utf8_lossy(&output.stdout);
match dep.check {
Check::PrintsVersion => {
let line = stdout.lines().next().unwrap_or_default().trim();
match dep.version {
None => eprintln!(" ✓ {} ({line})", dep.name),
Some(req_str) => {
let req = VersionReq::parse(req_str).expect("invalid semver requirement");
match extract_version(line) {
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() {
installable.push(dep);
} else {
failures.push(format!("{}: version mismatch (found {version}, requires {req_str})", dep.name));
}
}
None => {
eprintln!(" ✗ {} (could not parse version from '{line}')", dep.name);
failures.push(format!("{}: could not parse version from '{line}'", dep.name));
}
}
}
}
}
Check::Matches(check) => {
if !check(stdout.to_string()) {
eprintln!(" ✗ {} - check failed", dep.name);
if dep.install.is_some() {
installable.push(dep);
}
} else {
failures.push(format!("{}: version mismatch (found {version}, expected {expected})", dep.name));
eprintln!(" ✓ {}", dep.name);
}
}
} else {
eprintln!(" ✓ {} ({})", dep.name, version);
}
}
Ok(output) => {
@@ -147,7 +169,7 @@ pub fn check(task: &Task) -> Result<(), Error> {
let total = installable.len() + failures.len();
eprintln!("{total} requirement{} not met:", if total > 1 { "s" } else { "" });
for dep in &installable {
eprintln!(" - {}: {}", dep.name, dep.install.unwrap());
eprintln!(" - {}", dep.name);
}
for msg in &failures {
eprintln!(" - {msg}");
@@ -158,43 +180,149 @@ pub fn check(task: &Task) -> Result<(), Error> {
eprintln!("See: https://graphite.art/volunteer/guide/project-setup/");
}
let is_interactive = std::io::stdout().is_terminal() && std::io::stderr().is_terminal() && std::io::stdin().is_terminal();
// Don't prompt for automatic installation if we're not interactive session
if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() || !std::io::stdin().is_terminal() {
if !is_interactive {
return Ok(());
}
if installable.is_empty() {
if installable.is_empty() && failures.is_empty() {
return Ok(());
}
eprintln!();
eprintln!("The following can be installed automatically:");
for dep in &installable {
eprintln!(" {}", dep.install.unwrap());
}
eprintln!();
if installable.len() == 1 {
eprint!("Install it now? [Y/n] ");
} else {
eprint!("Install them now? [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.is_empty() || input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
if !installable.is_empty() {
eprintln!();
eprintln!("The following can be installed automatically:");
for dep in &installable {
let parts: Vec<&str> = dep.install.unwrap().split_whitespace().collect();
eprintln!("Running: {}...", dep.install.unwrap());
let status = Command::new(parts[0])
.args(&parts[1..])
.status()
.map_err(|e| Error::Io(e, format!("Failed to run '{}'", dep.install.unwrap())))?;
if !status.success() {
eprintln!("Failed to install {}", dep.name);
eprintln!(" {}: {}", dep.name, dep.install.description());
}
eprintln!();
if installable.len() == 1 {
eprint!("Install it now? [Y/n] ");
} else {
eprint!("Install them now? [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("y") || input.eq_ignore_ascii_case("yes") {
let mut successfully_installed = Vec::new();
for (i, dep) in installable.iter().enumerate() {
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),
Err(e) => return Err(Error::Command(e)),
}
}
InstallAction::Function { function, .. } => {
if let Err(e) = function() {
eprintln!("{e}");
eprintln!("Failed to install {}", dep.name);
} else {
successfully_installed.push(i);
}
}
InstallAction::None => unreachable!(),
}
}
for i in successfully_installed.into_iter().rev() {
installable.remove(i);
}
}
}
if !failures.is_empty() {
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]");
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") {
return Err(Error::RequirementsNotMet);
}
}
Ok(())
}
fn extract_version(line: &str) -> Option<Version> {
line.split_whitespace().find_map(|token| {
let token = token.trim_start_matches('v').trim_end_matches(|c: char| !c.is_ascii_alphanumeric());
if token.is_empty() {
return None;
}
if let Ok(version) = Version::parse(token) {
return Some(version);
}
let (core, suffix) = match token.find(['-', '+']) {
Some(i) => token.split_at(i),
None => (token, ""),
};
let parts: Vec<&str> = core.split('.').collect();
if parts.iter().any(|p| p.is_empty() || !p.chars().all(|c| c.is_ascii_digit())) {
return None;
}
let major = parts[0];
let minor = parts.get(1).copied().unwrap_or("0");
let patch = parts.get(2).copied().unwrap_or("0");
Version::parse(&format!("{major}.{minor}.{patch}{suffix}")).ok()
})
}
#[derive(Clone, Default)]
enum Check {
#[default]
PrintsVersion,
Matches(&'static dyn Fn(String) -> bool),
}
#[derive(Clone, Default)]
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>,
},
}
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,
}
}
}
impl From<&'static str> for InstallAction {
fn from(value: &'static str) -> Self {
InstallAction::Command(value)
}
}