ci-cargo

CI for Rust code.
git clone https://git.philomathiclife.com/repos/ci-cargo
Log | Files | Refs | README

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 }