aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUlf Lilleengen <[email protected]>2025-07-15 11:04:36 +0200
committerDario Nieuwenhuis <[email protected]>2025-08-02 22:45:50 +0200
commit8843b08b90dcf983ebeb13fc09d2a8c0b6e48c10 (patch)
treeb5f12c3e752b15de00c8a66856645a46edfed2c9
parenta3aa1e86296810ecf107d19d708589cc6e11366c (diff)
feat: add embassy-release tool
* Print dependencies of a crate * Bump version in dependent crates * Release using cargo release * Config file to control features and target
-rw-r--r--embassy-release/Cargo.toml12
-rw-r--r--embassy-release/src/main.rs303
-rw-r--r--release/config.toml1
3 files changed, 316 insertions, 0 deletions
diff --git a/embassy-release/Cargo.toml b/embassy-release/Cargo.toml
new file mode 100644
index 000000000..de548e650
--- /dev/null
+++ b/embassy-release/Cargo.toml
@@ -0,0 +1,12 @@
1[package]
2name = "embassy-release"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7clap = { version = "4.5.1", features = ["derive"] }
8walkdir = "2.5.0"
9toml = "0.8.8"
10toml_edit = { version = "0.23.1", features = ["serde"] }
11serde = { version = "1.0.198", features = ["derive"] }
12regex = "1.10.4"
diff --git a/embassy-release/src/main.rs b/embassy-release/src/main.rs
new file mode 100644
index 000000000..321d3872c
--- /dev/null
+++ b/embassy-release/src/main.rs
@@ -0,0 +1,303 @@
1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command as ProcessCommand;
5
6use clap::{Parser, Subcommand, ValueEnum};
7use regex::Regex;
8use serde::Deserialize;
9use toml_edit::{DocumentMut, Item, Value};
10use walkdir::WalkDir;
11
12/// Tool to traverse and operate on intra-repo Rust crate dependencies
13#[derive(Parser, Debug)]
14#[command(author, version, about)]
15struct 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)]
30enum TraversalOrder {
31 Pre,
32 Post,
33}
34
35#[derive(Debug, Subcommand)]
36enum 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)]
48enum ReleaseKind {
49 Patch,
50 Minor,
51}
52
53#[derive(Debug, Deserialize)]
54struct CargoToml {
55 package: Option<Package>,
56 dependencies: Option<Deps>,
57}
58
59#[derive(Debug, Deserialize)]
60struct Package {
61 name: String,
62 version: Option<String>,
63}
64
65#[derive(Debug, Deserialize)]
66#[serde(untagged)]
67enum Dep {
68 Version(String),
69 DetailedTable(HashMap<String, toml::Value>),
70}
71
72type Deps = std::collections::HashMap<String, Dep>;
73
74#[derive(Debug, Deserialize)]
75struct CrateConfig {
76 features: Option<Vec<String>>,
77 target: Option<String>,
78}
79
80type ReleaseConfig = HashMap<String, CrateConfig>;
81
82fn 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
110fn 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
138fn 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
148fn 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
157fn 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
207fn 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
262fn 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/release/config.toml b/release/config.toml
new file mode 100644
index 000000000..2292f4077
--- /dev/null
+++ b/release/config.toml
@@ -0,0 +1 @@
embassy-rp = { features = ["defmt", "unstable-pac", "time-driver", "rp2040"], target = "thumbv6m-none-eabi" }