/ src / action_impl / cargo.rs
cargo.rs
  1  //! Action that use Rust `cargo` too.
  2  
  3  use std::path::{Path, PathBuf};
  4  
  5  use serde::{Deserialize, Serialize};
  6  use tempfile::tempdir;
  7  use walkdir::WalkDir;
  8  
  9  use 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)]
 17  pub struct CargoFetch;
 18  
 19  impl 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)]
 64  pub struct CargoFmt;
 65  
 66  impl 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)]
 78  pub struct CargoClippy;
 79  
 80  impl 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)]
104  pub struct CargoDeny;
105  
106  impl 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)]
126  pub struct CargoDoc;
127  
128  impl 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)]
149  pub struct CargoBuild;
150  
151  impl 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)]
171  pub struct CargoTest;
172  
173  impl 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)]
186  pub struct CargoInstall;
187  
188  impl 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  
209  fn copy_partial_tree<P, PP, F>(src: P, dest: PP, wanted: F) -> Result<(), CargoError>
210  where
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)]
239  pub 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  
265  impl From<CargoError> for ActionError {
266      fn from(value: CargoError) -> Self {
267          Self::Cargo(value)
268      }
269  }