ci-cargo

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

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 }