main.rs (7909B)
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 //! * `ignored`: `cargo t -q --tests -- --ignored` is invoked for each combination of features. 30 //! * `include-ignored`: `cargo t -q --tests -- --include-ignored` is invoked for each combination of features. 31 //! * `--color`: `--color always` is passed to the above commands; otherwise without this option, `--color never` is 32 //! passed. 33 //! * `--dir <path to directory Cargo.toml is in>`: `ci` changes the working directory to the passed path (after 34 //! canonicalizing it) before executing. Without this, the current directory is used. 35 //! 36 //! When `clippy`, `doc_tests`, `tests`, `ignored`, and `include-ignored` are not passed; then `clippy`, 37 //! `doc_tests`, and `tests` are invoked. 38 //! 39 //! ## Limitations 40 //! 41 //! Any use of [`compile_error`] _not_ related to incompatible features will be silently ignored. 42 extern crate alloc; 43 /// Functionality related to the passed arguments. 44 mod args; 45 /// Functionality related to `Cargo.toml` parsing. 46 mod manifest; 47 use alloc::string::FromUtf8Error; 48 use args::{ArgsErr, Opts, Success}; 49 use core::{ 50 error::Error, 51 fmt::{self, Display, Formatter}, 52 }; 53 use std::{ 54 collections::HashSet, 55 env, fs, 56 io::{self, ErrorKind, Write as _}, 57 path::PathBuf, 58 }; 59 use toml::de::Error as TomlErr; 60 /// Application error. 61 enum E { 62 /// Error related to the passed arguments. 63 Args(ArgsErr), 64 /// Error related to running `cargo`. 65 Cmd(String), 66 /// I/O-related errors. 67 Io(io::Error), 68 /// Error related to the format of `Cargo.toml`. 69 Toml(TomlErr), 70 /// Error when `stdout` is not valid UTF-8. 71 Utf8(FromUtf8Error), 72 /// Error when `cargo t -q --color always --doc` errors due to 73 /// a lack of library targets. This is not an actual error as 74 /// it is used to signal that no more invocations should occur. 75 NoLibraryTargets, 76 } 77 impl Display for E { 78 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 79 match *self { 80 Self::Args(ref err) => err.fmt(f), 81 Self::Cmd(ref err) => err.fmt(f), 82 Self::Io(ref err) => err.fmt(f), 83 Self::Toml(ref err) => err.fmt(f), 84 Self::Utf8(ref err) => err.fmt(f), 85 Self::NoLibraryTargets => f.write_str("no library targets"), 86 } 87 } 88 } 89 impl fmt::Debug for E { 90 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 91 <Self as Display>::fmt(self, f) 92 } 93 } 94 impl Error for E {} 95 #[expect( 96 clippy::panic_in_result_fn, 97 reason = "asserts are fine when they indicate a bug" 98 )] 99 fn main() -> Result<(), E> { 100 /// Checks if `Cargo.toml` exists in `cur_dir`; if not, it recursively checks the ancestor 101 /// directories. 102 /// 103 /// We make this recursive in the rare (impossible?) case that traversal becomes circular; in which case, 104 /// we want a stack overflow to occur. 105 fn set_env(mut cur_dir: PathBuf) -> Result<bool, io::Error> { 106 if fs::exists("Cargo.toml")? { 107 Ok(true) 108 } else if cur_dir.pop() { 109 env::set_current_dir(cur_dir.as_path()).and_then(|()| set_env(cur_dir)) 110 } else { 111 Ok(false) 112 } 113 } 114 Opts::from_args().and_then(|(mut opt, path)| { 115 path.map_or_else( 116 || { 117 env::current_dir().and_then(|dir| { 118 set_env(dir).and_then(|exists| { 119 if exists { 120 Ok(()) 121 } else { 122 Err(io::Error::new(ErrorKind::NotFound, "Cargo.toml does not exist in the current directory nor any of the ancestor directories")) 123 } 124 }) 125 }) 126 }, 127 env::set_current_dir 128 ).map_err(E::Io).and_then(|()| { 129 fs::read_to_string("Cargo.toml") 130 .map_err(E::Io) 131 .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml)) 132 .and_then(|features_powerset| { 133 features_powerset 134 .into_iter() 135 .try_fold(HashSet::new(), |mut msgs, features| { 136 opt.run_cmd(features.as_str(), &mut msgs) 137 .and_then(|success| { 138 if matches!(success, Success::NoLibraryTargets) { 139 // We don't want to bother continuing to call `cargo t -q --doc` once we 140 // know there is no library target. 141 assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called"); 142 Err(E::NoLibraryTargets) 143 } else { 144 Ok(msgs) 145 } 146 }) 147 }) 148 .or_else(|e| { 149 if matches!(e, E::NoLibraryTargets) { 150 Ok(HashSet::new()) 151 } else { 152 Err(e) 153 } 154 }) 155 .and_then(|msgs| { 156 if msgs.is_empty() { 157 Ok(()) 158 } else { 159 io::stderr() 160 .lock() 161 .write_all( 162 msgs.into_iter() 163 .fold(String::new(), |mut buffer, msg| { 164 buffer.push_str(msg.as_str()); 165 buffer 166 }) 167 .as_bytes(), 168 ) 169 .map_err(E::Io) 170 } 171 }) 172 }) 173 }) 174 }) 175 }