Skip to main content

ambient_ci/action_impl/
cargo.rs

1//! Action that use Rust `cargo` too.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6use tempfile::tempdir;
7use walkdir::WalkDir;
8
9use crate::{
10    action::{ActionError, Context},
11    action_impl::{rust_toolchain_versions, spawn, spawn_in, ActionImpl},
12    util::{copy_file, mkdir, UtilError},
13};
14
15/// Download Rust crate dependencies using `cargo fetch`.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct CargoFetch;
18
19impl ActionImpl for CargoFetch {
20    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
21        rust_toolchain_versions(context)?;
22        let tmp = tempdir().map_err(CargoError::TempDir)?;
23        let dest = tmp.path();
24        copy_partial_tree(context.source_dir(), dest, |path| {
25            if let Some(name) = path.file_name().map(|s| s.as_encoded_bytes()) {
26                name == b"Cargo.toml"
27                    || name == b"Cargo.lock"
28                    || (name.ends_with(b".rs") && name != b"build.rs")
29            } else {
30                false
31            }
32        })?;
33
34        let lockfile = dest.join("Cargo.lock");
35        let deny1 = dest.join("deny.toml");
36        let deny2 = dest.join(".cargo/deny.toml");
37        let deny = deny1.exists() || deny2.exists();
38        if lockfile.exists() {
39            spawn_in(context, &["cargo", "fetch", "--locked"], dest.to_path_buf())?;
40            if deny {
41                spawn_in(
42                    context,
43                    &["cargo", "deny", "--locked", "fetch"],
44                    dest.to_path_buf(),
45                )?;
46            }
47        } else {
48            spawn_in(context, &["cargo", "fetch"], dest.to_path_buf())?;
49            if deny {
50                spawn_in(
51                    context,
52                    &["cargo", "deny", "--locked", "fetch"],
53                    dest.to_path_buf(),
54                )?;
55            }
56        }
57
58        Ok(())
59    }
60}
61
62/// Check that Rust code is formatted in the canonical way, using `cargo fmt --check`.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct CargoFmt;
65
66impl ActionImpl for CargoFmt {
67    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
68        rust_toolchain_versions(context)?;
69        spawn(context, &["cargo", "fmt", "--check"])?;
70        Ok(())
71    }
72}
73
74/// Check that Rust code is correct and idiomatic using `cargo clippy`.
75///
76/// Warnings are treated as errors.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct CargoClippy;
79
80impl ActionImpl for CargoClippy {
81    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
82        rust_toolchain_versions(context)?;
83        spawn(
84            context,
85            &[
86                "cargo",
87                "clippy",
88                "--offline",
89                "--locked",
90                "--workspace",
91                "--all-targets",
92                "--no-deps",
93                "--",
94                "--deny",
95                "warnings",
96            ],
97        )?;
98        Ok(())
99    }
100}
101
102/// Check Rust code for denied stuff, using `cargo deny`.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct CargoDeny;
105
106impl ActionImpl for CargoDeny {
107    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
108        rust_toolchain_versions(context)?;
109        spawn(
110            context,
111            &[
112                "cargo",
113                "deny",
114                "--offline",
115                "--locked",
116                "--workspace",
117                "check",
118            ],
119        )?;
120        Ok(())
121    }
122}
123
124/// Render Rust documentation comments, using `cargo doc`.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct CargoDoc;
127
128impl ActionImpl for CargoDoc {
129    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
130        rust_toolchain_versions(context)?;
131        spawn(
132            context,
133            &[
134                "env",
135                "RUSTDOCFLAGS=-D warnings",
136                "cargo",
137                "doc",
138                "--workspace",
139            ],
140        )?;
141        Ok(())
142    }
143}
144
145/// Build a Rust project.
146///
147/// Run `cargo build` in a way that all parts of the project are built.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct CargoBuild;
150
151impl ActionImpl for CargoBuild {
152    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
153        rust_toolchain_versions(context)?;
154        spawn(
155            context,
156            &[
157                "cargo",
158                "build",
159                "--offline",
160                "--locked",
161                "--workspace",
162                "--all-targets",
163            ],
164        )?;
165        Ok(())
166    }
167}
168
169/// Run automated test suite for a Rust project.
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct CargoTest;
172
173impl ActionImpl for CargoTest {
174    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
175        rust_toolchain_versions(context)?;
176        spawn(
177            context,
178            &["cargo", "test", "--offline", "--locked", "--workspace"],
179        )?;
180        Ok(())
181    }
182}
183
184/// Install a Rust project into the artifacts directory.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct CargoInstall;
187
188impl ActionImpl for CargoInstall {
189    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
190        rust_toolchain_versions(context)?;
191        let artifacts = context.artifacts_dir().to_string_lossy().to_string();
192        spawn(
193            context,
194            &[
195                "cargo",
196                "install",
197                "--offline",
198                "--locked",
199                "--bins",
200                "--path=.",
201                "--root",
202                &artifacts,
203            ],
204        )?;
205        Ok(())
206    }
207}
208
209fn copy_partial_tree<P, PP, F>(src: P, dest: PP, wanted: F) -> Result<(), CargoError>
210where
211    P: AsRef<Path>,
212    PP: AsRef<Path>,
213    F: Fn(&Path) -> bool,
214{
215    let src = src.as_ref();
216    let dest = dest.as_ref();
217
218    mkdir(dest)?;
219    for e in WalkDir::new(src) {
220        let path = e
221            .map_err(|err| CargoError::CopyTreeWalkDir(src.into(), err))?
222            .path()
223            .to_path_buf();
224        if wanted(&path) {
225            let dest = dest.join(path.strip_prefix(src).unwrap_or(&path));
226            if let Some(parent) = dest.parent() {
227                if !parent.exists() {
228                    mkdir(parent)?;
229                }
230            }
231            copy_file(&path, &dest)?;
232        }
233    }
234    Ok(())
235}
236
237/// Errors from Cargo actions.
238#[derive(Debug, thiserror::Error)]
239pub enum CargoError {
240    /// Forwarded from `util` module.
241    #[error(transparent)]
242    Util(#[from] UtilError),
243
244    /// Can't list files.
245    #[error("failed to list contents of upload directory")]
246    WalkDir(#[source] walkdir::Error),
247
248    /// Can't find a .changes file.
249    #[error("no *.changes file built for deb project")]
250    NoChanges,
251
252    /// Found more than one .changes file.
253    #[error("more than one *.changes file built for deb project")]
254    ManyChanges,
255
256    /// Can't copy directory tree.
257    #[error("failed to list files in directory {0} when copying files")]
258    CopyTreeWalkDir(PathBuf, #[source] walkdir::Error),
259
260    /// Couldn't create a temporary directory.
261    #[error("failed to create a temporary directory")]
262    TempDir(#[source] std::io::Error),
263}
264
265impl From<CargoError> for ActionError {
266    fn from(value: CargoError) -> Self {
267        Self::Cargo(value)
268    }
269}