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