diff options
| author | Ulf Lilleengen <[email protected]> | 2025-08-14 13:36:39 +0200 |
|---|---|---|
| committer | Ulf Lilleengen <[email protected]> | 2025-08-25 19:44:49 +0200 |
| commit | 6a347f1f09b0076af868dcd63d9139081c92172b (patch) | |
| tree | 27ccbc753f0950cbbdbd0cda73d24eb419b7cd96 | |
| parent | ac60eaeddd9c4accbe8dc20d0486382940723efb (diff) | |
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
31 files changed, 655 insertions, 370 deletions
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | - 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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | - 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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 8 | ## 0.2.0 - 2023-10-18 | 11 | ## 0.2.0 - 2023-10-18 |
| 9 | 12 | ||
| 10 | - Added support for IEEE 802.15.4 mediums. | 13 | - 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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | - 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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | ## 0.1.1 - 2025-08-14 | ||
| 12 | |||
| 13 | - 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 @@ | |||
| 1 | [package] | 1 | [package] |
| 2 | name = "embassy-net-nrf91" | 2 | name = "embassy-net-nrf91" |
| 3 | version = "0.1.0" | 3 | version = "0.1.1" |
| 4 | edition = "2021" | 4 | edition = "2021" |
| 5 | description = "embassy-net driver for Nordic nRF91-series cellular modems" | 5 | description = "embassy-net driver for Nordic nRF91-series cellular modems" |
| 6 | keywords = ["embedded", "nrf91", "embassy-net", "cellular"] | 6 | keywords = ["embedded", "nrf91", "embassy-net", "cellular"] |
| @@ -36,4 +36,4 @@ target = "thumbv7em-none-eabi" | |||
| 36 | features = ["defmt", "nrf-pac/nrf9160"] | 36 | features = ["defmt", "nrf-pac/nrf9160"] |
| 37 | 37 | ||
| 38 | [package.metadata.docs.rs] | 38 | [package.metadata.docs.rs] |
| 39 | features = ["defmt"] | 39 | 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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 8 | ## 0.2.0 - 2025-01-12 | 11 | ## 0.2.0 - 2025-01-12 |
| 9 | 12 | ||
| 10 | - Update `ppproto` to v0.2. | 13 | - 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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | ## 0.1.1 - 2025-08-14 | ||
| 12 | |||
| 13 | - 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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | ## Unreleased | 8 | <!-- next-header --> |
| 9 | ## Unreleased - ReleaseDate | ||
| 9 | 10 | ||
| 10 | No unreleased changes yet... Quick, go send a PR! | 11 | No unreleased changes yet... Quick, go send a PR! |
| 11 | 12 | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | use std::collections::{HashMap, HashSet}; | ||
| 2 | use std::fs; | ||
| 3 | use std::path::{Path, PathBuf}; | ||
| 4 | use std::process::Command as ProcessCommand; | ||
| 5 | |||
| 6 | use clap::{Parser, Subcommand, ValueEnum}; | ||
| 7 | use regex::Regex; | ||
| 8 | use serde::Deserialize; | ||
| 9 | use toml_edit::{DocumentMut, Item, Value}; | ||
| 10 | use walkdir::WalkDir; | ||
| 11 | |||
| 12 | /// Tool to traverse and operate on intra-repo Rust crate dependencies | ||
| 13 | #[derive(Parser, Debug)] | ||
| 14 | #[command(author, version, about)] | ||
| 15 | struct Args { | ||
| 16 | /// Path to the root crate | ||
| 17 | #[arg(value_name = "CRATE_PATH")] | ||
| 18 | crate_path: PathBuf, | ||
| 19 | |||
| 20 | /// Command to perform on each crate | ||
| 21 | #[command(subcommand)] | ||
| 22 | command: Command, | ||
| 23 | |||
| 24 | /// Traversal order | ||
| 25 | #[arg(short, long, default_value = "post")] | ||
| 26 | order: TraversalOrder, | ||
| 27 | } | ||
| 28 | |||
| 29 | #[derive(Debug, Clone, ValueEnum, PartialEq)] | ||
| 30 | enum TraversalOrder { | ||
| 31 | Pre, | ||
| 32 | Post, | ||
| 33 | } | ||
| 34 | |||
| 35 | #[derive(Debug, Subcommand)] | ||
| 36 | enum Command { | ||
| 37 | /// Print all dependencies | ||
| 38 | Dependencies, | ||
| 39 | |||
| 40 | /// Release crate | ||
| 41 | Release { | ||
| 42 | #[command(subcommand)] | ||
| 43 | kind: ReleaseKind, | ||
| 44 | }, | ||
| 45 | } | ||
| 46 | |||
| 47 | #[derive(Debug, Subcommand, Clone, Copy, PartialEq)] | ||
| 48 | enum ReleaseKind { | ||
| 49 | Patch, | ||
| 50 | Minor, | ||
| 51 | } | ||
| 52 | |||
| 53 | #[derive(Debug, Deserialize)] | ||
| 54 | struct CargoToml { | ||
| 55 | package: Option<Package>, | ||
| 56 | dependencies: Option<Deps>, | ||
| 57 | } | ||
| 58 | |||
| 59 | #[derive(Debug, Deserialize)] | ||
| 60 | struct Package { | ||
| 61 | name: String, | ||
| 62 | version: Option<String>, | ||
| 63 | } | ||
| 64 | |||
| 65 | #[derive(Debug, Deserialize)] | ||
| 66 | #[serde(untagged)] | ||
| 67 | enum Dep { | ||
| 68 | Version(String), | ||
| 69 | DetailedTable(HashMap<String, toml::Value>), | ||
| 70 | } | ||
| 71 | |||
| 72 | type Deps = std::collections::HashMap<String, Dep>; | ||
| 73 | |||
| 74 | #[derive(Debug, Deserialize)] | ||
| 75 | struct CrateConfig { | ||
| 76 | features: Option<Vec<String>>, | ||
| 77 | target: Option<String>, | ||
| 78 | } | ||
| 79 | |||
| 80 | type ReleaseConfig = HashMap<String, CrateConfig>; | ||
| 81 | |||
| 82 | fn find_path_deps(cargo_path: &Path) -> Vec<PathBuf> { | ||
| 83 | let content = fs::read_to_string(cargo_path).unwrap_or_else(|_| { | ||
| 84 | panic!("Failed to read {:?}", cargo_path); | ||
| 85 | }); | ||
| 86 | let parsed: CargoToml = toml::from_str(&content).unwrap_or_else(|e| { | ||
| 87 | panic!("Failed to parse {:?}: {}", cargo_path, e); | ||
| 88 | }); | ||
| 89 | |||
| 90 | let mut paths = vec![]; | ||
| 91 | if let Some(deps) = parsed.dependencies { | ||
| 92 | for (_name, dep) in deps { | ||
| 93 | match dep { | ||
| 94 | Dep::Version(_) => { | ||
| 95 | // External dependency — skip | ||
| 96 | } | ||
| 97 | Dep::DetailedTable(table) => { | ||
| 98 | if let Some(toml::Value::String(path)) = table.get("path") { | ||
| 99 | let dep_path = cargo_path.parent().unwrap().join(path).canonicalize().unwrap(); | ||
| 100 | paths.push(dep_path); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | paths | ||
| 108 | } | ||
| 109 | |||
| 110 | fn visit_recursive( | ||
| 111 | root_crate: &Path, | ||
| 112 | visited: &mut HashSet<PathBuf>, | ||
| 113 | output: &mut Vec<PathBuf>, | ||
| 114 | order: &TraversalOrder, | ||
| 115 | ) { | ||
| 116 | if !visited.insert(root_crate.to_path_buf()) { | ||
| 117 | return; | ||
| 118 | } | ||
| 119 | |||
| 120 | let cargo_toml = root_crate.join("Cargo.toml"); | ||
| 121 | let deps = find_path_deps(&cargo_toml); | ||
| 122 | |||
| 123 | if *order == TraversalOrder::Pre { | ||
| 124 | output.push(root_crate.to_path_buf()); | ||
| 125 | } | ||
| 126 | |||
| 127 | let mut deps_sorted = deps; | ||
| 128 | deps_sorted.sort(); | ||
| 129 | for dep in deps_sorted { | ||
| 130 | visit_recursive(&dep, visited, output, order); | ||
| 131 | } | ||
| 132 | |||
| 133 | if *order == TraversalOrder::Post { | ||
| 134 | output.push(root_crate.to_path_buf()); | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | fn get_crate_metadata(crate_path: &Path) -> Option<(String, String)> { | ||
| 139 | let cargo_toml = crate_path.join("Cargo.toml"); | ||
| 140 | let content = fs::read_to_string(&cargo_toml).ok()?; | ||
| 141 | let parsed: CargoToml = toml::from_str(&content).ok()?; | ||
| 142 | let pkg = parsed.package?; | ||
| 143 | let name = pkg.name; | ||
| 144 | let version = pkg.version?; | ||
| 145 | Some((name, version)) | ||
| 146 | } | ||
| 147 | |||
| 148 | fn load_release_config() -> ReleaseConfig { | ||
| 149 | let config_path = PathBuf::from("release/config.toml"); | ||
| 150 | if !config_path.exists() { | ||
| 151 | return HashMap::new(); | ||
| 152 | } | ||
| 153 | let content = fs::read_to_string(&config_path).expect("Failed to read release/config.toml"); | ||
| 154 | toml::from_str(&content).expect("Invalid TOML format in release/config.toml") | ||
| 155 | } | ||
| 156 | |||
| 157 | fn bump_dependency_versions(crate_name: &str, new_version: &str) -> Result<(), String> { | ||
| 158 | let mut cargo_files: Vec<PathBuf> = WalkDir::new(".") | ||
| 159 | .into_iter() | ||
| 160 | .filter_map(Result::ok) | ||
| 161 | .filter(|e| e.file_name() == "Cargo.toml") | ||
| 162 | .map(|e| e.into_path()) | ||
| 163 | .collect(); | ||
| 164 | |||
| 165 | cargo_files.sort(); | ||
| 166 | |||
| 167 | for path in cargo_files { | ||
| 168 | let content = fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; | ||
| 169 | |||
| 170 | let mut doc: DocumentMut = content | ||
| 171 | .parse() | ||
| 172 | .map_err(|e| format!("Failed to parse TOML in {}: {}", path.display(), e))?; | ||
| 173 | |||
| 174 | let mut changed = false; | ||
| 175 | |||
| 176 | for section in ["dependencies", "dev-dependencies", "build-dependencies"] { | ||
| 177 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 178 | if let Some(item) = dep_table.get_mut(crate_name) { | ||
| 179 | match item { | ||
| 180 | // e.g., foo = "0.1.0" | ||
| 181 | Item::Value(Value::String(_)) => { | ||
| 182 | *item = Item::Value(Value::from(new_version)); | ||
| 183 | changed = true; | ||
| 184 | } | ||
| 185 | // e.g., foo = { version = "...", ... } | ||
| 186 | Item::Value(Value::InlineTable(inline)) => { | ||
| 187 | if inline.contains_key("version") { | ||
| 188 | inline["version"] = Value::from(new_version); | ||
| 189 | changed = true; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | _ => {} // Leave unusual formats untouched | ||
| 193 | } | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | if changed { | ||
| 199 | fs::write(&path, doc.to_string()).map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; | ||
| 200 | println!("🔧 Updated {} to {} in {}", crate_name, new_version, path.display()); | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | Ok(()) | ||
| 205 | } | ||
| 206 | |||
| 207 | fn run_release_command( | ||
| 208 | crate_path: &Path, | ||
| 209 | crate_name: &str, | ||
| 210 | version: &str, | ||
| 211 | kind: &ReleaseKind, | ||
| 212 | config: Option<&CrateConfig>, | ||
| 213 | ) -> Result<(), String> { | ||
| 214 | let kind_str = match kind { | ||
| 215 | ReleaseKind::Patch => "patch", | ||
| 216 | ReleaseKind::Minor => "minor", | ||
| 217 | }; | ||
| 218 | |||
| 219 | if *kind == ReleaseKind::Minor { | ||
| 220 | bump_dependency_versions(crate_name, version)?; | ||
| 221 | } | ||
| 222 | |||
| 223 | let mut args: Vec<String> = vec!["release".into(), kind_str.into()]; | ||
| 224 | |||
| 225 | if let Some(cfg) = config { | ||
| 226 | if let Some(features) = &cfg.features { | ||
| 227 | args.push("--features".into()); | ||
| 228 | args.push(features.join(",")); | ||
| 229 | } | ||
| 230 | if let Some(target) = &cfg.target { | ||
| 231 | args.push("--target".into()); | ||
| 232 | args.push(target.clone()); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | let status = ProcessCommand::new("cargo") | ||
| 237 | .args(&args) | ||
| 238 | .current_dir(crate_path) | ||
| 239 | .status() | ||
| 240 | .map_err(|e| format!("Failed to run cargo release: {}", e))?; | ||
| 241 | |||
| 242 | if !status.success() { | ||
| 243 | return Err(format!("`cargo release {}` failed in crate {}", kind_str, crate_name)); | ||
| 244 | } | ||
| 245 | |||
| 246 | //args.push("--execute".into()); | ||
| 247 | //let status = ProcessCommand::new("cargo") | ||
| 248 | // .args(&args) | ||
| 249 | // .current_dir(crate_path) | ||
| 250 | // .status() | ||
| 251 | // .map_err(|e| format!("Failed to run cargo release --execute: {}", e))?; | ||
| 252 | |||
| 253 | //if !status.success() { | ||
| 254 | // return Err(format!( | ||
| 255 | // "`cargo release {kind_str} --execute` failed in crate {crate_name}" | ||
| 256 | // )); | ||
| 257 | //} | ||
| 258 | |||
| 259 | Ok(()) | ||
| 260 | } | ||
| 261 | |||
| 262 | fn main() { | ||
| 263 | let args = Args::parse(); | ||
| 264 | let root = args.crate_path.canonicalize().expect("Invalid root crate path"); | ||
| 265 | |||
| 266 | match args.command { | ||
| 267 | Command::Dependencies => { | ||
| 268 | let mut visited = HashSet::new(); | ||
| 269 | let mut ordered = vec![]; | ||
| 270 | visit_recursive(&root, &mut visited, &mut ordered, &args.order); | ||
| 271 | for path in ordered { | ||
| 272 | if let Some((name, _)) = get_crate_metadata(&path) { | ||
| 273 | println!("{name}"); | ||
| 274 | } else { | ||
| 275 | eprintln!("Warning: could not read crate name from {:?}", path); | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | Command::Release { kind } => { | ||
| 280 | let config = load_release_config(); | ||
| 281 | let path = root; | ||
| 282 | match get_crate_metadata(&path) { | ||
| 283 | Some((name, version)) => { | ||
| 284 | println!("🚀 Releasing {name}..."); | ||
| 285 | let crate_cfg = config.get(&name); | ||
| 286 | match run_release_command(&path, &name, &version, &kind, crate_cfg) { | ||
| 287 | Ok(_) => { | ||
| 288 | println!("✅ Released {name}"); | ||
| 289 | } | ||
| 290 | Err(e) => { | ||
| 291 | eprintln!("❌ Error releasing {name}:\n{e}"); | ||
| 292 | eprintln!("\nYou may retry with: `cargo run -- {path:?} release {kind:?}`"); | ||
| 293 | std::process::exit(1); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | } | ||
| 297 | None => { | ||
| 298 | eprintln!("Warning: Could not parse crate metadata in {:?}", path); | ||
| 299 | } | ||
| 300 | } | ||
| 301 | } | ||
| 302 | } | ||
| 303 | } | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | ## Unreleased | 8 | <!-- next-header --> |
| 9 | ## Unreleased - ReleaseDate | ||
| 9 | 10 | ||
| 10 | - Add `get_mut` to `LazyLock` | 11 | - Add `get_mut` to `LazyLock` |
| 11 | - Add more `Debug` impls to `embassy-sync`, particularly on `OnceLock` | 12 | - 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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | ## Unreleased | 8 | <!-- next-header --> |
| 9 | ## Unreleased - ReleaseDate | ||
| 9 | 10 | ||
| 10 | - Removed the embassy-executor dependency | 11 | - Removed the embassy-executor dependency |
| 11 | 12 | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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 @@ | |||
| 1 | # Changelog | ||
| 2 | |||
| 3 | All notable changes to this project will be documented in this file. | ||
| 4 | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| 7 | |||
| 8 | <!-- next-header --> | ||
| 9 | ## Unreleased - ReleaseDate | ||
| 10 | |||
| 11 | - 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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | ## Unreleased | 8 | <!-- next-header --> |
| 9 | ## Unreleased - ReleaseDate | ||
| 9 | 10 | ||
| 10 | ## 0.5.0 - 2025-07-22 | 11 | ## 0.5.0 - 2025-07-22 |
| 11 | 12 | ||
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. | |||
| 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
| 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 | 7 | ||
| 8 | ## Unreleased | 8 | <!-- next-header --> |
| 9 | ## Unreleased - ReleaseDate | ||
| 9 | 10 | ||
| 10 | ## 0.3.0 - 2025-07-22 | 11 | ## 0.3.0 - 2025-07-22 |
| 11 | 12 | ||
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 @@ | |||
| 1 | pre-release-replacements = [ | ||
| 2 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, | ||
| 3 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, | ||
| 4 | {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## Unreleased - ReleaseDate\n", exactly=1}, | ||
| 5 | ] | ||
diff --git a/embassy-release/Cargo.toml b/release/Cargo.toml index de548e650..cc1b155e2 100644 --- a/embassy-release/Cargo.toml +++ b/release/Cargo.toml | |||
| @@ -10,3 +10,6 @@ toml = "0.8.8" | |||
| 10 | toml_edit = { version = "0.23.1", features = ["serde"] } | 10 | toml_edit = { version = "0.23.1", features = ["serde"] } |
| 11 | serde = { version = "1.0.198", features = ["derive"] } | 11 | serde = { version = "1.0.198", features = ["derive"] } |
| 12 | regex = "1.10.4" | 12 | regex = "1.10.4" |
| 13 | anyhow = "1" | ||
| 14 | petgraph = "0.8.2" | ||
| 15 | 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 @@ | |||
| 1 | |||
| 2 | embassy-stm32 = { features = ["defmt", "unstable-pac", "exti", "time-driver-any", "time", "stm32h755zi-cm7", "dual-bank"], target = "thumbv7em-none-eabi" } | ||
| 3 | embassy-nrf = { features = ["nrf52840", "time", "defmt", "unstable-pac", "gpiote", "time-driver-rtc1"], target = "thumbv7em-none-eabihf" } | ||
| 4 | |||
| 1 | embassy-rp = { features = ["defmt", "unstable-pac", "time-driver", "rp2040"], target = "thumbv6m-none-eabi" } | 5 | embassy-rp = { features = ["defmt", "unstable-pac", "time-driver", "rp2040"], target = "thumbv6m-none-eabi" } |
| 6 | cyw43 = { features = ["defmt", "firmware-logs"], target = "thumbv6m-none-eabi" } | ||
| 7 | #cyw43-pio = { features = ["defmt", "embassy-rp/rp2040"], target = "thumbv6m-none-eabi" } | ||
| 8 | |||
| 9 | embassy-boot = { features = ["defmt"] } | ||
| 10 | #embassy-boot-nrf = { features = ["defmt", "embassy-nrf/nrf52840"], target = "thumbv7em-none-eabihf" } | ||
| 11 | #embassy-boot-rp = { features = ["defmt", "embassy-rp/rp2040"], target = "thumbv6m-none-eabi" } | ||
| 12 | #embassy-boot-stm32 = { features = ["defmt", "embassy-stm32/stm32f429zi"], target = "thumbv7em-none-eabi" } | ||
| 13 | |||
| 14 | embassy-time = { features = ["defmt", "std"] } | ||
| 15 | embassy-time-driver = { } | ||
| 16 | embassy-time-queue-utils = { features = ["defmt"] } | ||
| 17 | |||
| 18 | embassy-futures = { } | ||
| 19 | embassy-embedded-hal = { features = ["time"] } | ||
| 20 | embassy-hal-internal = { } | ||
| 21 | embassy-executor = { features = ["defmt", "arch-cortex-m", "executor-thread", "executor-interrupt"], target = "thumbv7em-none-eabi" } | ||
| 22 | embassy-executor-macros = { } | ||
| 23 | embassy-sync = { } | ||
| 24 | |||
| 25 | embassy-net = { features = ["defmt", "tcp", "udp", "raw", "dns", "icmp", "dhcpv4", "proto-ipv6", "medium-ethernet", "medium-ip", "medium-ieee802154", "multicast", "dhcpv4-hostname"] } | ||
| 26 | embassy-net-ppp = { } | ||
| 27 | embassy-net-esp-hosted = {} | ||
| 28 | embassy-net-driver-channel = {} | ||
| 29 | embassy-net-wiznet = {} | ||
| 30 | embassy-net-nrf91 = { features = ["defmt", "nrf9160"] } | ||
| 31 | embassy-net-driver = {} | ||
| 32 | embassy-net-tuntap = {} | ||
| 33 | embassy-net-adin1110 = {} | ||
| 34 | embassy-net-enc28j60 = {} | ||
| 35 | |||
| 36 | embassy-usb-driver = { } | ||
| 37 | embassy-usb-dfu = { features = ["dfu"] } | ||
| 38 | embassy-usb-synopsys-otg = { } | ||
| 39 | embassy-usb = { features = ["defmt", "usbd-hid"] } | ||
| 40 | embassy-usb-logger = { } | ||
| 41 | |||
| 42 | # Unreleased | ||
| 43 | # embassy-stm32-wpan = {} | ||
| 44 | # embassy-imxrt = {} | ||
| 45 | # embassy-nxp = {} | ||
| 46 | # embassy-mspm0 = {} | ||
diff --git a/cyw43-pio/release.toml b/release/release.toml index fb6feaf21..fb6feaf21 100644 --- a/cyw43-pio/release.toml +++ b/release/release.toml | |||
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 @@ | |||
| 1 | use std::collections::{BTreeMap, HashMap}; | ||
| 2 | use std::fs; | ||
| 3 | use std::path::{Path, PathBuf}; | ||
| 4 | use std::process::Command as ProcessCommand; | ||
| 5 | |||
| 6 | use anyhow::{anyhow, Result}; | ||
| 7 | use clap::{Parser, Subcommand}; | ||
| 8 | use petgraph::graph::{Graph, NodeIndex}; | ||
| 9 | use petgraph::visit::Bfs; | ||
| 10 | use petgraph::{Directed, Direction}; | ||
| 11 | use serde::Deserialize; | ||
| 12 | use toml_edit::{DocumentMut, Item, Value}; | ||
| 13 | |||
| 14 | /// Tool to traverse and operate on intra-repo Rust crate dependencies | ||
| 15 | #[derive(Parser, Debug)] | ||
| 16 | #[command(author, version, about)] | ||
| 17 | struct Args { | ||
| 18 | /// Path to embassy repository | ||
| 19 | #[arg(short, long)] | ||
| 20 | repo: PathBuf, | ||
| 21 | |||
| 22 | /// Command to perform on each crate | ||
| 23 | #[command(subcommand)] | ||
| 24 | command: Command, | ||
| 25 | } | ||
| 26 | |||
| 27 | #[derive(Debug, Subcommand)] | ||
| 28 | enum Command { | ||
| 29 | /// All crates and their direct dependencies | ||
| 30 | List, | ||
| 31 | /// List all dependencies for a crate | ||
| 32 | Dependencies { | ||
| 33 | /// Crate name to print dependencies for. | ||
| 34 | #[arg(value_name = "CRATE")] | ||
| 35 | crate_name: String, | ||
| 36 | }, | ||
| 37 | /// List all dependencies for a crate | ||
| 38 | Dependents { | ||
| 39 | /// Crate name to print dependencies for. | ||
| 40 | #[arg(value_name = "CRATE")] | ||
| 41 | crate_name: String, | ||
| 42 | }, | ||
| 43 | |||
| 44 | /// SemverCheck | ||
| 45 | SemverCheck { | ||
| 46 | /// Crate to check. Will traverse that crate an it's dependents. If not specified checks all crates. | ||
| 47 | #[arg(value_name = "CRATE")] | ||
| 48 | crate_name: String, | ||
| 49 | }, | ||
| 50 | /// Prepare to release a crate and all dependents that needs updating | ||
| 51 | /// - Semver checks | ||
| 52 | /// - Bump versions and commit | ||
| 53 | /// - Create tag. | ||
| 54 | PrepareRelease { | ||
| 55 | /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates. | ||
| 56 | #[arg(value_name = "CRATE")] | ||
| 57 | crate_name: String, | ||
| 58 | }, | ||
| 59 | } | ||
| 60 | |||
| 61 | #[derive(Debug, Subcommand, Clone, Copy, PartialEq)] | ||
| 62 | enum ReleaseKind { | ||
| 63 | Patch, | ||
| 64 | Minor, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Debug, Deserialize)] | ||
| 68 | struct CargoToml { | ||
| 69 | package: Option<Package>, | ||
| 70 | dependencies: Option<Deps>, | ||
| 71 | } | ||
| 72 | |||
| 73 | #[derive(Debug, Deserialize)] | ||
| 74 | struct Package { | ||
| 75 | name: String, | ||
| 76 | version: Option<String>, | ||
| 77 | } | ||
| 78 | |||
| 79 | #[derive(Debug, Deserialize)] | ||
| 80 | #[serde(untagged)] | ||
| 81 | enum Dep { | ||
| 82 | Version(String), | ||
| 83 | DetailedTable(BTreeMap<String, toml::Value>), | ||
| 84 | } | ||
| 85 | |||
| 86 | type Deps = std::collections::BTreeMap<String, Dep>; | ||
| 87 | |||
| 88 | #[derive(Debug, Clone, Deserialize)] | ||
| 89 | struct CrateConfig { | ||
| 90 | features: Option<Vec<String>>, | ||
| 91 | target: Option<String>, | ||
| 92 | } | ||
| 93 | |||
| 94 | type ReleaseConfig = HashMap<String, CrateConfig>; | ||
| 95 | |||
| 96 | fn load_release_config(repo: &Path) -> ReleaseConfig { | ||
| 97 | let config_path = repo.join("release/config.toml"); | ||
| 98 | if !config_path.exists() { | ||
| 99 | return HashMap::new(); | ||
| 100 | } | ||
| 101 | let content = fs::read_to_string(&config_path).expect("Failed to read release/config.toml"); | ||
| 102 | toml::from_str(&content).expect("Invalid TOML format in release/config.toml") | ||
| 103 | } | ||
| 104 | |||
| 105 | fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { | ||
| 106 | let path = &c.path; | ||
| 107 | c.id.version = new_version.to_string(); | ||
| 108 | let content = fs::read_to_string(&path)?; | ||
| 109 | let mut doc: DocumentMut = content.parse()?; | ||
| 110 | for section in ["package"] { | ||
| 111 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 112 | dep_table.insert("version", Item::Value(Value::from(new_version))); | ||
| 113 | } | ||
| 114 | } | ||
| 115 | fs::write(&path, doc.to_string())?; | ||
| 116 | Ok(()) | ||
| 117 | } | ||
| 118 | |||
| 119 | fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> { | ||
| 120 | let path = &to_update.path; | ||
| 121 | let content = fs::read_to_string(&path)?; | ||
| 122 | let mut doc: DocumentMut = content.parse()?; | ||
| 123 | let mut changed = false; | ||
| 124 | for section in ["dependencies", "dev-dependencies", "build-dependencies"] { | ||
| 125 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 126 | if let Some(item) = dep_table.get_mut(&dep.name) { | ||
| 127 | match item { | ||
| 128 | // e.g., foo = "0.1.0" | ||
| 129 | Item::Value(Value::String(_)) => { | ||
| 130 | *item = Item::Value(Value::from(new_version)); | ||
| 131 | changed = true; | ||
| 132 | } | ||
| 133 | // e.g., foo = { version = "...", ... } | ||
| 134 | Item::Value(Value::InlineTable(inline)) => { | ||
| 135 | if inline.contains_key("version") { | ||
| 136 | inline["version"] = Value::from(new_version); | ||
| 137 | changed = true; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | _ => {} // Leave unusual formats untouched | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | if changed { | ||
| 147 | fs::write(&path, doc.to_string())?; | ||
| 148 | println!("🔧 Updated {} to {} in {}", dep.name, new_version, path.display()); | ||
| 149 | } | ||
| 150 | Ok(()) | ||
| 151 | } | ||
| 152 | |||
| 153 | #[derive(Debug, Clone)] | ||
| 154 | struct Crate { | ||
| 155 | id: CrateId, | ||
| 156 | path: PathBuf, | ||
| 157 | config: CrateConfig, | ||
| 158 | dependencies: Vec<CrateId>, | ||
| 159 | } | ||
| 160 | |||
| 161 | #[derive(Debug, Clone, PartialOrd, Ord)] | ||
| 162 | struct CrateId { | ||
| 163 | name: String, | ||
| 164 | version: String, | ||
| 165 | } | ||
| 166 | |||
| 167 | impl PartialEq for CrateId { | ||
| 168 | fn eq(&self, other: &CrateId) -> bool { | ||
| 169 | self.name == other.name | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | impl Eq for CrateId {} | ||
| 174 | impl std::hash::Hash for CrateId { | ||
| 175 | fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||
| 176 | self.name.hash(state) | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | fn list_crates(path: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> { | ||
| 181 | let d = std::fs::read_dir(path)?; | ||
| 182 | let release_config = load_release_config(path); | ||
| 183 | let mut crates = BTreeMap::new(); | ||
| 184 | for c in d { | ||
| 185 | let entry = c?; | ||
| 186 | let name = entry.file_name().to_str().unwrap().to_string(); | ||
| 187 | if entry.file_type()?.is_dir() && name.starts_with("embassy-") { | ||
| 188 | let entry = entry.path().join("Cargo.toml"); | ||
| 189 | if entry.exists() { | ||
| 190 | let content = fs::read_to_string(&entry).unwrap_or_else(|_| { | ||
| 191 | panic!("Failed to read {:?}", entry); | ||
| 192 | }); | ||
| 193 | let parsed: CargoToml = toml::from_str(&content).unwrap_or_else(|e| { | ||
| 194 | panic!("Failed to parse {:?}: {}", entry, e); | ||
| 195 | }); | ||
| 196 | let p = parsed.package.unwrap(); | ||
| 197 | let id = CrateId { | ||
| 198 | name: p.name.clone(), | ||
| 199 | version: p.version.unwrap(), | ||
| 200 | }; | ||
| 201 | |||
| 202 | let mut dependencies = Vec::new(); | ||
| 203 | if let Some(deps) = parsed.dependencies { | ||
| 204 | for (k, v) in deps { | ||
| 205 | if k.starts_with("embassy-") { | ||
| 206 | dependencies.push(CrateId { | ||
| 207 | name: k, | ||
| 208 | version: match v { | ||
| 209 | Dep::Version(v) => v, | ||
| 210 | Dep::DetailedTable(table) => { | ||
| 211 | table.get("version").unwrap().as_str().unwrap().to_string() | ||
| 212 | } | ||
| 213 | }, | ||
| 214 | }); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | let path = path.join(entry); | ||
| 220 | if let Some(config) = release_config.get(&p.name) { | ||
| 221 | crates.insert( | ||
| 222 | id.clone(), | ||
| 223 | Crate { | ||
| 224 | id, | ||
| 225 | path, | ||
| 226 | dependencies, | ||
| 227 | config: config.clone(), | ||
| 228 | }, | ||
| 229 | ); | ||
| 230 | } | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | Ok(crates) | ||
| 235 | } | ||
| 236 | |||
| 237 | fn build_graph(crates: &BTreeMap<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) { | ||
| 238 | let mut graph = Graph::<CrateId, (), Directed>::new(); | ||
| 239 | let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new(); | ||
| 240 | |||
| 241 | // Helper to insert or get existing node | ||
| 242 | let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| { | ||
| 243 | if let Some(&idx) = map.get(&id) { | ||
| 244 | idx | ||
| 245 | } else { | ||
| 246 | let idx = graph.add_node(id.clone()); | ||
| 247 | map.insert(id, idx); | ||
| 248 | idx | ||
| 249 | } | ||
| 250 | }; | ||
| 251 | |||
| 252 | for krate in crates.values() { | ||
| 253 | get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); | ||
| 254 | } | ||
| 255 | |||
| 256 | for krate in crates.values() { | ||
| 257 | // Insert crate node if not exists | ||
| 258 | let crate_idx = get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); | ||
| 259 | |||
| 260 | // Insert dependencies and connect edges | ||
| 261 | for dep in krate.dependencies.iter() { | ||
| 262 | let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices); | ||
| 263 | graph.add_edge(crate_idx, dep_idx, ()); | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | (graph, node_indices) | ||
| 268 | } | ||
| 269 | |||
| 270 | fn main() -> Result<()> { | ||
| 271 | let args = Args::parse(); | ||
| 272 | |||
| 273 | let root = args.repo.canonicalize()?; //.expect("Invalid root crate path"); | ||
| 274 | let mut crates = list_crates(&root)?; | ||
| 275 | //println!("Crates: {:?}", crates); | ||
| 276 | let (mut graph, indices) = build_graph(&crates); | ||
| 277 | |||
| 278 | // use petgraph::dot::{Config, Dot}; | ||
| 279 | // println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); | ||
| 280 | |||
| 281 | match args.command { | ||
| 282 | Command::List => { | ||
| 283 | let ordered = petgraph::algo::toposort(&graph, None).unwrap(); | ||
| 284 | for node in ordered.iter() { | ||
| 285 | if graph.neighbors_directed(*node, Direction::Incoming).count() == 0 { | ||
| 286 | let start = graph.node_weight(*node).unwrap(); | ||
| 287 | let mut bfs = Bfs::new(&graph, *node); | ||
| 288 | while let Some(node) = bfs.next(&graph) { | ||
| 289 | let weight = graph.node_weight(node).unwrap(); | ||
| 290 | if weight.name == start.name { | ||
| 291 | println!("+ {}-{}", weight.name, weight.version); | ||
| 292 | } else { | ||
| 293 | println!("|- {}-{}", weight.name, weight.version); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | println!(""); | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } | ||
| 300 | Command::Dependencies { crate_name } => { | ||
| 301 | let idx = indices | ||
| 302 | .get(&CrateId { | ||
| 303 | name: crate_name.clone(), | ||
| 304 | version: "".to_string(), | ||
| 305 | }) | ||
| 306 | .expect("unable to find crate in tree"); | ||
| 307 | let mut bfs = Bfs::new(&graph, *idx); | ||
| 308 | while let Some(node) = bfs.next(&graph) { | ||
| 309 | let weight = graph.node_weight(node).unwrap(); | ||
| 310 | if weight.name == crate_name { | ||
| 311 | println!("+ {}-{}", weight.name, weight.version); | ||
| 312 | } else { | ||
| 313 | println!("|- {}-{}", weight.name, weight.version); | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 317 | Command::Dependents { crate_name } => { | ||
| 318 | let idx = indices | ||
| 319 | .get(&CrateId { | ||
| 320 | name: crate_name.clone(), | ||
| 321 | version: "".to_string(), | ||
| 322 | }) | ||
| 323 | .expect("unable to find crate in tree"); | ||
| 324 | let node = graph.node_weight(*idx).unwrap(); | ||
| 325 | println!("+ {}-{}", node.name, node.version); | ||
| 326 | for parent in graph.neighbors_directed(*idx, Direction::Incoming) { | ||
| 327 | let weight = graph.node_weight(parent).unwrap(); | ||
| 328 | println!("|- {}-{}", weight.name, weight.version); | ||
| 329 | } | ||
| 330 | } | ||
| 331 | Command::SemverCheck { crate_name } => { | ||
| 332 | let c = crates | ||
| 333 | .get(&CrateId { | ||
| 334 | name: crate_name.to_string(), | ||
| 335 | version: "".to_string(), | ||
| 336 | }) | ||
| 337 | .unwrap(); | ||
| 338 | check_semver(&c)?; | ||
| 339 | } | ||
| 340 | Command::PrepareRelease { crate_name } => { | ||
| 341 | let start = indices | ||
| 342 | .get(&CrateId { | ||
| 343 | name: crate_name.clone(), | ||
| 344 | version: "".to_string(), | ||
| 345 | }) | ||
| 346 | .expect("unable to find crate in tree"); | ||
| 347 | |||
| 348 | graph.reverse(); | ||
| 349 | |||
| 350 | let mut bfs = Bfs::new(&graph, *start); | ||
| 351 | |||
| 352 | while let Some(node) = bfs.next(&graph) { | ||
| 353 | let weight = graph.node_weight(node).unwrap(); | ||
| 354 | println!("Preparing {}", weight.name); | ||
| 355 | let mut c = crates.get_mut(weight).unwrap(); | ||
| 356 | let ver = semver::Version::parse(&c.id.version)?; | ||
| 357 | let newver = if let Err(_) = check_semver(&c) { | ||
| 358 | println!("Semver check failed, bumping minor!"); | ||
| 359 | semver::Version::new(ver.major, ver.minor + 1, 0) | ||
| 360 | } else { | ||
| 361 | semver::Version::new(ver.major, ver.minor, ver.patch + 1) | ||
| 362 | }; | ||
| 363 | |||
| 364 | println!( | ||
| 365 | "Updating {} from {} -> {}", | ||
| 366 | weight.name, | ||
| 367 | c.id.version, | ||
| 368 | newver.to_string() | ||
| 369 | ); | ||
| 370 | let newver = newver.to_string(); | ||
| 371 | |||
| 372 | update_version(&mut c, &newver)?; | ||
| 373 | let c = crates.get(weight).unwrap(); | ||
| 374 | |||
| 375 | // Update all nodes further down the tree | ||
| 376 | let mut bfs = Bfs::new(&graph, node); | ||
| 377 | while let Some(dep_node) = bfs.next(&graph) { | ||
| 378 | let dep_weight = graph.node_weight(dep_node).unwrap(); | ||
| 379 | let dep = crates.get(dep_weight).unwrap(); | ||
| 380 | update_versions(dep, &c.id, &newver)?; | ||
| 381 | } | ||
| 382 | |||
| 383 | // Update changelog | ||
| 384 | update_changelog(&root, &c)?; | ||
| 385 | } | ||
| 386 | |||
| 387 | let weight = graph.node_weight(*start).unwrap(); | ||
| 388 | let c = crates.get(weight).unwrap(); | ||
| 389 | publish_release(&root, &c, false)?; | ||
| 390 | |||
| 391 | println!("# Please inspect changes and run the following commands when happy:"); | ||
| 392 | |||
| 393 | println!("git commit -a -m 'chore: prepare crate releases'"); | ||
| 394 | let mut bfs = Bfs::new(&graph, *start); | ||
| 395 | while let Some(node) = bfs.next(&graph) { | ||
| 396 | let weight = graph.node_weight(node).unwrap(); | ||
| 397 | println!("git tag {}-v{}", weight.name, weight.version); | ||
| 398 | } | ||
| 399 | |||
| 400 | println!(""); | ||
| 401 | println!("# Run these commands to publish the crate and dependents:"); | ||
| 402 | |||
| 403 | let mut bfs = Bfs::new(&graph, *start); | ||
| 404 | while let Some(node) = bfs.next(&graph) { | ||
| 405 | let weight = graph.node_weight(node).unwrap(); | ||
| 406 | let c = crates.get(weight).unwrap(); | ||
| 407 | |||
| 408 | let mut args: Vec<String> = vec![ | ||
| 409 | "publish".to_string(), | ||
| 410 | "--manifest-path".to_string(), | ||
| 411 | c.path.display().to_string(), | ||
| 412 | ]; | ||
| 413 | |||
| 414 | if let Some(features) = &c.config.features { | ||
| 415 | args.push("--features".into()); | ||
| 416 | args.push(features.join(",")); | ||
| 417 | } | ||
| 418 | |||
| 419 | if let Some(target) = &c.config.target { | ||
| 420 | args.push("--target".into()); | ||
| 421 | args.push(target.clone()); | ||
| 422 | } | ||
| 423 | |||
| 424 | /* | ||
| 425 | let mut dry_run = args.clone(); | ||
| 426 | dry_run.push("--dry-run".to_string()); | ||
| 427 | |||
| 428 | println!("cargo {}", dry_run.join(" ")); | ||
| 429 | */ | ||
| 430 | println!("cargo {}", args.join(" ")); | ||
| 431 | } | ||
| 432 | |||
| 433 | println!(""); | ||
| 434 | println!("# Run this command to push changes and tags:"); | ||
| 435 | println!("git push --tags"); | ||
| 436 | } | ||
| 437 | } | ||
| 438 | Ok(()) | ||
| 439 | } | ||
| 440 | |||
| 441 | fn check_semver(c: &Crate) -> Result<()> { | ||
| 442 | let mut args: Vec<String> = vec![ | ||
| 443 | "semver-checks".to_string(), | ||
| 444 | "--manifest-path".to_string(), | ||
| 445 | c.path.display().to_string(), | ||
| 446 | "--default-features".to_string(), | ||
| 447 | ]; | ||
| 448 | if let Some(features) = &c.config.features { | ||
| 449 | args.push("--features".into()); | ||
| 450 | args.push(features.join(",")); | ||
| 451 | } | ||
| 452 | |||
| 453 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 454 | |||
| 455 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 456 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 457 | if !status.status.success() { | ||
| 458 | return Err(anyhow!("semver check failed")); | ||
| 459 | } else { | ||
| 460 | Ok(()) | ||
| 461 | } | ||
| 462 | } | ||
| 463 | |||
| 464 | fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { | ||
| 465 | let args: Vec<String> = vec![ | ||
| 466 | "release".to_string(), | ||
| 467 | "replace".to_string(), | ||
| 468 | "--config".to_string(), | ||
| 469 | repo.join("release").join("release.toml").display().to_string(), | ||
| 470 | "--manifest-path".to_string(), | ||
| 471 | c.path.display().to_string(), | ||
| 472 | "--execute".to_string(), | ||
| 473 | "--no-confirm".to_string(), | ||
| 474 | ]; | ||
| 475 | |||
| 476 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 477 | |||
| 478 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 479 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 480 | if !status.status.success() { | ||
| 481 | return Err(anyhow!("release replace failed")); | ||
| 482 | } else { | ||
| 483 | Ok(()) | ||
| 484 | } | ||
| 485 | } | ||
| 486 | |||
| 487 | fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { | ||
| 488 | let mut args: Vec<String> = vec![ | ||
| 489 | "publish".to_string(), | ||
| 490 | "--manifest-path".to_string(), | ||
| 491 | c.path.display().to_string(), | ||
| 492 | ]; | ||
| 493 | |||
| 494 | if let Some(features) = &c.config.features { | ||
| 495 | args.push("--features".into()); | ||
| 496 | args.push(features.join(",")); | ||
| 497 | } | ||
| 498 | |||
| 499 | if let Some(target) = &c.config.target { | ||
| 500 | args.push("--target".into()); | ||
| 501 | args.push(target.clone()); | ||
| 502 | } | ||
| 503 | |||
| 504 | if !push { | ||
| 505 | args.push("--dry-run".to_string()); | ||
| 506 | args.push("--allow-dirty".to_string()); | ||
| 507 | args.push("--keep-going".to_string()); | ||
| 508 | } | ||
| 509 | |||
| 510 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 511 | |||
| 512 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 513 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 514 | if !status.status.success() { | ||
| 515 | return Err(anyhow!("publish failed")); | ||
| 516 | } else { | ||
| 517 | Ok(()) | ||
| 518 | } | ||
| 519 | } | ||
