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 }