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.
This commit is contained in:
183
wfe-rustlang/src/rustup/config.rs
Normal file
183
wfe-rustlang/src/rustup/config.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Which rustup operation to perform.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum RustupCommand {
|
||||
/// Install Rust via rustup-init.
|
||||
Install,
|
||||
/// Install a toolchain (`rustup toolchain install`).
|
||||
ToolchainInstall,
|
||||
/// Add a component (`rustup component add`).
|
||||
ComponentAdd,
|
||||
/// Add a compilation target (`rustup target add`).
|
||||
TargetAdd,
|
||||
}
|
||||
|
||||
impl RustupCommand {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Install => "install",
|
||||
Self::ToolchainInstall => "toolchain-install",
|
||||
Self::ComponentAdd => "component-add",
|
||||
Self::TargetAdd => "target-add",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for rustup step types.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RustupConfig {
|
||||
pub command: RustupCommand,
|
||||
/// Toolchain to install or scope components/targets to (e.g. "nightly", "1.78.0").
|
||||
#[serde(default)]
|
||||
pub toolchain: Option<String>,
|
||||
/// Components to add (e.g. ["clippy", "rustfmt", "rust-src"]).
|
||||
#[serde(default)]
|
||||
pub components: Vec<String>,
|
||||
/// Compilation targets to add (e.g. ["wasm32-unknown-unknown"]).
|
||||
#[serde(default)]
|
||||
pub targets: Vec<String>,
|
||||
/// Rustup profile for initial install: "minimal", "default", or "complete".
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
/// Default toolchain to set during install.
|
||||
#[serde(default)]
|
||||
pub default_toolchain: Option<String>,
|
||||
/// Additional arguments appended to the command.
|
||||
#[serde(default)]
|
||||
pub extra_args: Vec<String>,
|
||||
/// Execution timeout in milliseconds.
|
||||
#[serde(default)]
|
||||
pub timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn command_as_str() {
|
||||
assert_eq!(RustupCommand::Install.as_str(), "install");
|
||||
assert_eq!(RustupCommand::ToolchainInstall.as_str(), "toolchain-install");
|
||||
assert_eq!(RustupCommand::ComponentAdd.as_str(), "component-add");
|
||||
assert_eq!(RustupCommand::TargetAdd.as_str(), "target-add");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_serde_kebab_case() {
|
||||
let json = r#""toolchain-install""#;
|
||||
let cmd: RustupCommand = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(cmd, RustupCommand::ToolchainInstall);
|
||||
|
||||
let serialized = serde_json::to_string(&RustupCommand::ComponentAdd).unwrap();
|
||||
assert_eq!(serialized, r#""component-add""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip_install() {
|
||||
let config = RustupConfig {
|
||||
command: RustupCommand::Install,
|
||||
toolchain: None,
|
||||
components: vec![],
|
||||
targets: vec![],
|
||||
profile: Some("minimal".to_string()),
|
||||
default_toolchain: Some("stable".to_string()),
|
||||
extra_args: vec![],
|
||||
timeout_ms: Some(300_000),
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let de: RustupConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(de.command, RustupCommand::Install);
|
||||
assert_eq!(de.profile, Some("minimal".to_string()));
|
||||
assert_eq!(de.default_toolchain, Some("stable".to_string()));
|
||||
assert_eq!(de.timeout_ms, Some(300_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip_toolchain_install() {
|
||||
let config = RustupConfig {
|
||||
command: RustupCommand::ToolchainInstall,
|
||||
toolchain: Some("nightly-2024-06-01".to_string()),
|
||||
components: vec![],
|
||||
targets: vec![],
|
||||
profile: None,
|
||||
default_toolchain: None,
|
||||
extra_args: vec![],
|
||||
timeout_ms: None,
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let de: RustupConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(de.command, RustupCommand::ToolchainInstall);
|
||||
assert_eq!(de.toolchain, Some("nightly-2024-06-01".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip_component_add() {
|
||||
let config = RustupConfig {
|
||||
command: RustupCommand::ComponentAdd,
|
||||
toolchain: Some("nightly".to_string()),
|
||||
components: vec!["clippy".to_string(), "rustfmt".to_string(), "rust-src".to_string()],
|
||||
targets: vec![],
|
||||
profile: None,
|
||||
default_toolchain: None,
|
||||
extra_args: vec![],
|
||||
timeout_ms: None,
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let de: RustupConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(de.command, RustupCommand::ComponentAdd);
|
||||
assert_eq!(de.components, vec!["clippy", "rustfmt", "rust-src"]);
|
||||
assert_eq!(de.toolchain, Some("nightly".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip_target_add() {
|
||||
let config = RustupConfig {
|
||||
command: RustupCommand::TargetAdd,
|
||||
toolchain: Some("stable".to_string()),
|
||||
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 json = serde_json::to_string(&config).unwrap();
|
||||
let de: RustupConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(de.command, RustupCommand::TargetAdd);
|
||||
assert_eq!(de.targets, vec!["wasm32-unknown-unknown", "aarch64-linux-android"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults() {
|
||||
let json = r#"{"command": "install"}"#;
|
||||
let config: RustupConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.command, RustupCommand::Install);
|
||||
assert!(config.toolchain.is_none());
|
||||
assert!(config.components.is_empty());
|
||||
assert!(config.targets.is_empty());
|
||||
assert!(config.profile.is_none());
|
||||
assert!(config.default_toolchain.is_none());
|
||||
assert!(config.extra_args.is_empty());
|
||||
assert!(config.timeout_ms.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_with_extra_args() {
|
||||
let config = RustupConfig {
|
||||
command: RustupCommand::ToolchainInstall,
|
||||
toolchain: Some("nightly".to_string()),
|
||||
components: vec![],
|
||||
targets: vec![],
|
||||
profile: Some("minimal".to_string()),
|
||||
default_toolchain: None,
|
||||
extra_args: vec!["--force".to_string()],
|
||||
timeout_ms: None,
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let de: RustupConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(de.extra_args, vec!["--force"]);
|
||||
}
|
||||
}
|
||||
5
wfe-rustlang/src/rustup/mod.rs
Normal file
5
wfe-rustlang/src/rustup/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod config;
|
||||
pub mod step;
|
||||
|
||||
pub use config::{RustupCommand, RustupConfig};
|
||||
pub use step::RustupStep;
|
||||
357
wfe-rustlang/src/rustup/step.rs
Normal file
357
wfe-rustlang/src/rustup/step.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
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"]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user