From 6a347f1f09b0076af868dcd63d9139081c92172b Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Thu, 14 Aug 2025 13:36:39 +0200 Subject: feat: add semver checks and releasing to releaser * List dependencies of a crate * List dependents of a crate * Perform semver-checks of a crate * Prepare a release for a crate and all dependents * Use a single release.toml for cargo-release * Add changelogs where missing --- cyw43-pio/release.toml | 5 - cyw43/release.toml | 5 - embassy-boot/CHANGELOG.md | 11 + embassy-embedded-hal/release.toml | 5 - embassy-executor/release.toml | 5 - embassy-futures/release.toml | 5 - embassy-net-adin1110/CHANGELOG.md | 11 + embassy-net-driver-channel/release.toml | 5 - embassy-net-driver/CHANGELOG.md | 3 + embassy-net-esp-hosted/CHANGELOG.md | 11 + embassy-net-nrf91/CHANGELOG.md | 13 + embassy-net-nrf91/Cargo.toml | 4 +- embassy-net-ppp/CHANGELOG.md | 3 + embassy-net-wiznet/CHANGELOG.md | 13 + embassy-net/CHANGELOG.md | 3 +- embassy-nrf/release.toml | 5 - embassy-release/Cargo.toml | 12 - embassy-release/src/main.rs | 303 ------------------- embassy-rp/release.toml | 5 - embassy-stm32/release.toml | 5 - embassy-sync/CHANGELOG.md | 3 +- embassy-time-driver/release.toml | 5 - embassy-time-queue-utils/CHANGELOG.md | 3 +- embassy-time/release.toml | 5 - embassy-usb-dfu/CHANGELOG.md | 11 + embassy-usb-driver/release.toml | 5 - embassy-usb-logger/CHANGELOG.md | 3 +- embassy-usb-synopsys-otg/CHANGELOG.md | 3 +- embassy-usb/release.toml | 5 - release/Cargo.toml | 15 + release/config.toml | 45 +++ release/release.toml | 5 + release/src/main.rs | 519 ++++++++++++++++++++++++++++++++ 33 files changed, 672 insertions(+), 387 deletions(-) delete mode 100644 cyw43-pio/release.toml delete mode 100644 cyw43/release.toml create mode 100644 embassy-boot/CHANGELOG.md delete mode 100644 embassy-embedded-hal/release.toml delete mode 100644 embassy-executor/release.toml delete mode 100644 embassy-futures/release.toml create mode 100644 embassy-net-adin1110/CHANGELOG.md delete mode 100644 embassy-net-driver-channel/release.toml create mode 100644 embassy-net-esp-hosted/CHANGELOG.md create mode 100644 embassy-net-nrf91/CHANGELOG.md create mode 100644 embassy-net-wiznet/CHANGELOG.md delete mode 100644 embassy-nrf/release.toml delete mode 100644 embassy-release/Cargo.toml delete mode 100644 embassy-release/src/main.rs delete mode 100644 embassy-rp/release.toml delete mode 100644 embassy-stm32/release.toml delete mode 100644 embassy-time-driver/release.toml delete mode 100644 embassy-time/release.toml create mode 100644 embassy-usb-dfu/CHANGELOG.md delete mode 100644 embassy-usb-driver/release.toml delete mode 100644 embassy-usb/release.toml create mode 100644 release/Cargo.toml create mode 100644 release/release.toml create mode 100644 release/src/main.rs diff --git a/cyw43-pio/release.toml b/cyw43-pio/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/cyw43-pio/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/cyw43/release.toml b/cyw43/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/cyw43/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-boot/CHANGELOG.md b/embassy-boot/CHANGELOG.md new file mode 100644 index 000000000..7042ad14c --- /dev/null +++ b/embassy-boot/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +- First release with changelog. diff --git a/embassy-embedded-hal/release.toml b/embassy-embedded-hal/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-embedded-hal/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-executor/release.toml b/embassy-executor/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-executor/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-futures/release.toml b/embassy-futures/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-futures/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-net-adin1110/CHANGELOG.md b/embassy-net-adin1110/CHANGELOG.md new file mode 100644 index 000000000..7042ad14c --- /dev/null +++ b/embassy-net-adin1110/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +- First release with changelog. diff --git a/embassy-net-driver-channel/release.toml b/embassy-net-driver-channel/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-net-driver-channel/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-net-driver/CHANGELOG.md b/embassy-net-driver/CHANGELOG.md index 165461eff..0c7c27d6e 100644 --- a/embassy-net-driver/CHANGELOG.md +++ b/embassy-net-driver/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased - ReleaseDate + ## 0.2.0 - 2023-10-18 - Added support for IEEE 802.15.4 mediums. diff --git a/embassy-net-esp-hosted/CHANGELOG.md b/embassy-net-esp-hosted/CHANGELOG.md new file mode 100644 index 000000000..7042ad14c --- /dev/null +++ b/embassy-net-esp-hosted/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +- First release with changelog. diff --git a/embassy-net-nrf91/CHANGELOG.md b/embassy-net-nrf91/CHANGELOG.md new file mode 100644 index 000000000..52cbf5ef3 --- /dev/null +++ b/embassy-net-nrf91/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +## 0.1.1 - 2025-08-14 + +- First release with changelog. diff --git a/embassy-net-nrf91/Cargo.toml b/embassy-net-nrf91/Cargo.toml index 9201dc84c..387627491 100644 --- a/embassy-net-nrf91/Cargo.toml +++ b/embassy-net-nrf91/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "embassy-net-nrf91" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "embassy-net driver for Nordic nRF91-series cellular modems" keywords = ["embedded", "nrf91", "embassy-net", "cellular"] @@ -36,4 +36,4 @@ target = "thumbv7em-none-eabi" features = ["defmt", "nrf-pac/nrf9160"] [package.metadata.docs.rs] -features = ["defmt"] +features = ["defmt", "nrf-pac/nrf9160"] diff --git a/embassy-net-ppp/CHANGELOG.md b/embassy-net-ppp/CHANGELOG.md index 6ce64ddcb..b364608d2 100644 --- a/embassy-net-ppp/CHANGELOG.md +++ b/embassy-net-ppp/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased - ReleaseDate + ## 0.2.0 - 2025-01-12 - Update `ppproto` to v0.2. diff --git a/embassy-net-wiznet/CHANGELOG.md b/embassy-net-wiznet/CHANGELOG.md new file mode 100644 index 000000000..52cbf5ef3 --- /dev/null +++ b/embassy-net-wiznet/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +## 0.1.1 - 2025-08-14 + +- First release with changelog. diff --git a/embassy-net/CHANGELOG.md b/embassy-net/CHANGELOG.md index 8773772ce..39bc6c0f0 100644 --- a/embassy-net/CHANGELOG.md +++ b/embassy-net/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased + +## Unreleased - ReleaseDate No unreleased changes yet... Quick, go send a PR! diff --git a/embassy-nrf/release.toml b/embassy-nrf/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-nrf/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-release/Cargo.toml b/embassy-release/Cargo.toml deleted file mode 100644 index de548e650..000000000 --- a/embassy-release/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "embassy-release" -version = "0.1.0" -edition = "2021" - -[dependencies] -clap = { version = "4.5.1", features = ["derive"] } -walkdir = "2.5.0" -toml = "0.8.8" -toml_edit = { version = "0.23.1", features = ["serde"] } -serde = { version = "1.0.198", features = ["derive"] } -regex = "1.10.4" diff --git a/embassy-release/src/main.rs b/embassy-release/src/main.rs deleted file mode 100644 index 321d3872c..000000000 --- a/embassy-release/src/main.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command as ProcessCommand; - -use clap::{Parser, Subcommand, ValueEnum}; -use regex::Regex; -use serde::Deserialize; -use toml_edit::{DocumentMut, Item, Value}; -use walkdir::WalkDir; - -/// Tool to traverse and operate on intra-repo Rust crate dependencies -#[derive(Parser, Debug)] -#[command(author, version, about)] -struct Args { - /// Path to the root crate - #[arg(value_name = "CRATE_PATH")] - crate_path: PathBuf, - - /// Command to perform on each crate - #[command(subcommand)] - command: Command, - - /// Traversal order - #[arg(short, long, default_value = "post")] - order: TraversalOrder, -} - -#[derive(Debug, Clone, ValueEnum, PartialEq)] -enum TraversalOrder { - Pre, - Post, -} - -#[derive(Debug, Subcommand)] -enum Command { - /// Print all dependencies - Dependencies, - - /// Release crate - Release { - #[command(subcommand)] - kind: ReleaseKind, - }, -} - -#[derive(Debug, Subcommand, Clone, Copy, PartialEq)] -enum ReleaseKind { - Patch, - Minor, -} - -#[derive(Debug, Deserialize)] -struct CargoToml { - package: Option, - dependencies: Option, -} - -#[derive(Debug, Deserialize)] -struct Package { - name: String, - version: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum Dep { - Version(String), - DetailedTable(HashMap), -} - -type Deps = std::collections::HashMap; - -#[derive(Debug, Deserialize)] -struct CrateConfig { - features: Option>, - target: Option, -} - -type ReleaseConfig = HashMap; - -fn find_path_deps(cargo_path: &Path) -> Vec { - let content = fs::read_to_string(cargo_path).unwrap_or_else(|_| { - panic!("Failed to read {:?}", cargo_path); - }); - let parsed: CargoToml = toml::from_str(&content).unwrap_or_else(|e| { - panic!("Failed to parse {:?}: {}", cargo_path, e); - }); - - let mut paths = vec![]; - if let Some(deps) = parsed.dependencies { - for (_name, dep) in deps { - match dep { - Dep::Version(_) => { - // External dependency — skip - } - Dep::DetailedTable(table) => { - if let Some(toml::Value::String(path)) = table.get("path") { - let dep_path = cargo_path.parent().unwrap().join(path).canonicalize().unwrap(); - paths.push(dep_path); - } - } - } - } - } - - paths -} - -fn visit_recursive( - root_crate: &Path, - visited: &mut HashSet, - output: &mut Vec, - order: &TraversalOrder, -) { - if !visited.insert(root_crate.to_path_buf()) { - return; - } - - let cargo_toml = root_crate.join("Cargo.toml"); - let deps = find_path_deps(&cargo_toml); - - if *order == TraversalOrder::Pre { - output.push(root_crate.to_path_buf()); - } - - let mut deps_sorted = deps; - deps_sorted.sort(); - for dep in deps_sorted { - visit_recursive(&dep, visited, output, order); - } - - if *order == TraversalOrder::Post { - output.push(root_crate.to_path_buf()); - } -} - -fn get_crate_metadata(crate_path: &Path) -> Option<(String, String)> { - let cargo_toml = crate_path.join("Cargo.toml"); - let content = fs::read_to_string(&cargo_toml).ok()?; - let parsed: CargoToml = toml::from_str(&content).ok()?; - let pkg = parsed.package?; - let name = pkg.name; - let version = pkg.version?; - Some((name, version)) -} - -fn load_release_config() -> ReleaseConfig { - let config_path = PathBuf::from("release/config.toml"); - if !config_path.exists() { - return HashMap::new(); - } - let content = fs::read_to_string(&config_path).expect("Failed to read release/config.toml"); - toml::from_str(&content).expect("Invalid TOML format in release/config.toml") -} - -fn bump_dependency_versions(crate_name: &str, new_version: &str) -> Result<(), String> { - let mut cargo_files: Vec = WalkDir::new(".") - .into_iter() - .filter_map(Result::ok) - .filter(|e| e.file_name() == "Cargo.toml") - .map(|e| e.into_path()) - .collect(); - - cargo_files.sort(); - - for path in cargo_files { - let content = fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - - let mut doc: DocumentMut = content - .parse() - .map_err(|e| format!("Failed to parse TOML in {}: {}", path.display(), e))?; - - let mut changed = false; - - for section in ["dependencies", "dev-dependencies", "build-dependencies"] { - if let Some(Item::Table(dep_table)) = doc.get_mut(section) { - if let Some(item) = dep_table.get_mut(crate_name) { - match item { - // e.g., foo = "0.1.0" - Item::Value(Value::String(_)) => { - *item = Item::Value(Value::from(new_version)); - changed = true; - } - // e.g., foo = { version = "...", ... } - Item::Value(Value::InlineTable(inline)) => { - if inline.contains_key("version") { - inline["version"] = Value::from(new_version); - changed = true; - } - } - _ => {} // Leave unusual formats untouched - } - } - } - } - - if changed { - fs::write(&path, doc.to_string()).map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; - println!("🔧 Updated {} to {} in {}", crate_name, new_version, path.display()); - } - } - - Ok(()) -} - -fn run_release_command( - crate_path: &Path, - crate_name: &str, - version: &str, - kind: &ReleaseKind, - config: Option<&CrateConfig>, -) -> Result<(), String> { - let kind_str = match kind { - ReleaseKind::Patch => "patch", - ReleaseKind::Minor => "minor", - }; - - if *kind == ReleaseKind::Minor { - bump_dependency_versions(crate_name, version)?; - } - - let mut args: Vec = vec!["release".into(), kind_str.into()]; - - if let Some(cfg) = config { - if let Some(features) = &cfg.features { - args.push("--features".into()); - args.push(features.join(",")); - } - if let Some(target) = &cfg.target { - args.push("--target".into()); - args.push(target.clone()); - } - } - - let status = ProcessCommand::new("cargo") - .args(&args) - .current_dir(crate_path) - .status() - .map_err(|e| format!("Failed to run cargo release: {}", e))?; - - if !status.success() { - return Err(format!("`cargo release {}` failed in crate {}", kind_str, crate_name)); - } - - //args.push("--execute".into()); - //let status = ProcessCommand::new("cargo") - // .args(&args) - // .current_dir(crate_path) - // .status() - // .map_err(|e| format!("Failed to run cargo release --execute: {}", e))?; - - //if !status.success() { - // return Err(format!( - // "`cargo release {kind_str} --execute` failed in crate {crate_name}" - // )); - //} - - Ok(()) -} - -fn main() { - let args = Args::parse(); - let root = args.crate_path.canonicalize().expect("Invalid root crate path"); - - match args.command { - Command::Dependencies => { - let mut visited = HashSet::new(); - let mut ordered = vec![]; - visit_recursive(&root, &mut visited, &mut ordered, &args.order); - for path in ordered { - if let Some((name, _)) = get_crate_metadata(&path) { - println!("{name}"); - } else { - eprintln!("Warning: could not read crate name from {:?}", path); - } - } - } - Command::Release { kind } => { - let config = load_release_config(); - let path = root; - match get_crate_metadata(&path) { - Some((name, version)) => { - println!("🚀 Releasing {name}..."); - let crate_cfg = config.get(&name); - match run_release_command(&path, &name, &version, &kind, crate_cfg) { - Ok(_) => { - println!("✅ Released {name}"); - } - Err(e) => { - eprintln!("❌ Error releasing {name}:\n{e}"); - eprintln!("\nYou may retry with: `cargo run -- {path:?} release {kind:?}`"); - std::process::exit(1); - } - } - } - None => { - eprintln!("Warning: Could not parse crate metadata in {:?}", path); - } - } - } - } -} diff --git a/embassy-rp/release.toml b/embassy-rp/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-rp/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-stm32/release.toml b/embassy-stm32/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-stm32/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-sync/CHANGELOG.md b/embassy-sync/CHANGELOG.md index fa0340c68..7418ead8d 100644 --- a/embassy-sync/CHANGELOG.md +++ b/embassy-sync/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased + +## Unreleased - ReleaseDate - Add `get_mut` to `LazyLock` - Add more `Debug` impls to `embassy-sync`, particularly on `OnceLock` diff --git a/embassy-time-driver/release.toml b/embassy-time-driver/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-time-driver/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-time-queue-utils/CHANGELOG.md b/embassy-time-queue-utils/CHANGELOG.md index 26200503c..510c29d58 100644 --- a/embassy-time-queue-utils/CHANGELOG.md +++ b/embassy-time-queue-utils/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased + +## Unreleased - ReleaseDate - Removed the embassy-executor dependency diff --git a/embassy-time/release.toml b/embassy-time/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-time/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-usb-dfu/CHANGELOG.md b/embassy-usb-dfu/CHANGELOG.md new file mode 100644 index 000000000..7042ad14c --- /dev/null +++ b/embassy-usb-dfu/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased - ReleaseDate + +- First release with changelog. diff --git a/embassy-usb-driver/release.toml b/embassy-usb-driver/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-usb-driver/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/embassy-usb-logger/CHANGELOG.md b/embassy-usb-logger/CHANGELOG.md index 79ea25839..c88b3ed6a 100644 --- a/embassy-usb-logger/CHANGELOG.md +++ b/embassy-usb-logger/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased + +## Unreleased - ReleaseDate ## 0.5.0 - 2025-07-22 diff --git a/embassy-usb-synopsys-otg/CHANGELOG.md b/embassy-usb-synopsys-otg/CHANGELOG.md index 9913ee533..55eef0a1e 100644 --- a/embassy-usb-synopsys-otg/CHANGELOG.md +++ b/embassy-usb-synopsys-otg/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased + +## Unreleased - ReleaseDate ## 0.3.0 - 2025-07-22 diff --git a/embassy-usb/release.toml b/embassy-usb/release.toml deleted file mode 100644 index fb6feaf21..000000000 --- a/embassy-usb/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, -] diff --git a/release/Cargo.toml b/release/Cargo.toml new file mode 100644 index 000000000..cc1b155e2 --- /dev/null +++ b/release/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "embassy-release" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.1", features = ["derive"] } +walkdir = "2.5.0" +toml = "0.8.8" +toml_edit = { version = "0.23.1", features = ["serde"] } +serde = { version = "1.0.198", features = ["derive"] } +regex = "1.10.4" +anyhow = "1" +petgraph = "0.8.2" +semver = "1.0.26" diff --git a/release/config.toml b/release/config.toml index 2292f4077..6b23217fa 100644 --- a/release/config.toml +++ b/release/config.toml @@ -1 +1,46 @@ + +embassy-stm32 = { features = ["defmt", "unstable-pac", "exti", "time-driver-any", "time", "stm32h755zi-cm7", "dual-bank"], target = "thumbv7em-none-eabi" } +embassy-nrf = { features = ["nrf52840", "time", "defmt", "unstable-pac", "gpiote", "time-driver-rtc1"], target = "thumbv7em-none-eabihf" } + embassy-rp = { features = ["defmt", "unstable-pac", "time-driver", "rp2040"], target = "thumbv6m-none-eabi" } +cyw43 = { features = ["defmt", "firmware-logs"], target = "thumbv6m-none-eabi" } +#cyw43-pio = { features = ["defmt", "embassy-rp/rp2040"], target = "thumbv6m-none-eabi" } + +embassy-boot = { features = ["defmt"] } +#embassy-boot-nrf = { features = ["defmt", "embassy-nrf/nrf52840"], target = "thumbv7em-none-eabihf" } +#embassy-boot-rp = { features = ["defmt", "embassy-rp/rp2040"], target = "thumbv6m-none-eabi" } +#embassy-boot-stm32 = { features = ["defmt", "embassy-stm32/stm32f429zi"], target = "thumbv7em-none-eabi" } + +embassy-time = { features = ["defmt", "std"] } +embassy-time-driver = { } +embassy-time-queue-utils = { features = ["defmt"] } + +embassy-futures = { } +embassy-embedded-hal = { features = ["time"] } +embassy-hal-internal = { } +embassy-executor = { features = ["defmt", "arch-cortex-m", "executor-thread", "executor-interrupt"], target = "thumbv7em-none-eabi" } +embassy-executor-macros = { } +embassy-sync = { } + +embassy-net = { features = ["defmt", "tcp", "udp", "raw", "dns", "icmp", "dhcpv4", "proto-ipv6", "medium-ethernet", "medium-ip", "medium-ieee802154", "multicast", "dhcpv4-hostname"] } +embassy-net-ppp = { } +embassy-net-esp-hosted = {} +embassy-net-driver-channel = {} +embassy-net-wiznet = {} +embassy-net-nrf91 = { features = ["defmt", "nrf9160"] } +embassy-net-driver = {} +embassy-net-tuntap = {} +embassy-net-adin1110 = {} +embassy-net-enc28j60 = {} + +embassy-usb-driver = { } +embassy-usb-dfu = { features = ["dfu"] } +embassy-usb-synopsys-otg = { } +embassy-usb = { features = ["defmt", "usbd-hid"] } +embassy-usb-logger = { } + +# Unreleased +# embassy-stm32-wpan = {} +# embassy-imxrt = {} +# embassy-nxp = {} +# embassy-mspm0 = {} diff --git a/release/release.toml b/release/release.toml new file mode 100644 index 000000000..fb6feaf21 --- /dev/null +++ b/release/release.toml @@ -0,0 +1,5 @@ +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## Unreleased - ReleaseDate\n", exactly=1}, +] diff --git a/release/src/main.rs b/release/src/main.rs new file mode 100644 index 000000000..38bb728a8 --- /dev/null +++ b/release/src/main.rs @@ -0,0 +1,519 @@ +use std::collections::{BTreeMap, HashMap}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command as ProcessCommand; + +use anyhow::{anyhow, Result}; +use clap::{Parser, Subcommand}; +use petgraph::graph::{Graph, NodeIndex}; +use petgraph::visit::Bfs; +use petgraph::{Directed, Direction}; +use serde::Deserialize; +use toml_edit::{DocumentMut, Item, Value}; + +/// Tool to traverse and operate on intra-repo Rust crate dependencies +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// Path to embassy repository + #[arg(short, long)] + repo: PathBuf, + + /// Command to perform on each crate + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// All crates and their direct dependencies + List, + /// List all dependencies for a crate + Dependencies { + /// Crate name to print dependencies for. + #[arg(value_name = "CRATE")] + crate_name: String, + }, + /// List all dependencies for a crate + Dependents { + /// Crate name to print dependencies for. + #[arg(value_name = "CRATE")] + crate_name: String, + }, + + /// SemverCheck + SemverCheck { + /// Crate to check. Will traverse that crate an it's dependents. If not specified checks all crates. + #[arg(value_name = "CRATE")] + crate_name: String, + }, + /// Prepare to release a crate and all dependents that needs updating + /// - Semver checks + /// - Bump versions and commit + /// - Create tag. + PrepareRelease { + /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates. + #[arg(value_name = "CRATE")] + crate_name: String, + }, +} + +#[derive(Debug, Subcommand, Clone, Copy, PartialEq)] +enum ReleaseKind { + Patch, + Minor, +} + +#[derive(Debug, Deserialize)] +struct CargoToml { + package: Option, + dependencies: Option, +} + +#[derive(Debug, Deserialize)] +struct Package { + name: String, + version: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum Dep { + Version(String), + DetailedTable(BTreeMap), +} + +type Deps = std::collections::BTreeMap; + +#[derive(Debug, Clone, Deserialize)] +struct CrateConfig { + features: Option>, + target: Option, +} + +type ReleaseConfig = HashMap; + +fn load_release_config(repo: &Path) -> ReleaseConfig { + let config_path = repo.join("release/config.toml"); + if !config_path.exists() { + return HashMap::new(); + } + let content = fs::read_to_string(&config_path).expect("Failed to read release/config.toml"); + toml::from_str(&content).expect("Invalid TOML format in release/config.toml") +} + +fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { + let path = &c.path; + c.id.version = new_version.to_string(); + let content = fs::read_to_string(&path)?; + let mut doc: DocumentMut = content.parse()?; + for section in ["package"] { + if let Some(Item::Table(dep_table)) = doc.get_mut(section) { + dep_table.insert("version", Item::Value(Value::from(new_version))); + } + } + fs::write(&path, doc.to_string())?; + Ok(()) +} + +fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> { + let path = &to_update.path; + let content = fs::read_to_string(&path)?; + let mut doc: DocumentMut = content.parse()?; + let mut changed = false; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(Item::Table(dep_table)) = doc.get_mut(section) { + if let Some(item) = dep_table.get_mut(&dep.name) { + match item { + // e.g., foo = "0.1.0" + Item::Value(Value::String(_)) => { + *item = Item::Value(Value::from(new_version)); + changed = true; + } + // e.g., foo = { version = "...", ... } + Item::Value(Value::InlineTable(inline)) => { + if inline.contains_key("version") { + inline["version"] = Value::from(new_version); + changed = true; + } + } + _ => {} // Leave unusual formats untouched + } + } + } + } + + if changed { + fs::write(&path, doc.to_string())?; + println!("🔧 Updated {} to {} in {}", dep.name, new_version, path.display()); + } + Ok(()) +} + +#[derive(Debug, Clone)] +struct Crate { + id: CrateId, + path: PathBuf, + config: CrateConfig, + dependencies: Vec, +} + +#[derive(Debug, Clone, PartialOrd, Ord)] +struct CrateId { + name: String, + version: String, +} + +impl PartialEq for CrateId { + fn eq(&self, other: &CrateId) -> bool { + self.name == other.name + } +} + +impl Eq for CrateId {} +impl std::hash::Hash for CrateId { + fn hash(&self, state: &mut H) { + self.name.hash(state) + } +} + +fn list_crates(path: &PathBuf) -> Result> { + let d = std::fs::read_dir(path)?; + let release_config = load_release_config(path); + let mut crates = BTreeMap::new(); + for c in d { + let entry = c?; + let name = entry.file_name().to_str().unwrap().to_string(); + if entry.file_type()?.is_dir() && name.starts_with("embassy-") { + let entry = entry.path().join("Cargo.toml"); + if entry.exists() { + let content = fs::read_to_string(&entry).unwrap_or_else(|_| { + panic!("Failed to read {:?}", entry); + }); + let parsed: CargoToml = toml::from_str(&content).unwrap_or_else(|e| { + panic!("Failed to parse {:?}: {}", entry, e); + }); + let p = parsed.package.unwrap(); + let id = CrateId { + name: p.name.clone(), + version: p.version.unwrap(), + }; + + let mut dependencies = Vec::new(); + if let Some(deps) = parsed.dependencies { + for (k, v) in deps { + if k.starts_with("embassy-") { + dependencies.push(CrateId { + name: k, + version: match v { + Dep::Version(v) => v, + Dep::DetailedTable(table) => { + table.get("version").unwrap().as_str().unwrap().to_string() + } + }, + }); + } + } + } + + let path = path.join(entry); + if let Some(config) = release_config.get(&p.name) { + crates.insert( + id.clone(), + Crate { + id, + path, + dependencies, + config: config.clone(), + }, + ); + } + } + } + } + Ok(crates) +} + +fn build_graph(crates: &BTreeMap) -> (Graph, HashMap) { + let mut graph = Graph::::new(); + let mut node_indices: HashMap = HashMap::new(); + + // Helper to insert or get existing node + let get_or_insert_node = |id: CrateId, graph: &mut Graph, map: &mut HashMap| { + if let Some(&idx) = map.get(&id) { + idx + } else { + let idx = graph.add_node(id.clone()); + map.insert(id, idx); + idx + } + }; + + for krate in crates.values() { + get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); + } + + for krate in crates.values() { + // Insert crate node if not exists + let crate_idx = get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); + + // Insert dependencies and connect edges + for dep in krate.dependencies.iter() { + let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices); + graph.add_edge(crate_idx, dep_idx, ()); + } + } + + (graph, node_indices) +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let root = args.repo.canonicalize()?; //.expect("Invalid root crate path"); + let mut crates = list_crates(&root)?; + //println!("Crates: {:?}", crates); + let (mut graph, indices) = build_graph(&crates); + + // use petgraph::dot::{Config, Dot}; + // println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); + + match args.command { + Command::List => { + let ordered = petgraph::algo::toposort(&graph, None).unwrap(); + for node in ordered.iter() { + if graph.neighbors_directed(*node, Direction::Incoming).count() == 0 { + let start = graph.node_weight(*node).unwrap(); + let mut bfs = Bfs::new(&graph, *node); + while let Some(node) = bfs.next(&graph) { + let weight = graph.node_weight(node).unwrap(); + if weight.name == start.name { + println!("+ {}-{}", weight.name, weight.version); + } else { + println!("|- {}-{}", weight.name, weight.version); + } + } + println!(""); + } + } + } + Command::Dependencies { crate_name } => { + let idx = indices + .get(&CrateId { + name: crate_name.clone(), + version: "".to_string(), + }) + .expect("unable to find crate in tree"); + let mut bfs = Bfs::new(&graph, *idx); + while let Some(node) = bfs.next(&graph) { + let weight = graph.node_weight(node).unwrap(); + if weight.name == crate_name { + println!("+ {}-{}", weight.name, weight.version); + } else { + println!("|- {}-{}", weight.name, weight.version); + } + } + } + Command::Dependents { crate_name } => { + let idx = indices + .get(&CrateId { + name: crate_name.clone(), + version: "".to_string(), + }) + .expect("unable to find crate in tree"); + let node = graph.node_weight(*idx).unwrap(); + println!("+ {}-{}", node.name, node.version); + for parent in graph.neighbors_directed(*idx, Direction::Incoming) { + let weight = graph.node_weight(parent).unwrap(); + println!("|- {}-{}", weight.name, weight.version); + } + } + Command::SemverCheck { crate_name } => { + let c = crates + .get(&CrateId { + name: crate_name.to_string(), + version: "".to_string(), + }) + .unwrap(); + check_semver(&c)?; + } + Command::PrepareRelease { crate_name } => { + let start = indices + .get(&CrateId { + name: crate_name.clone(), + version: "".to_string(), + }) + .expect("unable to find crate in tree"); + + graph.reverse(); + + let mut bfs = Bfs::new(&graph, *start); + + while let Some(node) = bfs.next(&graph) { + let weight = graph.node_weight(node).unwrap(); + println!("Preparing {}", weight.name); + let mut c = crates.get_mut(weight).unwrap(); + let ver = semver::Version::parse(&c.id.version)?; + let newver = if let Err(_) = check_semver(&c) { + println!("Semver check failed, bumping minor!"); + semver::Version::new(ver.major, ver.minor + 1, 0) + } else { + semver::Version::new(ver.major, ver.minor, ver.patch + 1) + }; + + println!( + "Updating {} from {} -> {}", + weight.name, + c.id.version, + newver.to_string() + ); + let newver = newver.to_string(); + + update_version(&mut c, &newver)?; + let c = crates.get(weight).unwrap(); + + // Update all nodes further down the tree + let mut bfs = Bfs::new(&graph, node); + while let Some(dep_node) = bfs.next(&graph) { + let dep_weight = graph.node_weight(dep_node).unwrap(); + let dep = crates.get(dep_weight).unwrap(); + update_versions(dep, &c.id, &newver)?; + } + + // Update changelog + update_changelog(&root, &c)?; + } + + let weight = graph.node_weight(*start).unwrap(); + let c = crates.get(weight).unwrap(); + publish_release(&root, &c, false)?; + + println!("# Please inspect changes and run the following commands when happy:"); + + println!("git commit -a -m 'chore: prepare crate releases'"); + let mut bfs = Bfs::new(&graph, *start); + while let Some(node) = bfs.next(&graph) { + let weight = graph.node_weight(node).unwrap(); + println!("git tag {}-v{}", weight.name, weight.version); + } + + println!(""); + println!("# Run these commands to publish the crate and dependents:"); + + let mut bfs = Bfs::new(&graph, *start); + while let Some(node) = bfs.next(&graph) { + let weight = graph.node_weight(node).unwrap(); + let c = crates.get(weight).unwrap(); + + let mut args: Vec = vec![ + "publish".to_string(), + "--manifest-path".to_string(), + c.path.display().to_string(), + ]; + + if let Some(features) = &c.config.features { + args.push("--features".into()); + args.push(features.join(",")); + } + + if let Some(target) = &c.config.target { + args.push("--target".into()); + args.push(target.clone()); + } + + /* + let mut dry_run = args.clone(); + dry_run.push("--dry-run".to_string()); + + println!("cargo {}", dry_run.join(" ")); + */ + println!("cargo {}", args.join(" ")); + } + + println!(""); + println!("# Run this command to push changes and tags:"); + println!("git push --tags"); + } + } + Ok(()) +} + +fn check_semver(c: &Crate) -> Result<()> { + let mut args: Vec = vec![ + "semver-checks".to_string(), + "--manifest-path".to_string(), + c.path.display().to_string(), + "--default-features".to_string(), + ]; + if let Some(features) = &c.config.features { + args.push("--features".into()); + args.push(features.join(",")); + } + + let status = ProcessCommand::new("cargo").args(&args).output()?; + + println!("{}", core::str::from_utf8(&status.stdout).unwrap()); + eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); + if !status.status.success() { + return Err(anyhow!("semver check failed")); + } else { + Ok(()) + } +} + +fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { + let args: Vec = vec![ + "release".to_string(), + "replace".to_string(), + "--config".to_string(), + repo.join("release").join("release.toml").display().to_string(), + "--manifest-path".to_string(), + c.path.display().to_string(), + "--execute".to_string(), + "--no-confirm".to_string(), + ]; + + let status = ProcessCommand::new("cargo").args(&args).output()?; + + println!("{}", core::str::from_utf8(&status.stdout).unwrap()); + eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); + if !status.status.success() { + return Err(anyhow!("release replace failed")); + } else { + Ok(()) + } +} + +fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { + let mut args: Vec = vec![ + "publish".to_string(), + "--manifest-path".to_string(), + c.path.display().to_string(), + ]; + + if let Some(features) = &c.config.features { + args.push("--features".into()); + args.push(features.join(",")); + } + + if let Some(target) = &c.config.target { + args.push("--target".into()); + args.push(target.clone()); + } + + if !push { + args.push("--dry-run".to_string()); + args.push("--allow-dirty".to_string()); + args.push("--keep-going".to_string()); + } + + let status = ProcessCommand::new("cargo").args(&args).output()?; + + println!("{}", core::str::from_utf8(&status.stdout).unwrap()); + eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); + if !status.status.success() { + return Err(anyhow!("publish failed")); + } else { + Ok(()) + } +} -- cgit