ci

CI for all possible combinations of features in Cargo.toml.
git clone https://git.philomathiclife.com/repos/ci
Log | Files | Refs | README

main.rs (8552B)


      1 //! # `ci`
      2 //!
      3 //! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with
      4 //! `clippy -q`, `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`.
      5 //! Additionally `ci` always updates the dependencies in `Cargo.toml` to the newest stable version even if such
      6 //! version is not [SemVer](https://semver.org/) compatible. If any dependencies are upgraded, then they will
      7 //! be written to `stderr`. This occurs before any other command is performed.
      8 //!
      9 //! `ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables
     10 //! feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains
     11 //! `foo` and `bar`, `foo` and `fizz`, or `bar` and `fizz` will be tested.
     12 //!
     13 //! `ci` writes to `stderr` iff a command errors for a reason other than [`compile_error`] or `stderr` is written
     14 //! to on success. When a non-`compile_error` occurs, `ci` is terminated after writing the offending command and
     15 //! features. Upon successful completion, `ci` writes all _unique_ messages that were collected. `stdout` is
     16 //! never written to.
     17 //!
     18 //! ## Why is this useful?
     19 //!
     20 //! The number of possible configurations grows exponentially based on the number of features in `Cargo.toml`.
     21 //! This can easily cause a crate to not be tested with certain combinations of features. Instead of manually
     22 //! invoking `cargo` with each possible combination of features, this handles it automatically.
     23 //!
     24 //! ## Options  
     25 //!   
     26 //! * `clippy`: `cargo clippy -q` is invoked for each combination of features.
     27 //! * `doc_tests`: `cargo t -q --doc` is invoked for each combination of features.
     28 //! * `tests`: `cargo t -q --tests` is invoked for each combination of features.
     29 //! * `--color`: `--color always` is passed to the above commands; otherwise without this option, `--color never` is
     30 //!   passed.
     31 //! * `--dir <path to directory Cargo.toml is in>`: `ci` changes the working directory to the passed path (after
     32 //!   canonicalizing it) before executing. Without this, the current directory is used.
     33 //!
     34 //! When `clippy`, `doc_tests`, and `tests` are not passed; then all three are invoked.
     35 //!
     36 //! ## Limitations
     37 //!
     38 //! Any use of [`compile_error`] _not_ related to incompatible features will be silently ignored.
     39 #![deny(
     40     unknown_lints,
     41     future_incompatible,
     42     let_underscore,
     43     missing_docs,
     44     nonstandard_style,
     45     rust_2018_compatibility,
     46     rust_2018_idioms,
     47     rust_2021_compatibility,
     48     rust_2024_compatibility,
     49     unsafe_code,
     50     unused,
     51     unused_crate_dependencies,
     52     warnings,
     53     clippy::all,
     54     clippy::cargo,
     55     clippy::complexity,
     56     clippy::correctness,
     57     clippy::nursery,
     58     clippy::pedantic,
     59     clippy::perf,
     60     clippy::restriction,
     61     clippy::style,
     62     clippy::suspicious
     63 )]
     64 #![expect(
     65     clippy::blanket_clippy_restriction_lints,
     66     clippy::implicit_return,
     67     clippy::min_ident_chars,
     68     clippy::missing_trait_methods,
     69     clippy::question_mark_used,
     70     clippy::ref_patterns,
     71     clippy::single_call_fn,
     72     clippy::single_char_lifetime_names,
     73     clippy::unseparated_literal_suffix,
     74     reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
     75 )]
     76 extern crate alloc;
     77 /// Functionality related to the passed arguments.
     78 mod args;
     79 /// Functionality related to `Cargo.toml` parsing.
     80 mod manifest;
     81 use alloc::string::FromUtf8Error;
     82 use args::{ArgsErr, Opts, Success};
     83 use core::{
     84     error::Error,
     85     fmt::{self, Display, Formatter},
     86 };
     87 use std::{
     88     collections::HashSet,
     89     env, fs,
     90     io::{self, ErrorKind, Write},
     91     path::PathBuf,
     92 };
     93 use toml::de::Error as TomlErr;
     94 /// Application error.
     95 enum E {
     96     /// Error related to the passed arguments.
     97     Args(ArgsErr),
     98     /// Error related to running `cargo`.
     99     Cmd(String),
    100     /// I/O-related errors.
    101     Io(io::Error),
    102     /// Error related to the format of `Cargo.toml`.
    103     Toml(TomlErr),
    104     /// Error when `stdout` is not valid UTF-8.
    105     Utf8(FromUtf8Error),
    106     /// Error when `cargo t -q --color always --doc` errors due to
    107     /// a lack of library targets. This is not an actual error as
    108     /// it is used to signal that no more invocations should occur.
    109     NoLibraryTargets,
    110 }
    111 impl Display for E {
    112     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    113         match *self {
    114             Self::Args(ref err) => err.fmt(f),
    115             Self::Cmd(ref err) => err.fmt(f),
    116             Self::Io(ref err) => err.fmt(f),
    117             Self::Toml(ref err) => err.fmt(f),
    118             Self::Utf8(ref err) => err.fmt(f),
    119             Self::NoLibraryTargets => f.write_str("no library targets"),
    120         }
    121     }
    122 }
    123 impl fmt::Debug for E {
    124     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    125         <Self as Display>::fmt(self, f)
    126     }
    127 }
    128 impl Error for E {}
    129 #[expect(
    130     clippy::panic_in_result_fn,
    131     reason = "asserts are fine when they indicate a bug"
    132 )]
    133 fn main() -> Result<(), E> {
    134     /// Checks if `Cargo.toml` exists in `cur_dir`; if not, it recursively checks the ancestor
    135     /// directories.
    136     ///
    137     /// We make this recursive in the rare (impossible?) case that traversal becomes circular; in which case,
    138     /// we want a stack overflow to occur.
    139     fn set_env(mut cur_dir: PathBuf) -> Result<bool, io::Error> {
    140         if fs::exists("Cargo.toml")? {
    141             Ok(true)
    142         } else if cur_dir.pop() {
    143             env::set_current_dir(cur_dir.as_path()).and_then(|()| set_env(cur_dir))
    144         } else {
    145             Ok(false)
    146         }
    147     }
    148     Opts::from_args().and_then(|(mut opt, path)| {
    149         path.map_or_else(
    150             || {
    151                 env::current_dir().and_then(|dir| {
    152                     set_env(dir).and_then(|exists| {
    153                         if exists {
    154                             Ok(())
    155                         } else {
    156                             Err(io::Error::new(ErrorKind::NotFound, "Cargo.toml does not exist in the current directory nor any of the ancestor directories"))
    157                         }
    158                     })
    159                 })
    160             },
    161             env::set_current_dir
    162         ).map_err(E::Io).and_then(|()| {
    163             fs::read_to_string("Cargo.toml")
    164                 .map_err(E::Io)
    165                 .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
    166                 .and_then(|features_powerset| {
    167                     features_powerset
    168                         .into_iter()
    169                         .try_fold(HashSet::new(), |mut msgs, features| {
    170                             opt.run_cmd(features.as_str(), &mut msgs)
    171                                 .and_then(|success| {
    172                                     if matches!(success, Success::NoLibraryTargets) {
    173                                         // We don't want to bother continuing to call `cargo t -q --doc` once we
    174                                         // know there is no library target.
    175                                         assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
    176                                         Err(E::NoLibraryTargets)
    177                                     } else {
    178                                         Ok(msgs)
    179                                     }
    180                                 })
    181                         })
    182                         .or_else(|e| {
    183                             if matches!(e, E::NoLibraryTargets) {
    184                                 Ok(HashSet::new())
    185                             } else {
    186                                 Err(e)
    187                             }
    188                         })
    189                         .and_then(|msgs| {
    190                             if msgs.is_empty() {
    191                                 Ok(())
    192                             } else {
    193                                 io::stderr()
    194                                     .lock()
    195                                     .write_all(
    196                                         msgs.into_iter()
    197                                             .fold(String::new(), |mut buffer, msg| {
    198                                                 buffer.push_str(msg.as_str());
    199                                                 buffer
    200                                             })
    201                                             .as_bytes(),
    202                                     )
    203                                     .map_err(E::Io)
    204                             }
    205                         })
    206                 })
    207         })
    208     })
    209 }