main.rs (17844B)
1 //! Consult [`README.md`](https://crates.io/crates/ci-cargo). 2 extern crate alloc; 3 /// Functionality related to parsing CLI arguments. 4 mod args; 5 /// Functionality related to running `cargo`. 6 mod cargo; 7 /// Functionality related to `Cargo.toml` parsing. 8 mod manifest; 9 /// Contains a `const bool` that is `true` iff `rustup` is supported by the platform. 10 mod rustup; 11 use args::{ArgsErr, HELP_MSG, MetaCmd}; 12 use cargo::{CargoErr, Options, Toolchain, ToolchainErr}; 13 #[cfg(target_os = "openbsd")] 14 use core::ffi::CStr; 15 use manifest::{Manifest, ManifestErr}; 16 #[cfg(target_os = "openbsd")] 17 use priv_sep::{Errno, Permissions, Promise, Promises}; 18 use std::{ 19 collections::HashSet, 20 env, 21 fs::{self, File, TryLockError}, 22 io::{self, BufWriter, Error, Read as _, Write as _}, 23 path::{Path, PathBuf}, 24 process::ExitCode, 25 }; 26 /// Application error. 27 enum E { 28 /// Error related to the passed arguments. 29 Args(ArgsErr), 30 /// Error getting the current directory. 31 CurDir(Error), 32 /// Error looking for `Cargo.toml`. 33 CargoTomlIo(Error, PathBuf), 34 /// Error when `Cargo.toml` could not be found. 35 /// 36 /// Note this is not returned when `--dir` is passed. 37 CargoTomlDoesNotExist(PathBuf), 38 /// Error canonicalizing `--dir`. 39 CanonicalizePath(Error, PathBuf), 40 /// Error setting the working directory. 41 SetDir(Error, PathBuf), 42 /// Error reading `Cargo.toml`. 43 CargoTomlRead(Error, PathBuf), 44 /// Error acquiring shared lock on `Cargo.toml`. 45 CargoTomlLock(TryLockError, PathBuf), 46 /// Error when `Cargo.toml` length does not match the length we read. 47 CargoTomlLenMismatch(PathBuf), 48 /// Error related to extracting the necessary data from `Cargo.toml`. 49 Manifest(Box<ManifestErr>), 50 /// Error looking for `rust-toolchain.toml`. 51 RustToolchainTomlIo(Error, PathBuf), 52 /// Error when `--ignore-msrv` was passed when using the `stable` toolchain. 53 IgnoreMsrvStable, 54 /// Error from `Msrv::compare_to_other`. 55 Toolchain(Box<ToolchainErr>), 56 /// Error from OpenBSD `pledge`. 57 #[cfg(target_os = "openbsd")] 58 Pledge(Errno), 59 /// Error from OpenBSD `unveil`. 60 #[cfg(target_os = "openbsd")] 61 Unveil(Errno), 62 /// Variant returned where there are too many features to generate the power set on. 63 TooManyFeatures(PathBuf), 64 /// Unable to write non-terminating messages to stderr. 65 StdErr, 66 /// Unable to write the help message to stdout. 67 Help(Error), 68 /// Unable to write the version message to stdout. 69 Version(Error), 70 /// `cargo` erred. 71 Cargo(Box<CargoErr>), 72 /// Unable to write the summary message to stdout. 73 Summary(Error), 74 } 75 impl E { 76 /// Writes `self` to `stderr` before returning [`ExitCode::FAILURE`]. 77 fn into_exit_code(self) -> ExitCode { 78 let mut stderr = io::stderr().lock(); 79 match self { 80 Self::Args(e) => e.write(stderr), 81 Self::CurDir(err) => { 82 writeln!( 83 stderr, 84 "There was an error getting the working directory: {err}." 85 ) 86 } 87 Self::CargoTomlIo(err, p) => { 88 writeln!(stderr, "There was an error looking for Cargo.toml in {} and its ancestor directories: {err}.", p.display()) 89 } 90 Self::CargoTomlDoesNotExist(p) => { 91 writeln!(stderr, "Cargo.toml does not exist in {} nor its ancestor directories.", p.display()) 92 } 93 Self::CanonicalizePath(err, p) => { 94 writeln!( 95 stderr, 96 "There was an error canonicalizing the path {}: {err}.", 97 p.display() 98 ) 99 } 100 Self::SetDir(err, p) => { 101 writeln!( 102 stderr, 103 "There was an error changing the working directory to {}: {err}.", p.display() 104 ) 105 } 106 Self::CargoTomlRead(err, p) => { 107 writeln!(stderr, "There was an error reading {}: {err}.", p.display()) 108 } 109 Self::CargoTomlLock(err, p) => { 110 writeln!(stderr, "There was an error acquiring a shared lock on {}: {err}.", p.display()) 111 } 112 Self::CargoTomlLenMismatch(p) => writeln!(stderr, "The number of bytes read from {} does not match the length reported from the file system.", p.display()), 113 Self::Manifest(e) => e.write(stderr), 114 Self::RustToolchainTomlIo(err, p) => { 115 writeln!( 116 stderr, 117 "There was an error looking for rust-toolchain.toml in {} and its ancestor directories: {err}.", p.display() 118 ) 119 } 120 Self::IgnoreMsrvStable => writeln!(stderr, "--ignore-msrv was passed when using the stable toolchain."), 121 Self::Toolchain(e) => e.write(stderr), 122 #[cfg(target_os = "openbsd")] 123 Self::Pledge(e) => writeln!(stderr, "pledge(2) erred: {e}."), 124 #[cfg(target_os = "openbsd")] 125 Self::Unveil(e) => writeln!(stderr, "unveil(2) erred: {e}."), 126 Self::TooManyFeatures(p) => writeln!(stderr, "There are too many features defined in {}. The max number of features allowed is the number of bits that make up a pointer.", p.display()), 127 Self::StdErr => Ok(()), 128 Self::Help(err) => writeln!( 129 stderr, 130 "There was an error writing ci-cargo help to stdout: {err}." 131 ), 132 Self::Version(err) => writeln!( 133 stderr, 134 "There was an error writing ci-cargo version to stdout: {err}." 135 ), 136 Self::Cargo(e) => e.write(stderr), 137 Self::Summary(err) => writeln!( 138 stderr, 139 "There was an error writing the summary to stdout: {err}." 140 ), 141 } 142 .map_or(ExitCode::FAILURE, |()| ExitCode::FAILURE) 143 } 144 } 145 /// No-op. 146 #[cfg(not(target_os = "openbsd"))] 147 #[expect(clippy::unnecessary_wraps, reason = "unify OpenBSD with non-OpenBSD")] 148 const fn priv_init<Never>() -> Result<(), Never> { 149 Ok(()) 150 } 151 /// `pledge(2)`s `exec flock proc rpath stdio unveil` in addition to `unveil(2)`ing the file system 152 /// for reads. 153 #[cfg(target_os = "openbsd")] 154 fn priv_init() -> Result<(), E> { 155 Promises::new([ 156 Promise::Exec, 157 Promise::Flock, 158 Promise::Proc, 159 Promise::Rpath, 160 Promise::Stdio, 161 Promise::Unveil, 162 ]) 163 .pledge() 164 .map_err(E::Pledge) 165 .and_then(|()| Permissions::READ.unveil(c"/").map_err(E::Unveil)) 166 } 167 /// `c"/"`. 168 #[cfg(target_os = "openbsd")] 169 const ROOT: &CStr = c"/"; 170 /// `"Cargo.toml"`. 171 fn cargo_toml() -> &'static Path { 172 Path::new("Cargo.toml") 173 } 174 /// `"rust-toolchain.toml"`. 175 fn rust_toolchain_toml() -> &'static Path { 176 Path::new("rust-toolchain.toml") 177 } 178 /// No-op. 179 #[cfg(not(target_os = "openbsd"))] 180 #[expect(clippy::unnecessary_wraps, reason = "unify OpenBSD with non-OpenBSD")] 181 const fn priv_sep_final<Never>(_: &Path) -> Result<(), Never> { 182 Ok(()) 183 } 184 /// Removes read permissions to entire file system before allowing execute permissions to `cargo_path` or `ROOT`. 185 /// Adds read permissions for `"/dev/null"`. Last remove `flock unveil` from `pledge(2)`. 186 #[cfg(target_os = "openbsd")] 187 fn priv_sep_final(cargo_path: &Path) -> Result<(), E> { 188 Permissions::NONE 189 .unveil(ROOT) 190 .map_err(E::Unveil) 191 .and_then(|()| { 192 if cargo_path.is_absolute() { 193 Permissions::EXECUTE.unveil(cargo_path) 194 } else { 195 Permissions::EXECUTE.unveil(ROOT) 196 } 197 .and_then(|()| Permissions::READ.unveil(c"/dev/null")) 198 .map_err(E::Unveil) 199 .and_then(|()| Promises::pledge_raw(c"exec proc rpath stdio").map_err(E::Pledge)) 200 }) 201 } 202 /// Finds `file` in `cur_dir` or its ancestor directories returning `true` iff `file` exists. Searching is 203 /// done from child directories up. 204 /// 205 /// We make this recursive in the rare (impossible?) case that traversal becomes circular; in which case, 206 /// we want a stack overflow to occur. 207 fn get_path_of_file(cur_dir: &mut PathBuf, file: &Path) -> Result<bool, Error> { 208 cur_dir.push(file); 209 fs::exists(&cur_dir).and_then(|exists| { 210 // Remove `file`. 211 _ = cur_dir.pop(); 212 if exists { 213 Ok(true) 214 } else if cur_dir.pop() { 215 get_path_of_file(cur_dir, file) 216 } else { 217 Ok(false) 218 } 219 }) 220 } 221 /// Current version of this crate. 222 const VERSION: &str = concat!("ci-cargo ", env!("CARGO_PKG_VERSION")); 223 #[expect( 224 clippy::arithmetic_side_effects, 225 reason = "comment justifies correctness" 226 )] 227 #[expect( 228 clippy::verbose_file_reads, 229 reason = "false positive since we want to lock the file" 230 )] 231 fn main() -> ExitCode { 232 priv_init().and_then(|()| MetaCmd::from_args(env::args_os()).map_err(E::Args).and_then(|meta_cmd| { 233 match meta_cmd { 234 MetaCmd::Help => io::stdout().lock().write_all(HELP_MSG.as_bytes()).map_err(E::Help), 235 MetaCmd::Version => writeln!(io::stdout().lock(), "{VERSION}").map_err(E::Version), 236 MetaCmd::Cargo(cmd, mut opts) => opts.exec_dir.map_or_else( 237 || env::current_dir().map_err(E::CurDir).and_then(|mut path| { 238 let search_start = path.clone(); 239 get_path_of_file(&mut path, cargo_toml()).map_err(|e| E::CargoTomlIo(e, search_start.clone())).and_then(|exists| if exists { Ok(path) } else { Err(E::CargoTomlDoesNotExist(search_start)) }) 240 }), 241 |path| fs::canonicalize(&path).map_err(|e| E::CanonicalizePath(e, path)), 242 ).and_then(|mut cur_dir| env::set_current_dir(&cur_dir).map_err(|e| E::SetDir(e, cur_dir.clone())).and_then(|()| { 243 cur_dir.push(cargo_toml()); 244 let mut skip_no_feats = false; 245 // `ignore_features` is unique, so we simply need to check for the first 246 // occurrence of an empty string and remove it. We do this _before_ calling 247 // `Manifest::from_toml` since the empty string is treated special in that it 248 // represents the empty set of features. It is not the name of a feature. Note 249 // `cargo` disallows an empty string to be a feature name, so there is no fear 250 // of misinterpeting it. 251 if let Err(ig_idx) = opts.ignore_features.iter().try_fold(0, |idx, feat| { 252 if feat.is_empty() { 253 Err(idx) 254 } else { 255 // Clearly can't overflow since this is the index. Caps at `isize::MAX`. 256 Ok(idx + 1) 257 } 258 }) { 259 skip_no_feats = true; 260 drop(opts.ignore_features.swap_remove(ig_idx)); 261 } 262 File::options().read(true).open(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|mut toml_file| { 263 toml_file.try_lock_shared().map_err(|e| E::CargoTomlLock(e, cur_dir.clone())).and_then(|()| { 264 toml_file.metadata().map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|meta| { 265 let meta_len = usize::try_from(meta.len()).unwrap_or(usize::MAX); 266 let mut toml_utf8 = Vec::with_capacity(meta_len); 267 toml_file.read_to_end(&mut toml_utf8).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|len| { 268 drop(toml_file); 269 if meta_len == len { 270 String::from_utf8(toml_utf8).map_err(|e| E::CargoTomlRead(Error::other(e), cur_dir.clone())).and_then(|toml| { 271 Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir, &opts.ignore_features).map_err(E::Manifest).and_then(|man| { 272 if opts.default_toolchain || (!rustup::SUPPORTED && opts.rustup_home.is_none()) { 273 Ok(Toolchain::Default(opts.ignore_msrv)) 274 } else { 275 let mut cargo_toml_path = cur_dir.clone(); 276 _ = cargo_toml_path.pop(); 277 get_path_of_file(&mut cargo_toml_path, rust_toolchain_toml()).map_err(|e| E::RustToolchainTomlIo(e, cargo_toml_path)).and_then(|rust_toolchain_exists| if rust_toolchain_exists { Ok(Toolchain::Default(opts.ignore_msrv)) } else if opts.ignore_msrv { Err(E::IgnoreMsrvStable) } else { Ok(Toolchain::Stable) }) 278 }.and_then(|toolchain| priv_sep_final(&opts.cargo_path).and_then(|()| man.package().msrv().map_or(Ok(None), |msrv| if !opts.skip_msrv && (rustup::SUPPORTED || opts.rustup_home.is_some()) { 279 msrv.compare_to_other(matches!(toolchain, Toolchain::Default(_)), opts.rustup_home.as_deref(), &opts.cargo_path, opts.cargo_home.as_deref()).map_err(E::Toolchain) 280 } else { 281 Ok(None) 282 }).and_then(|msrv_string| { 283 let default_feature_does_not_exist = !man.features().contains_default(); 284 man.features().power_set(skip_no_feats).map_err(|_e| E::TooManyFeatures(cur_dir)).and_then(|power_set_opt| power_set_opt.map_or_else(|| Ok(()), |mut power_set| { 285 let mut non_term_errs = HashSet::new(); 286 cmd.run(Options { toolchain, rustup_home: opts.rustup_home, cargo_path: opts.cargo_path, cargo_home: opts.cargo_home, package_name: man.package().name(), color: opts.color, ignore_compile_errors: opts.ignore_compile_errors, default_feature_does_not_exist, non_terminating_errors: &mut non_term_errs, }, msrv_string.as_deref(), &mut power_set, opts.progress).map_err(E::Cargo).and_then(|()| { 287 if non_term_errs.is_empty() { 288 Ok(()) 289 } else { 290 // `StderrLock` is not buffered. 291 let mut stderr = BufWriter::new(io::stderr().lock()); 292 non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr) 293 } 294 }).and_then(|()| { 295 if opts.summary { 296 let mut stdout = io::stdout().lock(); 297 if matches!(toolchain, Toolchain::Stable) { 298 if let Some(ref msrv_val) = msrv_string { 299 writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}") 300 } else { 301 writeln!(stdout, "Toolchain used: cargo +stable") 302 } 303 } else if let Some(ref msrv_val) = msrv_string { 304 writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}") 305 } else { 306 writeln!(stdout, "Toolchain used: cargo") 307 }.and_then(|()| { 308 writeln!(stdout, "Features used:").and_then(|()| { 309 power_set.reset(); 310 while let Some(features) = power_set.next_set() { 311 writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features })?; 312 } 313 Ok(()) 314 }) 315 }).map_err(E::Summary) 316 } else { 317 Ok(()) 318 } 319 }) 320 })) 321 }))) 322 }) 323 }) 324 } else { 325 Err(E::CargoTomlLenMismatch(cur_dir)) 326 } 327 }) 328 }) 329 }) 330 }) 331 })) 332 } 333 })).map_or_else(E::into_exit_code, |()| ExitCode::SUCCESS) 334 }