main.rs (8667B)
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. Any `ignored` 39 //! tests are not run. 40 #![deny( 41 unknown_lints, 42 future_incompatible, 43 let_underscore, 44 missing_docs, 45 nonstandard_style, 46 rust_2018_compatibility, 47 rust_2018_idioms, 48 rust_2021_compatibility, 49 rust_2024_compatibility, 50 unsafe_code, 51 unused, 52 unused_crate_dependencies, 53 warnings, 54 clippy::all, 55 clippy::cargo, 56 clippy::complexity, 57 clippy::correctness, 58 clippy::nursery, 59 clippy::pedantic, 60 clippy::perf, 61 clippy::restriction, 62 clippy::style, 63 clippy::suspicious 64 )] 65 #![expect( 66 clippy::blanket_clippy_restriction_lints, 67 clippy::arbitrary_source_item_ordering, 68 clippy::implicit_return, 69 clippy::min_ident_chars, 70 clippy::missing_trait_methods, 71 clippy::question_mark_used, 72 clippy::ref_patterns, 73 clippy::return_and_then, 74 clippy::single_call_fn, 75 clippy::single_char_lifetime_names, 76 clippy::unseparated_literal_suffix, 77 reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" 78 )] 79 extern crate alloc; 80 /// Functionality related to the passed arguments. 81 mod args; 82 /// Functionality related to `Cargo.toml` parsing. 83 mod manifest; 84 use alloc::string::FromUtf8Error; 85 use args::{ArgsErr, Opts, Success}; 86 use core::{ 87 error::Error, 88 fmt::{self, Display, Formatter}, 89 }; 90 use std::{ 91 collections::HashSet, 92 env, fs, 93 io::{self, ErrorKind, Write as _}, 94 path::PathBuf, 95 }; 96 use toml::de::Error as TomlErr; 97 /// Application error. 98 enum E { 99 /// Error related to the passed arguments. 100 Args(ArgsErr), 101 /// Error related to running `cargo`. 102 Cmd(String), 103 /// I/O-related errors. 104 Io(io::Error), 105 /// Error related to the format of `Cargo.toml`. 106 Toml(TomlErr), 107 /// Error when `stdout` is not valid UTF-8. 108 Utf8(FromUtf8Error), 109 /// Error when `cargo t -q --color always --doc` errors due to 110 /// a lack of library targets. This is not an actual error as 111 /// it is used to signal that no more invocations should occur. 112 NoLibraryTargets, 113 } 114 impl Display for E { 115 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 116 match *self { 117 Self::Args(ref err) => err.fmt(f), 118 Self::Cmd(ref err) => err.fmt(f), 119 Self::Io(ref err) => err.fmt(f), 120 Self::Toml(ref err) => err.fmt(f), 121 Self::Utf8(ref err) => err.fmt(f), 122 Self::NoLibraryTargets => f.write_str("no library targets"), 123 } 124 } 125 } 126 impl fmt::Debug for E { 127 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 128 <Self as Display>::fmt(self, f) 129 } 130 } 131 impl Error for E {} 132 #[expect( 133 clippy::panic_in_result_fn, 134 reason = "asserts are fine when they indicate a bug" 135 )] 136 fn main() -> Result<(), E> { 137 /// Checks if `Cargo.toml` exists in `cur_dir`; if not, it recursively checks the ancestor 138 /// directories. 139 /// 140 /// We make this recursive in the rare (impossible?) case that traversal becomes circular; in which case, 141 /// we want a stack overflow to occur. 142 fn set_env(mut cur_dir: PathBuf) -> Result<bool, io::Error> { 143 if fs::exists("Cargo.toml")? { 144 Ok(true) 145 } else if cur_dir.pop() { 146 env::set_current_dir(cur_dir.as_path()).and_then(|()| set_env(cur_dir)) 147 } else { 148 Ok(false) 149 } 150 } 151 Opts::from_args().and_then(|(mut opt, path)| { 152 path.map_or_else( 153 || { 154 env::current_dir().and_then(|dir| { 155 set_env(dir).and_then(|exists| { 156 if exists { 157 Ok(()) 158 } else { 159 Err(io::Error::new(ErrorKind::NotFound, "Cargo.toml does not exist in the current directory nor any of the ancestor directories")) 160 } 161 }) 162 }) 163 }, 164 env::set_current_dir 165 ).map_err(E::Io).and_then(|()| { 166 fs::read_to_string("Cargo.toml") 167 .map_err(E::Io) 168 .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml)) 169 .and_then(|features_powerset| { 170 features_powerset 171 .into_iter() 172 .try_fold(HashSet::new(), |mut msgs, features| { 173 opt.run_cmd(features.as_str(), &mut msgs) 174 .and_then(|success| { 175 if matches!(success, Success::NoLibraryTargets) { 176 // We don't want to bother continuing to call `cargo t -q --doc` once we 177 // know there is no library target. 178 assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called"); 179 Err(E::NoLibraryTargets) 180 } else { 181 Ok(msgs) 182 } 183 }) 184 }) 185 .or_else(|e| { 186 if matches!(e, E::NoLibraryTargets) { 187 Ok(HashSet::new()) 188 } else { 189 Err(e) 190 } 191 }) 192 .and_then(|msgs| { 193 if msgs.is_empty() { 194 Ok(()) 195 } else { 196 io::stderr() 197 .lock() 198 .write_all( 199 msgs.into_iter() 200 .fold(String::new(), |mut buffer, msg| { 201 buffer.push_str(msg.as_str()); 202 buffer 203 }) 204 .as_bytes(), 205 ) 206 .map_err(E::Io) 207 } 208 }) 209 }) 210 }) 211 }) 212 }