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.
358 lines
12 KiB
Rust
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"]
|
|
);
|
|
}
|
|
}
|