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


      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     process::{Command, Stdio},
     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     Opts::from_args().and_then(|(mut opt, path)| {
    135         path.map_or(Ok(()), |p| env::set_current_dir(p).map_err(E::Io)).and_then(|()| {
    136             Command::new("cargo").stderr(Stdio::piped()).stdin(Stdio::null()).stdout(Stdio::piped()).args(["upgrade", "-i", "allow"]).output().map_err(E::Io).and_then(|output| {
    137                 if output.status.success() {
    138                     if output.stdout.is_empty() {
    139                         Ok(())
    140                     } else {
    141                         String::from_utf8(output.stdout).map_err(E::Utf8).and_then(|out| {
    142                             io::stderr()
    143                                 .lock()
    144                                 .write_all(out.as_bytes())
    145                                 .map_err(E::Io)
    146                         })
    147                     }
    148                 } else {
    149                     String::from_utf8(output.stderr).map_err(E::Utf8).and_then(|mut err| {
    150                         err.push_str("\ncargo upgrade -i allow");
    151                         Err(E::Cmd(err))
    152                     })
    153                 }
    154             }).and_then(|()| {
    155                 fs::read_to_string("Cargo.toml")
    156                     .map_err(E::Io)
    157                     .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
    158                     .and_then(|features_powerset| {
    159                         features_powerset
    160                             .into_iter()
    161                             .try_fold(HashSet::new(), |mut msgs, features| {
    162                                 opt.run_cmd(features.as_str(), &mut msgs)
    163                                     .and_then(|success| {
    164                                         if matches!(success, Success::NoLibraryTargets) {
    165                                             // We don't want to bother continuing to call `cargo t -q --doc` once we
    166                                             // know there is no library target.
    167                                             assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
    168                                             Err(E::NoLibraryTargets)
    169                                         } else {
    170                                             Ok(msgs)
    171                                         }
    172                                     })
    173                             })
    174                             .or_else(|e| {
    175                                 if matches!(e, E::NoLibraryTargets) {
    176                                     Ok(HashSet::new())
    177                                 } else {
    178                                     Err(e)
    179                                 }
    180                             })
    181                             .and_then(|msgs| {
    182                                 if msgs.is_empty() {
    183                                     Ok(())
    184                                 } else {
    185                                     io::stderr()
    186                                         .lock()
    187                                         .write_all(
    188                                             msgs.into_iter()
    189                                                 .fold(String::new(), |mut buffer, msg| {
    190                                                     buffer.push_str(msg.as_str());
    191                                                     buffer
    192                                                 })
    193                                                 .as_bytes(),
    194                                         )
    195                                         .map_err(E::Io)
    196                                 }
    197                             })
    198                     })
    199                 })
    200             })
    201     })
    202 }