1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-11-26 16:12:31 +02:00

refactor cargo xtasks (#2907)

* improve error handling
* show executed command lines
* reduce code duplication (run_command, run_mdbook_command)
This commit is contained in:
michael-kerscher
2025-09-18 23:48:15 +02:00
committed by GitHub
parent d6be949511
commit 33bc3f4088

View File

@@ -19,18 +19,14 @@
//! `cargo xtask install-tools` and the logic defined here will install
//! the tools.
use anyhow::{Ok, Result, anyhow};
use anyhow::{Context, Result, anyhow};
use clap::{Parser, Subcommand};
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::process::Command;
fn main() -> Result<()> {
if let Err(e) = execute_task() {
eprintln!("{e}");
std::process::exit(-1);
}
Ok(())
execute_task()
}
#[derive(Parser)]
@@ -83,11 +79,28 @@ enum Task {
fn execute_task() -> Result<()> {
let cli = Cli::parse();
match cli.task {
Task::InstallTools => install_tools()?,
Task::WebTests { dir } => run_web_tests(dir)?,
Task::RustTests => run_rust_tests()?,
Task::Serve { language, output } => start_web_server(language, output)?,
Task::Build { language, output } => build(language, output)?,
Task::InstallTools => install_tools(),
Task::WebTests { dir } => run_web_tests(dir),
Task::RustTests => run_rust_tests(),
Task::Serve { language, output } => start_web_server(language, output),
Task::Build { language, output } => build(language, output),
}
}
/// Executes a command and returns an error if it fails.
fn run_command(cmd: &mut Command) -> Result<()> {
let command_display = format!("{cmd:?}");
println!("> {command_display}");
let status = cmd
.status()
.with_context(|| format!("Failed to execute command: {command_display}"))?;
if !status.success() {
let exit_description = if let Some(code) = status.code() {
format!("exited with status code: {}", code)
} else {
"was terminated by a signal".to_string()
};
return Err(anyhow!("Command `{command_display}` {exit_description}"));
}
Ok(())
}
@@ -97,155 +110,126 @@ fn install_tools() -> Result<()> {
const PINNED_NIGHTLY: &str = "nightly-2025-09-01";
// Install rustup components
let rustup_steps = [
["toolchain", "install", "--profile", "minimal", PINNED_NIGHTLY],
["component", "add", "rustfmt", "--toolchain", PINNED_NIGHTLY],
];
for args in rustup_steps {
let status = std::process::Command::new("rustup").args(args).status()?;
if !status.success() {
return Err(anyhow!(
"Command 'rustup {}' failed with status {:?}",
args.join(" "),
status.code()
));
}
let mut cmd = Command::new("rustup");
cmd.args(args);
run_command(&mut cmd)?;
}
let path_to_mdbook_exerciser =
Path::new(env!("CARGO_WORKSPACE_DIR")).join("mdbook-exerciser");
let path_to_mdbook_course =
Path::new(env!("CARGO_WORKSPACE_DIR")).join("mdbook-course");
let install_args = vec![
// The --locked flag is important for reproducible builds. It also
// avoids breakage due to skews between mdbook and mdbook-svgbob.
vec!["mdbook", "--locked", "--version", "0.4.52"],
vec!["mdbook-svgbob", "--locked", "--version", "0.2.2"],
vec!["mdbook-pandoc", "--locked", "--version", "0.10.4"],
vec!["mdbook-i18n-helpers", "--locked", "--version", "0.3.6"],
vec!["i18n-report", "--locked", "--version", "0.2.0"],
vec!["mdbook-linkcheck2", "--locked", "--version", "0.9.1"],
// Mdbook-exerciser and mdbook-course are located in this repository.
// To make it possible to install them from any directory we need to
// specify their path from the workspace root.
vec!["--path", path_to_mdbook_exerciser.to_str().unwrap(), "--locked"],
vec!["--path", path_to_mdbook_course.to_str().unwrap(), "--locked"],
let cargo = env!("CARGO");
// The --locked flag is important for reproducible builds.
let tools = [
("mdbook", "0.4.52"),
("mdbook-svgbob", "0.2.2"),
("mdbook-pandoc", "0.10.4"),
("mdbook-i18n-helpers", "0.3.6"),
("i18n-report", "0.2.0"),
("mdbook-linkcheck2", "0.9.1"),
];
for args in &install_args {
let status = Command::new(env!("CARGO"))
.arg("install")
.args(args)
.status()
.expect("Failed to execute cargo install");
for (tool, version) in tools {
let mut cmd = Command::new(cargo);
cmd.args(["install", tool, "--version", version, "--locked"]);
run_command(&mut cmd)?;
}
if !status.success() {
let error_message = format!(
"Command 'cargo install {}' exited with status code: {}",
args.join(" "),
status.code().unwrap()
);
return Err(anyhow!(error_message));
}
// Install local tools from the workspace.
let workspace_dir = Path::new(env!("CARGO_WORKSPACE_DIR"));
let local_tools = ["mdbook-exerciser", "mdbook-course"];
for tool in local_tools {
let mut cmd = Command::new(cargo);
cmd.args(["install", "--path"])
.arg(workspace_dir.join(tool))
.arg("--locked");
run_command(&mut cmd)?;
}
// Uninstall original linkcheck if currently installed (see issue no 2773)
uninstall_mdbook_linkcheck();
uninstall_mdbook_linkcheck()?;
Ok(())
}
fn uninstall_mdbook_linkcheck() {
fn uninstall_mdbook_linkcheck() -> Result<()> {
println!("Uninstalling old mdbook-linkcheck if installed...");
let output = Command::new(env!("CARGO"))
.arg("uninstall")
.arg("mdbook-linkcheck")
.args(["uninstall", "mdbook-linkcheck"])
.output()
.expect("Failed to execute cargo uninstall mdbook-linkcheck");
.context("Failed to execute `cargo uninstall mdbook-linkcheck`")?;
if !output.status.success() {
if String::from_utf8_lossy(&output.stderr)
.into_owned()
.contains("did not match any packages")
{
println!("mdbook-linkcheck not installed. Continuing...");
} else {
eprintln!(
"An error occurred during uninstallation of mdbook-linkcheck:\n{:#?}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
// This specific error is OK, it just means the package wasn't installed.
if !stderr.contains("did not match any packages") {
return Err(anyhow!(
"Failed to uninstall `mdbook-linkcheck`.\n--- stderr:\n{stderr}"
));
}
println!("mdbook-linkcheck not installed. Continuing...");
}
Ok(())
}
fn run_web_tests(dir: Option<PathBuf>) -> Result<()> {
println!("Running web tests...");
let workspace_dir = Path::new(env!("CARGO_WORKSPACE_DIR"));
let absolute_dir = dir.map(|d| d.canonicalize()).transpose()?;
if let Some(d) = &absolute_dir {
println!("Refreshing slide lists...");
let path_to_refresh_slides_script = Path::new("tests")
let refresh_slides_script = Path::new("tests")
.join("src")
.join("slides")
.join("create-slide.list.sh");
let status = Command::new(path_to_refresh_slides_script)
.current_dir(Path::new(env!("CARGO_WORKSPACE_DIR")))
.arg(d)
.status()
.expect("Failed to execute create-slide.list.sh");
if !status.success() {
let error_message = format!(
"Command 'cargo xtask web-tests' exited with status code: {}",
status.code().unwrap()
);
return Err(anyhow!(error_message));
}
let mut cmd = Command::new(&refresh_slides_script);
cmd.current_dir(workspace_dir).arg(d);
run_command(&mut cmd)?;
}
let path_to_tests_dir = Path::new(env!("CARGO_WORKSPACE_DIR")).join("tests");
let mut command = Command::new("npm");
command.current_dir(path_to_tests_dir.to_str().unwrap());
command.arg("test");
let tests_dir = workspace_dir.join("tests");
let mut cmd = Command::new("npm");
cmd.current_dir(tests_dir).arg("test");
if let Some(d) = absolute_dir {
command.env("TEST_BOOK_DIR", d);
cmd.env("TEST_BOOK_DIR", d);
}
let status = command.status().expect("Failed to execute npm test");
if !status.success() {
let error_message = format!(
"Command 'cargo xtask web-tests' exited with status code: {}",
status.code().unwrap()
);
return Err(anyhow!(error_message));
}
Ok(())
run_command(&mut cmd)
}
fn run_rust_tests() -> Result<()> {
println!("Running rust tests...");
let path_to_workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let status = Command::new("mdbook")
.current_dir(path_to_workspace_root.to_str().unwrap())
.arg("test")
.status()
.expect("Failed to execute mdbook test");
let mut cmd = Command::new("mdbook");
cmd.current_dir(workspace_root).arg("test");
run_command(&mut cmd)
}
if !status.success() {
let error_message = format!(
"Command 'cargo xtask rust-tests' exited with status code: {}",
status.code().unwrap()
);
return Err(anyhow!(error_message));
fn run_mdbook_command(
subcommand: &str,
language: Option<String>,
output_arg: Option<PathBuf>,
) -> Result<()> {
let workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let mut cmd = Command::new("mdbook");
cmd.current_dir(workspace_root).arg(subcommand);
if let Some(language) = &language {
println!("Language: {language}");
cmd.env("MDBOOK_BOOK__LANGUAGE", language);
}
Ok(())
cmd.arg("-d");
cmd.arg(get_output_dir(language, output_arg));
run_command(&mut cmd)
}
fn start_web_server(
@@ -253,58 +237,12 @@ fn start_web_server(
output_arg: Option<PathBuf>,
) -> Result<()> {
println!("Starting web server ...");
let path_to_workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let mut command = Command::new("mdbook");
command.current_dir(path_to_workspace_root.to_str().unwrap());
command.arg("serve");
if let Some(language) = &language {
println!("Language: {}", &language);
command.env("MDBOOK_BOOK__LANGUAGE", &language);
}
command.arg("-d");
command.arg(get_output_dir(language, output_arg));
let status = command.status().expect("Failed to execute mdbook serve");
if !status.success() {
let error_message = format!(
"Command 'cargo xtask serve' exited with status code: {}",
status.code().unwrap()
);
return Err(anyhow!(error_message));
}
Ok(())
run_mdbook_command("serve", language, output_arg)
}
fn build(language: Option<String>, output_arg: Option<PathBuf>) -> Result<()> {
println!("Building course...");
let path_to_workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let mut command = Command::new("mdbook");
command.current_dir(path_to_workspace_root.to_str().unwrap());
command.arg("build");
if let Some(language) = &language {
println!("Language: {}", &language);
command.env("MDBOOK_BOOK__LANGUAGE", language);
}
command.arg("-d");
command.arg(get_output_dir(language, output_arg));
let status = command.status().expect("Failed to execute mdbook build");
if !status.success() {
let error_message = format!(
"Command 'cargo xtask build' exited with status code: {}",
status.code().unwrap()
);
return Err(anyhow!(error_message));
}
Ok(())
run_mdbook_command("build", language, output_arg)
}
fn get_output_dir(language: Option<String>, output_arg: Option<PathBuf>) -> PathBuf {