Files
honey-biscuit-workshop/workshop-engine/src/upm/rust.rs
T
Catty Steve 681b7d1333
CI / ${{ matrix.crate }} (workshop-engine) (push) Failing after 36m14s
CI / ${{ matrix.crate }} (workshop-baker) (push) Successful in 1h35m51s
style: format all code to comply with cargo fmt check
2026-04-28 19:17:56 +08:00

320 lines
10 KiB
Rust

use crate::error::DependencyError;
use crate::types::{DeltaExecutionContext, Engine, EventSender, ExecutionContext};
use crate::upm::UPMSysDeps;
use async_trait::async_trait;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use super::UserPackageManager;
/// Default manifest paths used when `manifests` is not specified
/// in the `[rust.cargo]` sub-config.
///
/// The bootstrap stage creates `/workspace` as the standard project
/// root. Most Docker-based builds have a single `Cargo.toml` here.
fn default_manifests() -> Vec<PathBuf> {
vec![PathBuf::from("/workspace/Cargo.toml")]
}
#[derive(Clone)]
pub struct Rust;
// ── RustConfig (root) ─────────────────────────────────────────────
/// Top-level configuration for the `rust` UPM.
///
/// ```yaml
/// rust:
/// toolchain: "nightly-2024-01-01"
/// components: ["clippy", "rustfmt"]
/// targets: ["wasm32-unknown-unknown"]
/// cargo:
/// fetch: true
/// locked: true
/// manifests: ["/workspace/Cargo.toml"]
/// packages: ["cargo-audit"]
/// ```
#[derive(Debug, Clone, Deserialize)]
struct RustConfig {
#[allow(dead_code)]
/// UPM backend type (currently only `"rustup"`).
#[serde(default = "default_rust_type", rename = "type")]
type_: String,
/// Environment variables injected into every rustup/cargo child process.
/// Each entry maps to a var name; `None` unsets the variable.
#[serde(default, rename = "env")]
env_: HashMap<String, Option<String>>,
/// Rust toolchain to activate via `rustup default`.
/// If unset, the existing default toolchain is left alone.
#[serde(default)]
toolchain: Option<String>,
/// Rustup components to install (e.g. `clippy`, `rustfmt`).
#[serde(default)]
components: Vec<String>,
/// Compilation targets for cross-compilation.
/// Each entry runs `rustup target add <target>`.
#[serde(default)]
targets: Vec<String>,
/// Cargo sub-configuration (fetch, install, manifests, …).
/// Optional — omit if you only need rustup-level setup.
#[serde(default)]
cargo: CargoSubConfig,
}
fn default_rust_type() -> String {
"rustup".to_string()
}
// ── Cargo sub-config ───────────────────────────────────────────────
/// Nested cargo configuration inside `rust`.
#[derive(Debug, Clone, Deserialize, Default)]
struct CargoSubConfig {
/// Crate names to install via `cargo install`.
#[serde(default)]
packages: Vec<String>,
/// Alternative registry URL (passed as `--registry`).
#[serde(default)]
registry: Option<String>,
/// Additional flags forwarded to `cargo install`
/// (e.g. `--force`, `--version 0.1.0`).
#[serde(default)]
args: Vec<String>,
/// One or more `Cargo.toml` paths for `cargo fetch`.
/// Each path spawns a separate `cargo fetch --manifest-path <path>`.
#[serde(default = "default_manifests")]
manifests: Vec<PathBuf>,
/// When `true`, run `cargo fetch` before installing packages.
#[serde(default)]
fetch: bool,
/// Require an up-to-date `Cargo.lock` (passes `--locked`).
#[serde(default)]
locked: bool,
}
// ── UserPackageManager impl ────────────────────────────────────────
#[async_trait]
impl UserPackageManager for Rust {
fn name(&self) -> &'static str {
"rust"
}
fn system_dependency(&self, _config: &serde_yaml::Value) -> UPMSysDeps {
UPMSysDeps::new(vec!["rustup".to_string()], false)
}
async fn main(
&self,
config: &serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), DependencyError> {
let cfg: RustConfig = serde_yaml::from_value(config.clone()).map_err(|e| {
log::error!("Failed to parse user.rust config: {}", e);
DependencyError::ParseError()
})?;
let engine = Engine::new();
let mut commands: Vec<String> = Vec::new();
// ── 1. rustup default <toolchain> ─────────────────────────
if let Some(ref tc) = cfg.toolchain {
commands.push(format!("rustup default '{}'", tc));
}
// ── 2. rustup component add <comp…> ──────────────────────
if !cfg.components.is_empty() {
commands.push(format!("rustup component add {}", cfg.components.join(" ")));
}
// ── 3. rustup target add <target…> ───────────────────────
if !cfg.targets.is_empty() {
commands.push(format!("rustup target add {}", cfg.targets.join(" ")));
}
// ── 4. cargo fetch ───────────────────────────────────────
//
// `cargo fetch` without --target only fetches host-target
// dependencies. For cross-compilation we must fetch once
// per (manifest, target) pair.
let cargo = &cfg.cargo;
if cargo.fetch {
let mut base = String::from("cargo fetch");
if cargo.locked {
base.push_str(" --locked");
}
for manifest in &cargo.manifests {
// Host target (no --target)
commands.push(format!("{} --manifest-path '{}'", base, manifest.display()));
// Cross-compilation targets
for target in &cfg.targets {
commands.push(format!(
"{} --target '{}' --manifest-path '{}'",
base,
target,
manifest.display()
));
}
}
}
// ── 5. cargo install ─────────────────────────────────────
if !cargo.packages.is_empty() {
let mut cmd = String::from("cargo install");
if cargo.locked {
cmd.push_str(" --locked");
}
if let Some(ref registry) = cargo.registry {
cmd.push_str(&format!(" --registry '{}'", registry));
}
for a in &cargo.args {
cmd.push(' ');
cmd.push_str(a);
}
for pkg in &cargo.packages {
cmd.push(' ');
cmd.push_str(pkg);
}
commands.push(cmd);
}
// ── Execute ──────────────────────────────────────────────
for cmd in &commands {
log::info!("Rust UPM executing: {}", cmd);
let delta = DeltaExecutionContext {
env_vars: cfg.env_.clone(),
..Default::default()
};
let result = engine
.execute_command_with_delta(cmd, ctx, event_tx, &delta)
.await;
if let Err(e) = result {
log::error!("Rust command failed: {}", e);
return Err(DependencyError::CustomFailed(e));
}
}
if commands.is_empty() {
log::warn!("Rust UPM configured with nothing to do");
}
Ok(())
}
}
// ── Tests ──────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_name() {
assert_eq!(Rust.name(), "rust");
}
#[test]
fn test_system_dependency() {
let deps = Rust.system_dependency(&serde_yaml::Value::Null);
assert_eq!(deps.packages, vec!["rustup"]);
assert!(!deps.mapped);
}
#[test]
fn test_parse_empty() {
let yaml = "{}";
let cfg: RustConfig = serde_yaml::from_str(yaml).unwrap();
assert!(cfg.toolchain.is_none());
assert!(cfg.components.is_empty());
assert!(cfg.targets.is_empty());
assert!(!cfg.cargo.fetch);
}
#[test]
fn test_parse_rustup_only() {
let yaml = r#"
toolchain: "nightly-2024-01-01"
components: ["clippy", "rustfmt"]
targets: ["wasm32-unknown-unknown", "x86_64-unknown-linux-musl"]
"#;
let cfg: RustConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cfg.toolchain.as_deref(), Some("nightly-2024-01-01"));
assert_eq!(cfg.components, vec!["clippy", "rustfmt"]);
assert_eq!(
cfg.targets,
vec!["wasm32-unknown-unknown", "x86_64-unknown-linux-musl"]
);
assert!(!cfg.cargo.fetch);
}
#[test]
fn test_parse_cargo_subconfig() {
let yaml = r#"
components: ["clippy"]
cargo:
fetch: true
locked: true
manifests:
- "/workspace/foo/Cargo.toml"
- "/workspace/bar/Cargo.toml"
packages: ["cargo-audit"]
registry: "https://mirrors.tuna.tsinghua.edu.cn/crates.io-index"
"#;
let cfg: RustConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cfg.components, vec!["clippy"]);
let c = &cfg.cargo;
assert!(c.fetch);
assert!(c.locked);
assert_eq!(
c.manifests,
vec![
PathBuf::from("/workspace/foo/Cargo.toml"),
PathBuf::from("/workspace/bar/Cargo.toml"),
]
);
assert_eq!(c.packages, vec!["cargo-audit"]);
assert_eq!(
c.registry.as_deref(),
Some("https://mirrors.tuna.tsinghua.edu.cn/crates.io-index")
);
}
#[test]
fn test_cargo_defaults() {
let yaml = r#"
components: ["rustfmt"]
cargo: {}
"#;
let cfg: RustConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cfg.components, vec!["rustfmt"]);
assert!(!cfg.cargo.fetch);
assert!(!cfg.cargo.locked);
assert_eq!(
cfg.cargo.manifests,
vec![PathBuf::from("/workspace/Cargo.toml")]
);
}
#[test]
fn test_default_manifests() {
assert_eq!(
default_manifests(),
vec![PathBuf::from("/workspace/Cargo.toml")]
);
}
}