320 lines
10 KiB
Rust
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")]
|
|
);
|
|
}
|
|
}
|