Skip to main content

ambient_ci/action_impl/
rustup.rs

1use std::{path::PathBuf, process::Command};
2
3use clingwrap::runner::{CommandError, CommandRunner};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    action::{ActionError, Context},
8    action_impl::ActionImpl,
9    runlog::RunLogSource,
10    util::{mkdir, UtilError},
11};
12
13/// Subdirectory of the dependencies directory for files `rustup` downloads.
14pub const RUSTUP_DIR: &str = "rustup";
15
16const DEFAULT_CHANNEL: &str = "stable";
17
18/// Install Rust toolchains and components, using rustup.
19/// The host needs to have `rustup` installed.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct Rustup {
22    channel: String,
23    target: Option<String>,
24    components: Option<Vec<String>>,
25    profile: Option<String>,
26}
27
28impl Rustup {
29    /// Create a new `Rustup` action.
30    pub fn new(
31        channel: &Option<String>,
32        target: &Option<String>,
33        components: &Option<Vec<String>>,
34        profile: &Option<String>,
35    ) -> Self {
36        Self {
37            channel: channel.clone().unwrap_or(DEFAULT_CHANNEL.to_string()),
38            target: target.to_owned(),
39            components: components.to_owned(),
40            profile: profile.to_owned(),
41        }
42    }
43}
44
45impl ActionImpl for Rustup {
46    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
47        context
48            .runlog()
49            .debug(RunLogSource::PrePlan, format!("rustup {self:?}"));
50
51        let dir = context.deps_dir().join(RUSTUP_DIR);
52        if !dir.exists() {
53            mkdir(&dir).map_err(|err| RustupError::RustupDir(dir.clone(), err))?;
54        }
55
56        context.set_plan_env("RUSTUP_HOME", format!("/ci/deps/{RUSTUP_DIR}"));
57
58        let mut cmd = Command::new("rustup");
59        cmd.env("RUSTUP_HOME", dir.as_os_str());
60        cmd.arg("install");
61        cmd.arg(&self.channel);
62        if let Some(v) = &self.target {
63            cmd.arg("--target");
64            cmd.arg(v);
65        }
66        if let Some(v) = &self.profile {
67            cmd.arg("--profile");
68            cmd.arg(v);
69        }
70        if let Some(list) = &self.components {
71            cmd.arg("--component");
72            let mut v = String::new();
73            for c in list {
74                if !v.is_empty() {
75                    v.push(',');
76                }
77                v.push_str(c);
78            }
79            cmd.arg(v);
80        }
81
82        context.runlog().start_program(RunLogSource::PrePlan, &cmd);
83        let mut runner = CommandRunner::new(cmd);
84        runner.capture_stdout();
85        runner.capture_stderr();
86        match runner.execute() {
87            Ok(output) => {
88                context
89                    .runlog()
90                    .program_succeeded(RunLogSource::PrePlan, &output);
91                Ok(())
92            }
93            Err(err) => {
94                context.runlog().program_failed(RunLogSource::PrePlan, &err);
95                Err(RustupError::Rustup(err).into())
96            }
97        }
98    }
99}
100
101/// Errors from the Pwd action.
102#[derive(Debug, thiserror::Error)]
103pub enum RustupError {
104    /// Can't run rustup.
105    #[error("failed to run rustup")]
106    Rustup(#[source] CommandError),
107
108    /// Can't create dependencies dirs for rustup.
109    #[error("failed to create rustup directory {0}")]
110    RustupDir(PathBuf, #[source] UtilError),
111}
112
113impl From<RustupError> for ActionError {
114    fn from(value: RustupError) -> Self {
115        Self::Rustup(value)
116    }
117}