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