diff options
| -rw-r--r-- | embassy-release/Cargo.toml | 12 | ||||
| -rw-r--r-- | embassy-release/src/main.rs | 303 | ||||
| -rw-r--r-- | release/config.toml | 1 |
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] | ||
| 2 | name = "embassy-release" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2021" | ||
| 5 | |||
| 6 | [dependencies] | ||
| 7 | clap = { version = "4.5.1", features = ["derive"] } | ||
| 8 | walkdir = "2.5.0" | ||
| 9 | toml = "0.8.8" | ||
| 10 | toml_edit = { version = "0.23.1", features = ["serde"] } | ||
| 11 | serde = { version = "1.0.198", features = ["derive"] } | ||
| 12 | regex = "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 @@ | |||
| 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/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" } | |||
