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 }