From 0137fbb5c4273b72570bc6f3897fe4c16374660f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Quentin?= Date: Mon, 30 Jun 2025 12:29:46 +0200 Subject: [PATCH] xtask: msrv-bump (#3708) * Add bump-msrv command * Make regex dependency optional * Adjustments * Make bump-msrv a release sub-command * Don't assume to bump MSRV for all packages always * Check Check * Re-arrange code * Remove controversial CLI-switch --- xtask/Cargo.toml | 3 +- xtask/src/commands/release.rs | 10 +- xtask/src/commands/release/bump_msrv.rs | 190 ++++++++++++++++++++++++ xtask/src/main.rs | 2 + 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 xtask/src/commands/release/bump_msrv.rs diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c60db7a10..1f54f4e0f 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -15,6 +15,7 @@ kuchikiki = { version = "0.8.2", optional = true } log = "0.4.22" minijinja = { version = "2.5.0", default-features = false } opener = { version = "0.7.2", optional = true } +regex = { version = "1.11.1", optional = true } rocket = { version = "0.5.1", optional = true } semver = { version = "1.0.23", features = ["serde"] } serde = { version = "1.0.215", default-features = false, features = ["derive"] } @@ -47,4 +48,4 @@ pretty_assertions = "1.2.0" deploy-docs = ["dep:reqwest", "dep:kuchikiki"] preview-docs = ["dep:opener", "dep:rocket"] semver-checks = [ "dep:cargo-semver-checks", "dep:rustdoc-types", "dep:flate2", "dep:temp-file" ] -release = ["semver-checks", "dep:opener", "dep:urlencoding"] +release = ["semver-checks", "dep:opener", "dep:urlencoding", "dep:regex"] diff --git a/xtask/src/commands/release.rs b/xtask/src/commands/release.rs index 72e8d8afa..1ba91071e 100644 --- a/xtask/src/commands/release.rs +++ b/xtask/src/commands/release.rs @@ -1,5 +1,7 @@ use clap::Subcommand; +#[cfg(feature = "release")] +pub mod bump_msrv; pub mod bump_version; #[cfg(feature = "release")] pub mod execute_plan; @@ -18,13 +20,13 @@ pub use bump_version::*; pub use execute_plan::*; #[cfg(feature = "release")] pub use plan::*; +#[cfg(feature = "release")] +pub use post_release::*; pub use publish::*; #[cfg(feature = "release")] pub use publish_plan::*; pub use semver_check::*; pub use tag_releases::*; -#[cfg(feature = "release")] -pub use post_release::*; pub const PLACEHOLDER: &str = "{{currentVersion}}"; @@ -73,4 +75,8 @@ pub enum Release { Publish(PublishArgs), /// Generate git tags for all new package releases. TagReleases(TagReleasesArgs), + /// Update the MSRV (Badges in README.md, "rust-version" in Cargo.toml, the + /// toolchain used in CI) + #[cfg(feature = "release")] + BumpMsrv(bump_msrv::BumpMsrvArgs), } diff --git a/xtask/src/commands/release/bump_msrv.rs b/xtask/src/commands/release/bump_msrv.rs new file mode 100644 index 000000000..2a4f823aa --- /dev/null +++ b/xtask/src/commands/release/bump_msrv.rs @@ -0,0 +1,190 @@ +use std::path::Path; + +use anyhow::{Result, bail}; +use clap::Args; +use regex::{Captures, Regex}; +use strum::IntoEnumIterator; +use toml_edit::value; + +use crate::{Package, cargo::CargoToml}; + +#[derive(Debug, Args)] +pub struct BumpMsrvArgs { + /// The MSRV to be used + #[arg(long)] + pub msrv: String, + + /// Package(s) to target. + #[arg(value_enum, default_values_t = Package::iter())] + pub packages: Vec, + + /// Don't actually change any files + #[arg(long)] + pub dry_run: bool, +} + +/// Bump the MSRV +/// +/// This will process +/// - `Cargo.toml` for the packages (adjust (or add if not present) the +/// "rust-version") +/// - `README.md` for the packages if it exists (adjusts the MSRV badge) +/// - IF the esp-hal package was touched: .github/workflows/ci.yml (adapts the +/// `MSRV: ""` entry) +/// +/// Non-published packages are not touched. +/// +/// If it detects a package which other packages in the repo depend on it will +/// also apply the changes there. (Can be disabled) +pub fn bump_msrv(workspace: &Path, args: BumpMsrvArgs) -> Result<()> { + let new_msrv = semver::Version::parse(&args.msrv)?; + if !new_msrv.pre.is_empty() || !new_msrv.build.is_empty() { + bail!("Invalid MSRV: {}", args.msrv); + } + + let mut to_process = args.packages.clone(); + + // add crates which depend on any of the packages to bump + add_dependent_crates(workspace, &mut to_process)?; + + // don't process crates which are not published + let to_process: Vec = to_process + .iter() + .filter(|pkg| { + let cargo_toml = CargoToml::new(workspace, **pkg).unwrap(); + cargo_toml.is_published() + }) + .copied() + .collect(); + + let adjust_ci = to_process.contains(&Package::EspHal); + + // process packages + let badge_re = Regex::new( + r"(?https://img.shields.io/badge/MSRV-)(?[0123456789.]*)(?-)", + )?; + for package in to_process { + println!("Processing {package}"); + let mut cargo_toml = CargoToml::new(workspace, package)?; + let package_path = cargo_toml.package_path(); + + let package_table = cargo_toml + .manifest + .as_table_mut() + .get_mut("package") + .and_then(|pkg| pkg.as_table_mut()); + + if let Some(package_table) = package_table { + let mut previous_rust_version = None; + if let Some(rust_version) = package_table.get_mut("rust-version") { + let rust_version = rust_version.as_str().unwrap(); + if semver::Version::parse(&rust_version)? > new_msrv { + bail!("Downgrading rust-version is not supported"); + } + previous_rust_version = Some(rust_version.to_string()) + } + + package_table["rust-version"] = value(&new_msrv.to_string()); + if !args.dry_run { + cargo_toml.save()?; + } + + let readme_path = package_path.join("README.md"); + if readme_path.exists() { + let readme = std::fs::read_to_string(&readme_path)?; + let readme = badge_re.replace(&readme, |caps: &Captures| { + format!("{}{new_msrv}{}", &caps["prefix"], &caps["postfix"]) + }); + + if !args.dry_run { + std::fs::write(readme_path, readme.as_bytes())?; + } + } + + if !args.dry_run { + if let Some(previous_rust_version) = previous_rust_version { + check_mentions(&package_path, &previous_rust_version)?; + } + } + } + } + + if adjust_ci { + // process ".github/workflows/ci.yml" + println!("Processing .github/workflows/ci.yml"); + let ci_yml_path = workspace.join(".github/workflows/ci.yml"); + + let ci_yml = std::fs::read_to_string(&ci_yml_path)?; + let ci_yml = Regex::new("(MSRV:.*\\\")([0123456789.]*)(\\\")")? + .replace(&ci_yml, |caps: &Captures| { + format!("{}{new_msrv}{}", &caps[1], &caps[3]) + }); + if !args.dry_run { + std::fs::write(ci_yml_path, ci_yml.as_bytes())?; + } + } + + println!("\nPlease review the changes before committing."); + Ok(()) +} + +/// Add all crates in the repo which depend on the given packages +fn add_dependent_crates( + workspace: &Path, + pkgs_to_process: &mut Vec, +) -> Result<(), anyhow::Error> { + Ok( + while { + let mut added = false; + + // iterate over ALL known crates + for package in Package::iter() { + let mut cargo_toml = CargoToml::new(workspace, package.clone())?; + + // iterate the dependencies in the repo + for dep in cargo_toml.repo_dependencies() { + let dependency_should_be_processed = pkgs_to_process.contains(&dep); + let current_package_already_contained = pkgs_to_process.contains(&package); + if dependency_should_be_processed && !current_package_already_contained { + added = true; + pkgs_to_process.push(package); + } + } + } + + // break once we haven't added any more crates the to be processed list + added + } {}, + ) +} + +/// Check files in the package and show if we find the version string in any +/// file. Most probably it will report false positives but maybe not. +fn check_mentions(package_path: &std::path::PathBuf, previous_rust_version: &str) -> Result<()> { + for entry in walkdir::WalkDir::new(package_path) + .into_iter() + .filter_map(|entry| { + let path = entry.unwrap().into_path(); + + if !path.is_file() { + return None; + } + + if path.components().any(|c| c.as_os_str() == "target") { + return None; + } + + Some(path) + }) + { + let contents = std::fs::read_to_string(&entry)?; + if contents.contains(previous_rust_version) { + println!( + "⚠️ '{previous_rust_version}' found in file {} - might be a false positive, otherwise consider adjusting the xtask.", + entry.display() + ); + } + } + + Ok(()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d876c6a5d..d4a29b818 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -145,6 +145,8 @@ fn main() -> Result<()> { Release::PublishPlan(args) => publish_plan(&workspace, args), #[cfg(feature = "release")] Release::PostRelease => post_release(&workspace), + #[cfg(feature = "release")] + Release::BumpMsrv(args) => bump_msrv::bump_msrv(&workspace, args), }, Cli::Ci(args) => run_ci_checks(&workspace, args),