ambient-ci 0.14.0

A continuous integration engine
Documentation
use std::{path::PathBuf, process::Command};

use clingwrap::runner::{CommandError, CommandRunner};
use serde::{Deserialize, Serialize};

use crate::{
    action::{ActionError, Context},
    action_impl::ActionImpl,
    runlog::RunLogSource,
    util::{mkdir, UtilError},
};

/// Subdirectory of the dependencies directory for files `rustup` downloads.
pub const RUSTUP_DIR: &str = "rustup";

const DEFAULT_CHANNEL: &str = "stable";

/// Install Rust toolchains and components, using rustup.
/// The host needs to have `rustup` installed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rustup {
    channel: String,
    target: Option<String>,
    components: Option<Vec<String>>,
    profile: Option<String>,
}

impl Rustup {
    /// Create a new `Rustup` action.
    pub fn new(
        channel: &Option<String>,
        target: &Option<String>,
        components: &Option<Vec<String>>,
        profile: &Option<String>,
    ) -> Self {
        Self {
            channel: channel.clone().unwrap_or(DEFAULT_CHANNEL.to_string()),
            target: target.to_owned(),
            components: components.to_owned(),
            profile: profile.to_owned(),
        }
    }
}

impl ActionImpl for Rustup {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        context
            .runlog()
            .debug(RunLogSource::PrePlan, format!("rustup {self:?}"));

        let dir = context.deps_dir().join(RUSTUP_DIR);
        if !dir.exists() {
            mkdir(&dir).map_err(|err| RustupError::RustupDir(dir.clone(), err))?;
        }

        context.set_plan_env("RUSTUP_HOME", format!("/ci/deps/{RUSTUP_DIR}"));

        let mut cmd = Command::new("rustup");
        cmd.env("RUSTUP_HOME", dir.as_os_str());
        cmd.arg("install");
        cmd.arg(&self.channel);
        if let Some(v) = &self.target {
            cmd.arg("--target");
            cmd.arg(v);
        }
        if let Some(v) = &self.profile {
            cmd.arg("--profile");
            cmd.arg(v);
        }
        if let Some(list) = &self.components {
            cmd.arg("--component");
            let mut v = String::new();
            for c in list {
                if !v.is_empty() {
                    v.push(',');
                }
                v.push_str(c);
            }
            cmd.arg(v);
        }

        context.runlog().start_program(RunLogSource::PrePlan, &cmd);
        let mut runner = CommandRunner::new(cmd);
        runner.capture_stdout();
        runner.capture_stderr();
        match runner.execute() {
            Ok(output) => {
                context
                    .runlog()
                    .program_succeeded(RunLogSource::PrePlan, &output);
                Ok(())
            }
            Err(err) => {
                context.runlog().program_failed(RunLogSource::PrePlan, &err);
                Err(RustupError::Rustup(err).into())
            }
        }
    }
}

/// Errors from the Pwd action.
#[derive(Debug, thiserror::Error)]
pub enum RustupError {
    /// Can't run rustup.
    #[error("failed to run rustup")]
    Rustup(#[source] CommandError),

    /// Can't create dependencies dirs for rustup.
    #[error("failed to create rustup directory {0}")]
    RustupDir(PathBuf, #[source] UtilError),
}

impl From<RustupError> for ActionError {
    fn from(value: RustupError) -> Self {
        Self::Rustup(value)
    }
}