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 }