ci

CI for all possible combinations of features in Cargo.toml.
git clone https://git.philomathiclife.com/repos/ci
Log | Files | Refs | README

commit 4a84e6ec6a9e6a008977fa48c761d479285c495c
parent 0a7fb27cbcc4c2114f63423d777737d7119d6196
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri,  6 Sep 2024 18:22:34 -0600

lint reasons

Diffstat:
MCargo.toml | 6+++---
MREADME.md | 14++++++++++++++
Msrc/args.rs | 16++++++++--------
Msrc/main.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/manifest.rs | 15+++++++++------
5 files changed, 114 insertions(+), 66 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,14 +9,14 @@ license = "MIT OR Apache-2.0" name = "ci" readme = "README.md" repository = "https://git.philomathiclife.com/repos/ci/" -rust-version = "1.77.0" -version = "0.1.0" +rust-version = "1.81.0" +version = "0.1.1" [badges] maintenance = { status = "actively-developed" } [dependencies] -serde = { version = "1.0.208", default-features = false } +serde = { version = "1.0.210", default-features = false } toml = { version = "0.8.19", default-features = false, features = ["parse"] } [profile.release] diff --git a/README.md b/README.md @@ -33,3 +33,17 @@ When `clippy`, `doc_tests`, and `tests` are not passed; then all three are invok ## Limitations Any use of `compile_error` _not_ related to incompatible features will be silently ignored. + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0). +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT). + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, +as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/src/args.rs b/src/args.rs @@ -1,15 +1,16 @@ use super::E; -use core::fmt::{self, Display, Formatter}; +use core::{ + error::Error, + fmt::{self, Display, Formatter}, +}; use std::{ collections::HashSet, - env, - error::Error, - fs, + env, fs, path::PathBuf, process::{Command, Stdio}, }; /// Error returned when parsing arguments passed to the application. -#[allow(clippy::module_name_repetitions)] +#[expect(clippy::module_name_repetitions, reason = "prefer this name")] #[derive(Clone, Debug)] pub enum ArgsErr { /// Error when no arguments exist. @@ -22,7 +23,6 @@ pub enum ArgsErr { MissingPath, } impl Display for ArgsErr { - #[allow(clippy::ref_patterns)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::NoArgs => write!(f, "no arguments exist including the name of the process itself"), @@ -141,7 +141,7 @@ impl Opts { /// /// `self` is mutated iff `Self::None(_, false)` and `cargo t -q --doc` errors /// due to a lack of library target; in which case, the second `bool` becomes `true`. - #[allow(clippy::too_many_lines)] + #[expect(clippy::too_many_lines, reason = "not too many")] pub fn run_cmd( &mut self, features: &str, @@ -263,7 +263,7 @@ impl Opts { } } /// Returns `Opts` and the directory `ci` should run in based on arguments passed to the application. - #[allow(clippy::redundant_else)] + #[expect(clippy::redundant_else, reason = "when else-if is used, prefer else")] pub fn from_args() -> Result<(Self, Option<PathBuf>), E> { let mut args = env::args(); if args.next().is_none() { diff --git a/src/main.rs b/src/main.rs @@ -2,6 +2,9 @@ //! //! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with //! `clippy -q`, `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`. +//! Additionally `ci` always updates the dependencies in `Cargo.toml` to the newest stable version even if such +//! version is not [SemVer](https://semver.org/) compatible. If any dependencies are upgraded, then they will +//! be written to `stderr`. This occurs before any other command is performed. //! //! `ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables //! feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains @@ -45,6 +48,7 @@ rust_2024_compatibility, unsafe_code, unused, + unused_crate_dependencies, warnings, clippy::all, clippy::cargo, @@ -57,15 +61,17 @@ clippy::style, clippy::suspicious )] -#![allow( +#![expect( clippy::blanket_clippy_restriction_lints, clippy::implicit_return, clippy::min_ident_chars, clippy::missing_trait_methods, clippy::question_mark_used, + clippy::ref_patterns, clippy::single_call_fn, clippy::single_char_lifetime_names, - clippy::unseparated_literal_suffix + clippy::unseparated_literal_suffix, + reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" )] extern crate alloc; /// Functionality related to the passed arguments. @@ -74,13 +80,15 @@ mod args; mod manifest; use alloc::string::FromUtf8Error; use args::{ArgsErr, Opts, Success}; -use core::fmt::{self, Display, Formatter}; +use core::{ + error::Error, + fmt::{self, Display, Formatter}, +}; use std::{ collections::HashSet, - env, - error::Error, - fs, + env, fs, io::{self, Write}, + process::{Command, Stdio}, }; use toml::de::Error as TomlErr; /// Application error. @@ -101,7 +109,6 @@ enum E { NoLibraryTargets, } impl Display for E { - #[allow(clippy::ref_patterns)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::Args(ref err) => err.fmt(f), @@ -119,52 +126,76 @@ impl fmt::Debug for E { } } impl Error for E {} +#[expect( + clippy::panic_in_result_fn, + reason = "asserts are fine when they indicate a bug" +)] fn main() -> Result<(), E> { Opts::from_args().and_then(|(mut opt, path)| { path.map_or(Ok(()), |p| env::set_current_dir(p).map_err(E::Io)).and_then(|()| { - fs::read_to_string("Cargo.toml") - .map_err(E::Io) - .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml)) - .and_then(|features_powerset| { - features_powerset - .into_iter() - .try_fold(HashSet::new(), |mut msgs, features| { - opt.run_cmd(features.as_str(), &mut msgs) - .and_then(|success| { - if matches!(success, Success::NoLibraryTargets) { - // We don't want to bother continuing to call `cargo t -q --doc` once we - // know there is no library target. - assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called"); - Err(E::NoLibraryTargets) - } else { - Ok(msgs) - } - }) - }) - .or_else(|e| { - if matches!(e, E::NoLibraryTargets) { - Ok(HashSet::new()) - } else { - Err(e) - } - }) - .and_then(|msgs| { - if msgs.is_empty() { - Ok(()) - } else { - io::stderr() - .lock() - .write_all( - msgs.into_iter() - .fold(String::new(), |mut buffer, msg| { - buffer.push_str(msg.as_str()); - buffer - }) - .as_bytes(), - ) - .map_err(E::Io) - } + Command::new("cargo").stderr(Stdio::piped()).stdin(Stdio::null()).stdout(Stdio::piped()).args(["upgrade", "-i", "allow"]).output().map_err(E::Io).and_then(|output| { + if output.status.success() { + if output.stdout.is_empty() { + Ok(()) + } else { + String::from_utf8(output.stdout).map_err(E::Utf8).and_then(|out| { + io::stderr() + .lock() + .write_all(out.as_bytes()) + .map_err(E::Io) }) + } + } else { + String::from_utf8(output.stderr).map_err(E::Utf8).and_then(|mut err| { + err.push_str("\ncargo upgrade -i allow"); + Err(E::Cmd(err)) + }) + } + }).and_then(|()| { + fs::read_to_string("Cargo.toml") + .map_err(E::Io) + .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml)) + .and_then(|features_powerset| { + features_powerset + .into_iter() + .try_fold(HashSet::new(), |mut msgs, features| { + opt.run_cmd(features.as_str(), &mut msgs) + .and_then(|success| { + if matches!(success, Success::NoLibraryTargets) { + // We don't want to bother continuing to call `cargo t -q --doc` once we + // know there is no library target. + assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called"); + Err(E::NoLibraryTargets) + } else { + Ok(msgs) + } + }) + }) + .or_else(|e| { + if matches!(e, E::NoLibraryTargets) { + Ok(HashSet::new()) + } else { + Err(e) + } + }) + .and_then(|msgs| { + if msgs.is_empty() { + Ok(()) + } else { + io::stderr() + .lock() + .write_all( + msgs.into_iter() + .fold(String::new(), |mut buffer, msg| { + buffer.push_str(msg.as_str()); + buffer + }) + .as_bytes(), + ) + .map_err(E::Io) + } + }) + }) }) }) }) diff --git a/src/manifest.rs b/src/manifest.rs @@ -59,12 +59,13 @@ impl<'de> Deserialize<'de> for ImpliedFeatures { /// Features with the features that are enabled. struct Features(HashMap<String, ImpliedFeatures>); impl Features { - #[allow( + #[expect( clippy::arithmetic_side_effects, clippy::as_conversions, clippy::cast_possible_truncation, clippy::indexing_slicing, - clippy::unreachable + clippy::unreachable, + reason = "comments in code explain their correctness" )] /// Returns all possible combinations of features. fn powerset(self) -> Vec<String> { @@ -237,14 +238,14 @@ impl<'de> Deserialize<'de> for Manifest { impl<'f> Visitor<'f> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - formatter.write_str("'features'") + write!(formatter, "'{FEATURES}'") } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: Error, { Ok(match v { - "features" => Field::Features, + FEATURES => Field::Features, _ => Field::Other, }) } @@ -257,7 +258,7 @@ impl<'de> Deserialize<'de> for Manifest { match key { Field::Features => { if features.is_some() { - return Err(Error::duplicate_field("features")); + return Err(Error::duplicate_field(FEATURES)); } features = Some(map.next_value()?); } @@ -269,10 +270,12 @@ impl<'de> Deserialize<'de> for Manifest { )) } } + /// `features`. + const FEATURES: &str = "features"; deserializer.deserialize_map(ManifestVisitor) } } -/// Returns all possible combinations of features. +/// Returns possible combinations of features. pub fn from_toml(val: &str) -> Result<Vec<String>, TomlErr> { toml::from_str::<Manifest>(val).map(|man| man.0.powerset()) }