main.rs (17736B)
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 /// Last remove `flock rpath 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 .map_err(E::Unveil) 198 .and_then(|()| Promises::pledge_raw(c"exec proc stdio").map_err(E::Pledge)) 199 }) 200 } 201 /// Finds `file` in `cur_dir` or its ancestor directories returning `true` iff `file` exists. Searching is 202 /// done from child directories up. 203 /// 204 /// We make this recursive in the rare (impossible?) case that traversal becomes circular; in which case, 205 /// we want a stack overflow to occur. 206 fn get_path_of_file(cur_dir: &mut PathBuf, file: &Path) -> Result<bool, Error> { 207 cur_dir.push(file); 208 fs::exists(&cur_dir).and_then(|exists| { 209 // Remove `file`. 210 _ = cur_dir.pop(); 211 if exists { 212 Ok(true) 213 } else if cur_dir.pop() { 214 get_path_of_file(cur_dir, file) 215 } else { 216 Ok(false) 217 } 218 }) 219 } 220 /// Current version of this crate. 221 const VERSION: &str = concat!("ci-cargo ", env!("CARGO_PKG_VERSION")); 222 #[expect( 223 clippy::arithmetic_side_effects, 224 reason = "comment justifies correctness" 225 )] 226 #[expect( 227 clippy::verbose_file_reads, 228 reason = "false positive since we want to lock the file" 229 )] 230 fn main() -> ExitCode { 231 priv_init().and_then(|()| MetaCmd::from_args(env::args_os()).map_err(E::Args).and_then(|meta_cmd| { 232 match meta_cmd { 233 MetaCmd::Help => io::stdout().lock().write_all(HELP_MSG.as_bytes()).map_err(E::Help), 234 MetaCmd::Version => writeln!(io::stdout().lock(), "{VERSION}").map_err(E::Version), 235 MetaCmd::Cargo(cmd, mut opts) => opts.exec_dir.map_or_else( 236 || env::current_dir().map_err(E::CurDir).and_then(|mut path| { 237 let search_start = path.clone(); 238 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)) }) 239 }), 240 |path| fs::canonicalize(&path).map_err(|e| E::CanonicalizePath(e, path)), 241 ).and_then(|mut cur_dir| env::set_current_dir(&cur_dir).map_err(|e| E::SetDir(e, cur_dir.clone())).and_then(|()| { 242 cur_dir.push(cargo_toml()); 243 let mut skip_no_feats = false; 244 // `ignore_features` is unique, so we simply need to check for the first 245 // occurrence of an empty string and remove it. We do this _before_ calling 246 // `Manifest::from_toml` since the empty string is treated special in that it 247 // represents the empty set of features. It is not the name of a feature. Note 248 // `cargo` disallows an empty string to be a feature name, so there is no fear 249 // of misinterpeting it. 250 if let Err(ig_idx) = opts.ignore_features.iter().try_fold(0, |idx, feat| { 251 if feat.is_empty() { 252 Err(idx) 253 } else { 254 // Clearly can't overflow since this is the index. Caps at `isize::MAX`. 255 Ok(idx + 1) 256 } 257 }) { 258 skip_no_feats = true; 259 drop(opts.ignore_features.swap_remove(ig_idx)); 260 } 261 File::options().read(true).open(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|mut toml_file| { 262 toml_file.try_lock_shared().map_err(|e| E::CargoTomlLock(e, cur_dir.clone())).and_then(|()| { 263 toml_file.metadata().map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|meta| { 264 let meta_len = usize::try_from(meta.len()).unwrap_or(usize::MAX); 265 let mut toml_utf8 = Vec::with_capacity(meta_len); 266 toml_file.read_to_end(&mut toml_utf8).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|len| { 267 drop(toml_file); 268 if meta_len == len { 269 String::from_utf8(toml_utf8).map_err(|e| E::CargoTomlRead(Error::other(e), cur_dir.clone())).and_then(|toml| { 270 Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir, &opts.ignore_features).map_err(E::Manifest).and_then(|man| { 271 if opts.default_toolchain || (!rustup::SUPPORTED && opts.rustup_home.is_none()) { 272 Ok(Toolchain::Default(opts.ignore_msrv)) 273 } else { 274 let mut cargo_toml_path = cur_dir.clone(); 275 _ = cargo_toml_path.pop(); 276 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) }) 277 }.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()) { 278 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) 279 } else { 280 Ok(None) 281 }).and_then(|msrv_string| { 282 let default_feature_does_not_exist = !man.features().contains_default(); 283 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| { 284 let mut non_term_errs = HashSet::new(); 285 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(|()| { 286 if non_term_errs.is_empty() { 287 Ok(()) 288 } else { 289 // `StderrLock` is not buffered. 290 let mut stderr = BufWriter::new(io::stderr().lock()); 291 non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr) 292 } 293 }).and_then(|()| { 294 if opts.summary { 295 let mut stdout = io::stdout().lock(); 296 if matches!(toolchain, Toolchain::Stable) { 297 if let Some(ref msrv_val) = msrv_string { 298 writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}") 299 } else { 300 writeln!(stdout, "Toolchain used: cargo +stable") 301 } 302 } else if let Some(ref msrv_val) = msrv_string { 303 writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}") 304 } else { 305 writeln!(stdout, "Toolchain used: cargo") 306 }.and_then(|()| { 307 writeln!(stdout, "Features used:").and_then(|()| { 308 power_set.reset(); 309 while let Some(features) = power_set.next_set() { 310 writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features })?; 311 } 312 Ok(()) 313 }) 314 }).map_err(E::Summary) 315 } else { 316 Ok(()) 317 } 318 }) 319 })) 320 }))) 321 }) 322 }) 323 } else { 324 Err(E::CargoTomlLenMismatch(cur_dir)) 325 } 326 }) 327 }) 328 }) 329 }) 330 })) 331 } 332 })).map_or_else(E::into_exit_code, |()| ExitCode::SUCCESS) 333 }