ci-cargo

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

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 }