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
This commit is contained in:
Björn Quentin 2025-06-30 12:29:46 +02:00 committed by GitHub
parent 0f73826a81
commit 0137fbb5c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 202 additions and 3 deletions

View File

@ -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"]

View File

@ -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),
}

View File

@ -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<Package>,
/// 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: "<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<Package> = 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"(?<prefix>https://img.shields.io/badge/MSRV-)(?<msrv>[0123456789.]*)(?<postfix>-)",
)?;
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<Package>,
) -> 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(())
}

View File

@ -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),