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 (7466B)


      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, Write},
     91 };
     92 use toml::de::Error as TomlErr;
     93 /// Application error.
     94 enum E {
     95     /// Error related to the passed arguments.
     96     Args(ArgsErr),
     97     /// Error related to running `cargo`.
     98     Cmd(String),
     99     /// I/O-related errors.
    100     Io(io::Error),
    101     /// Error related to the format of `Cargo.toml`.
    102     Toml(TomlErr),
    103     /// Error when `stdout` is not valid UTF-8.
    104     Utf8(FromUtf8Error),
    105     /// Error when `cargo t -q --color always --doc` errors due to
    106     /// a lack of library targets. This is not an actual error as
    107     /// it is used to signal that no more invocations should occur.
    108     NoLibraryTargets,
    109 }
    110 impl Display for E {
    111     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    112         match *self {
    113             Self::Args(ref err) => err.fmt(f),
    114             Self::Cmd(ref err) => err.fmt(f),
    115             Self::Io(ref err) => err.fmt(f),
    116             Self::Toml(ref err) => err.fmt(f),
    117             Self::Utf8(ref err) => err.fmt(f),
    118             Self::NoLibraryTargets => f.write_str("no library targets"),
    119         }
    120     }
    121 }
    122 impl fmt::Debug for E {
    123     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    124         <Self as Display>::fmt(self, f)
    125     }
    126 }
    127 impl Error for E {}
    128 #[expect(
    129     clippy::panic_in_result_fn,
    130     reason = "asserts are fine when they indicate a bug"
    131 )]
    132 fn main() -> Result<(), E> {
    133     Opts::from_args().and_then(|(mut opt, path)| {
    134         path.map_or(Ok(()), |p| env::set_current_dir(p).map_err(E::Io)).and_then(|()| {
    135             fs::read_to_string("Cargo.toml")
    136                 .map_err(E::Io)
    137                 .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
    138                 .and_then(|features_powerset| {
    139                     features_powerset
    140                         .into_iter()
    141                         .try_fold(HashSet::new(), |mut msgs, features| {
    142                             opt.run_cmd(features.as_str(), &mut msgs)
    143                                 .and_then(|success| {
    144                                     if matches!(success, Success::NoLibraryTargets) {
    145                                         // We don't want to bother continuing to call `cargo t -q --doc` once we
    146                                         // know there is no library target.
    147                                         assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
    148                                         Err(E::NoLibraryTargets)
    149                                     } else {
    150                                         Ok(msgs)
    151                                     }
    152                                 })
    153                         })
    154                         .or_else(|e| {
    155                             if matches!(e, E::NoLibraryTargets) {
    156                                 Ok(HashSet::new())
    157                             } else {
    158                                 Err(e)
    159                             }
    160                         })
    161                         .and_then(|msgs| {
    162                             if msgs.is_empty() {
    163                                 Ok(())
    164                             } else {
    165                                 io::stderr()
    166                                     .lock()
    167                                     .write_all(
    168                                         msgs.into_iter()
    169                                             .fold(String::new(), |mut buffer, msg| {
    170                                                 buffer.push_str(msg.as_str());
    171                                                 buffer
    172                                             })
    173                                             .as_bytes(),
    174                                     )
    175                                     .map_err(E::Io)
    176                             }
    177                         })
    178                 })
    179         })
    180     })
    181 }