Files
wfe/wfe-rustlang/src/rustup/step.rs
Sienna Meridian Satterwhite 0cb26df68b feat(wfe-rustlang): add Rust toolchain step executors
New crate providing cargo and rustup step types for WFE workflows:

Cargo steps: build, test, check, clippy, fmt, doc, publish
Rustup steps: rust-install, rustup-toolchain, rustup-component, rustup-target

Shared CargoConfig base with toolchain, package, features, release,
target, profile, extra_args, env, working_dir, and timeout support.
Toolchain override via rustup run for any cargo command.
2026-03-29 16:56:07 +01:00

358 lines
12 KiB
Rust

use async_trait::async_trait;
use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use crate::rustup::config::{RustupCommand, RustupConfig};
pub struct RustupStep {
config: RustupConfig,
}
impl RustupStep {
pub fn new(config: RustupConfig) -> Self {
Self { config }
}
pub fn build_command(&self) -> tokio::process::Command {
match self.config.command {
RustupCommand::Install => self.build_install_command(),
RustupCommand::ToolchainInstall => self.build_toolchain_install_command(),
RustupCommand::ComponentAdd => self.build_component_add_command(),
RustupCommand::TargetAdd => self.build_target_add_command(),
}
}
fn build_install_command(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("sh");
// Pipe rustup-init through sh with non-interactive flag.
let mut script = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y".to_string();
if let Some(ref profile) = self.config.profile {
script.push_str(&format!(" --profile {profile}"));
}
if let Some(ref tc) = self.config.default_toolchain {
script.push_str(&format!(" --default-toolchain {tc}"));
}
for arg in &self.config.extra_args {
script.push_str(&format!(" {arg}"));
}
cmd.arg("-c").arg(&script);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd
}
fn build_toolchain_install_command(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("rustup");
cmd.args(["toolchain", "install"]);
if let Some(ref tc) = self.config.toolchain {
cmd.arg(tc);
}
if let Some(ref profile) = self.config.profile {
cmd.args(["--profile", profile]);
}
for arg in &self.config.extra_args {
cmd.arg(arg);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd
}
fn build_component_add_command(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("rustup");
cmd.args(["component", "add"]);
for component in &self.config.components {
cmd.arg(component);
}
if let Some(ref tc) = self.config.toolchain {
cmd.args(["--toolchain", tc]);
}
for arg in &self.config.extra_args {
cmd.arg(arg);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd
}
fn build_target_add_command(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("rustup");
cmd.args(["target", "add"]);
for target in &self.config.targets {
cmd.arg(target);
}
if let Some(ref tc) = self.config.toolchain {
cmd.args(["--toolchain", tc]);
}
for arg in &self.config.extra_args {
cmd.arg(arg);
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd
}
}
#[async_trait]
impl StepBody for RustupStep {
async fn run(&mut self, context: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> {
let step_name = context.step.name.as_deref().unwrap_or("unknown");
let subcmd = self.config.command.as_str();
tracing::info!(step = step_name, command = subcmd, "running rustup");
let mut cmd = self.build_command();
let output = if let Some(timeout_ms) = self.config.timeout_ms {
let duration = std::time::Duration::from_millis(timeout_ms);
match tokio::time::timeout(duration, cmd.output()).await {
Ok(result) => result.map_err(|e| {
WfeError::StepExecution(format!("Failed to spawn rustup {subcmd}: {e}"))
})?,
Err(_) => {
return Err(WfeError::StepExecution(format!(
"rustup {subcmd} timed out after {timeout_ms}ms"
)));
}
}
} else {
cmd.output()
.await
.map_err(|e| WfeError::StepExecution(format!("Failed to spawn rustup {subcmd}: {e}")))?
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
return Err(WfeError::StepExecution(format!(
"rustup {subcmd} exited with code {code}\nstdout: {stdout}\nstderr: {stderr}"
)));
}
let mut outputs = serde_json::Map::new();
outputs.insert(
format!("{step_name}.stdout"),
serde_json::Value::String(stdout),
);
outputs.insert(
format!("{step_name}.stderr"),
serde_json::Value::String(stderr),
);
Ok(ExecutionResult {
proceed: true,
output_data: Some(serde_json::Value::Object(outputs)),
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn install_config() -> RustupConfig {
RustupConfig {
command: RustupCommand::Install,
toolchain: None,
components: vec![],
targets: vec![],
profile: None,
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
}
}
#[test]
fn build_install_command_minimal() {
let step = RustupStep::new(install_config());
let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "sh");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args[0], "-c");
assert!(args[1].contains("rustup.rs"));
assert!(args[1].contains("-y"));
}
#[test]
fn build_install_command_with_profile_and_toolchain() {
let mut config = install_config();
config.profile = Some("minimal".to_string());
config.default_toolchain = Some("nightly".to_string());
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert!(args[1].contains("--profile minimal"));
assert!(args[1].contains("--default-toolchain nightly"));
}
#[test]
fn build_install_command_with_extra_args() {
let mut config = install_config();
config.extra_args = vec!["--no-modify-path".to_string()];
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert!(args[1].contains("--no-modify-path"));
}
#[test]
fn build_toolchain_install_command() {
let config = RustupConfig {
command: RustupCommand::ToolchainInstall,
toolchain: Some("nightly-2024-06-01".to_string()),
components: vec![],
targets: vec![],
profile: Some("minimal".to_string()),
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["toolchain", "install", "nightly-2024-06-01", "--profile", "minimal"]);
}
#[test]
fn build_toolchain_install_with_extra_args() {
let config = RustupConfig {
command: RustupCommand::ToolchainInstall,
toolchain: Some("stable".to_string()),
components: vec![],
targets: vec![],
profile: None,
default_toolchain: None,
extra_args: vec!["--force".to_string()],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["toolchain", "install", "stable", "--force"]);
}
#[test]
fn build_component_add_command() {
let config = RustupConfig {
command: RustupCommand::ComponentAdd,
toolchain: Some("nightly".to_string()),
components: vec!["clippy".to_string(), "rustfmt".to_string()],
targets: vec![],
profile: None,
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["component", "add", "clippy", "rustfmt", "--toolchain", "nightly"]);
}
#[test]
fn build_component_add_without_toolchain() {
let config = RustupConfig {
command: RustupCommand::ComponentAdd,
toolchain: None,
components: vec!["rust-src".to_string()],
targets: vec![],
profile: None,
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["component", "add", "rust-src"]);
}
#[test]
fn build_target_add_command() {
let config = RustupConfig {
command: RustupCommand::TargetAdd,
toolchain: Some("stable".to_string()),
components: vec![],
targets: vec!["wasm32-unknown-unknown".to_string()],
profile: None,
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["target", "add", "wasm32-unknown-unknown", "--toolchain", "stable"]);
}
#[test]
fn build_target_add_multiple_targets() {
let config = RustupConfig {
command: RustupCommand::TargetAdd,
toolchain: None,
components: vec![],
targets: vec![
"wasm32-unknown-unknown".to_string(),
"aarch64-linux-android".to_string(),
],
profile: None,
default_toolchain: None,
extra_args: vec![],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(args, vec!["target", "add", "wasm32-unknown-unknown", "aarch64-linux-android"]);
}
#[test]
fn build_target_add_with_extra_args() {
let config = RustupConfig {
command: RustupCommand::TargetAdd,
toolchain: Some("nightly".to_string()),
components: vec![],
targets: vec!["x86_64-unknown-linux-musl".to_string()],
profile: None,
default_toolchain: None,
extra_args: vec!["--force".to_string()],
timeout_ms: None,
};
let step = RustupStep::new(config);
let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect();
assert_eq!(
args,
vec!["target", "add", "x86_64-unknown-linux-musl", "--toolchain", "nightly", "--force"]
);
}
}