aboutsummaryrefslogtreecommitdiff
path: root/release/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'release/src/main.rs')
-rw-r--r--release/src/main.rs514
1 files changed, 0 insertions, 514 deletions
diff --git a/release/src/main.rs b/release/src/main.rs
deleted file mode 100644
index 22c926b86..000000000
--- a/release/src/main.rs
+++ /dev/null
@@ -1,514 +0,0 @@
1use std::collections::{BTreeMap, HashMap};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command as ProcessCommand;
5
6use anyhow::{anyhow, bail, Result};
7use cargo_semver_checks::ReleaseType;
8use clap::{Parser, Subcommand};
9use petgraph::graph::{Graph, NodeIndex};
10use petgraph::visit::Bfs;
11use petgraph::{Directed, Direction};
12use simple_logger::SimpleLogger;
13use toml_edit::{DocumentMut, Item, Value};
14use types::*;
15
16fn check_publish_dependencies(ctx: &Context) -> Result<()> {
17 for krate in ctx.crates.values() {
18 if krate.publish {
19 for dep_name in &krate.dependencies {
20 if let Some(dep_crate) = ctx.crates.get(dep_name) {
21 if !dep_crate.publish {
22 return Err(anyhow!(
23 "Publishable crate '{}' depends on non-publishable crate '{}'. This is not allowed.",
24 krate.name,
25 dep_name
26 ));
27 }
28 }
29 }
30 }
31 }
32 Ok(())
33}
34
35mod build;
36mod cargo;
37mod semver_check;
38mod types;
39
40/// Tool to traverse and operate on intra-repo Rust crate dependencies
41#[derive(Parser, Debug)]
42#[command(author, version, about)]
43struct Args {
44 /// Command to perform on each crate
45 #[command(subcommand)]
46 command: Command,
47}
48
49#[derive(Debug, Subcommand)]
50enum Command {
51 /// All crates and their direct dependencies
52 List,
53 /// List all dependencies for a crate
54 Dependencies {
55 /// Crate name to print dependencies for.
56 #[arg(value_name = "CRATE")]
57 crate_name: String,
58 },
59 /// List all dependencies for a crate
60 Dependents {
61 /// Crate name to print dependencies for.
62 #[arg(value_name = "CRATE")]
63 crate_name: String,
64 },
65
66 /// Build
67 Build {
68 /// Crate to check. If not specified checks all crates.
69 #[arg(value_name = "CRATE")]
70 crate_name: Option<String>,
71 },
72 /// SemverCheck
73 SemverCheck {
74 /// Specific crate name to check
75 #[arg(value_name = "CRATE")]
76 crate_name: String,
77 },
78 /// Prepare to release a crate and all dependents that needs updating
79 /// - Semver checks
80 /// - Bump versions and commit
81 /// - Create tag.
82 PrepareRelease {
83 /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates.
84 #[arg(value_name = "CRATE")]
85 crate_name: String,
86 },
87}
88
89fn update_version(c: &mut Crate, new_version: &str) -> Result<()> {
90 let path = c.path.join("Cargo.toml");
91 c.version = new_version.to_string();
92 let content = fs::read_to_string(&path)?;
93 let mut doc: DocumentMut = content.parse()?;
94 for section in ["package"] {
95 if let Some(Item::Table(dep_table)) = doc.get_mut(section) {
96 dep_table.insert("version", Item::Value(Value::from(new_version)));
97 }
98 }
99 fs::write(&path, doc.to_string())?;
100 Ok(())
101}
102
103fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> {
104 let path = to_update.path.join("Cargo.toml");
105 let content = fs::read_to_string(&path)?;
106 let mut doc: DocumentMut = content.parse()?;
107 let mut changed = false;
108 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
109 if let Some(Item::Table(dep_table)) = doc.get_mut(section) {
110 if let Some(item) = dep_table.get_mut(&dep) {
111 match item {
112 // e.g., foo = "0.1.0"
113 Item::Value(Value::String(_)) => {
114 *item = Item::Value(Value::from(new_version));
115 changed = true;
116 }
117 // e.g., foo = { version = "...", ... }
118 Item::Value(Value::InlineTable(inline)) => {
119 if inline.contains_key("version") {
120 inline["version"] = Value::from(new_version);
121 changed = true;
122 }
123 }
124 _ => {} // Leave unusual formats untouched
125 }
126 }
127 }
128 }
129
130 if changed {
131 fs::write(&path, doc.to_string())?;
132 println!("🔧 Updated {} to {} in {}", dep, new_version, path.display());
133 }
134 Ok(())
135}
136
137fn list_crates(root: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> {
138 let mut crates = BTreeMap::new();
139 discover_crates(root, &mut crates)?;
140 Ok(crates)
141}
142
143fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap<CrateId, Crate>) -> Result<()> {
144 let d = std::fs::read_dir(dir)?;
145 for c in d {
146 let entry = c?;
147 if entry.file_type()?.is_dir() {
148 let path = dir.join(entry.path());
149 let cargo_toml = path.join("Cargo.toml");
150
151 if cargo_toml.exists() {
152 let content = fs::read_to_string(&cargo_toml)?;
153
154 // Try to parse as a crate, skip if it's a workspace
155 let parsed: Result<ParsedCrate, _> = toml::from_str(&content);
156 if let Ok(parsed) = parsed {
157 let id = parsed.package.name;
158
159 let metadata = &parsed.package.metadata.embassy;
160
161 if metadata.skip {
162 continue;
163 }
164
165 let mut dependencies = Vec::new();
166 for (k, _) in parsed.dependencies {
167 if k.starts_with("embassy-") {
168 dependencies.push(k);
169 }
170 }
171
172 let mut configs = metadata.build.clone();
173 if configs.is_empty() {
174 configs.push(BuildConfig {
175 features: vec![],
176 target: None,
177 artifact_dir: None,
178 })
179 }
180
181 crates.insert(
182 id.clone(),
183 Crate {
184 name: id,
185 version: parsed.package.version,
186 path,
187 dependencies,
188 configs,
189 publish: parsed.package.publish,
190 },
191 );
192 }
193 } else {
194 // Recursively search subdirectories, but only for examples, tests, and docs
195 let file_name = entry.file_name();
196 let dir_name = file_name.to_string_lossy();
197 if dir_name == "examples" || dir_name == "tests" || dir_name == "docs" {
198 discover_crates(&path, crates)?;
199 }
200 }
201 }
202 }
203 Ok(())
204}
205
206fn build_graph(crates: &BTreeMap<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) {
207 let mut graph = Graph::<CrateId, (), Directed>::new();
208 let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new();
209
210 // Helper to insert or get existing node
211 let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| {
212 if let Some(&idx) = map.get(&id) {
213 idx
214 } else {
215 let idx = graph.add_node(id.clone());
216 map.insert(id, idx);
217 idx
218 }
219 };
220
221 for krate in crates.values() {
222 get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices);
223 }
224
225 for krate in crates.values() {
226 // Insert crate node if not exists
227 let crate_idx = get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices);
228
229 // Insert dependencies and connect edges
230 for dep in krate.dependencies.iter() {
231 let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices);
232 graph.add_edge(crate_idx, dep_idx, ());
233 }
234 }
235
236 (graph, node_indices)
237}
238
239struct Context {
240 root: PathBuf,
241 crates: BTreeMap<String, Crate>,
242 graph: Graph<String, ()>,
243 indices: HashMap<String, NodeIndex>,
244}
245
246fn find_repo_root() -> Result<PathBuf> {
247 let mut path = std::env::current_dir()?.canonicalize()?;
248
249 loop {
250 // Check if this directory contains a .git directory
251 if path.join(".git").exists() {
252 return Ok(path);
253 }
254
255 // Move to parent directory
256 match path.parent() {
257 Some(parent) => path = parent.to_path_buf(),
258 None => break,
259 }
260 }
261
262 Err(anyhow!(
263 "Could not find repository root. Make sure you're running this tool from within the embassy repository."
264 ))
265}
266
267fn load_context() -> Result<Context> {
268 let root = find_repo_root()?;
269 let crates = list_crates(&root)?;
270 let (graph, indices) = build_graph(&crates);
271
272 let ctx = Context {
273 root,
274 crates,
275 graph,
276 indices,
277 };
278
279 // Check for publish dependency conflicts
280 check_publish_dependencies(&ctx)?;
281
282 Ok(ctx)
283}
284
285fn main() -> Result<()> {
286 SimpleLogger::new().init().unwrap();
287 let args = Args::parse();
288 let mut ctx = load_context()?;
289
290 match args.command {
291 Command::List => {
292 let ordered = petgraph::algo::toposort(&ctx.graph, None).unwrap();
293 for node in ordered.iter() {
294 let start = ctx.graph.node_weight(*node).unwrap();
295 let mut bfs = Bfs::new(&ctx.graph, *node);
296 while let Some(node) = bfs.next(&ctx.graph) {
297 let weight = ctx.graph.node_weight(node).unwrap();
298 let c = ctx.crates.get(weight).unwrap();
299 if weight == start {
300 println!("+ {}-{}", weight, c.version);
301 } else {
302 println!("|- {}-{}", weight, c.version);
303 }
304 }
305 println!("");
306 }
307 }
308 Command::Dependencies { crate_name } => {
309 let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
310 let mut bfs = Bfs::new(&ctx.graph, *idx);
311 while let Some(node) = bfs.next(&ctx.graph) {
312 let weight = ctx.graph.node_weight(node).unwrap();
313 let crt = ctx.crates.get(weight).unwrap();
314 if *weight == crate_name {
315 println!("+ {}-{}", weight, crt.version);
316 } else {
317 println!("|- {}-{}", weight, crt.version);
318 }
319 }
320 }
321 Command::Dependents { crate_name } => {
322 let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
323 let weight = ctx.graph.node_weight(*idx).unwrap();
324 let crt = ctx.crates.get(weight).unwrap();
325 println!("+ {}-{}", weight, crt.version);
326 for parent in ctx.graph.neighbors_directed(*idx, Direction::Incoming) {
327 let weight = ctx.graph.node_weight(parent).unwrap();
328 let crt = ctx.crates.get(weight).unwrap();
329 println!("|- {}-{}", weight, crt.version);
330 }
331 }
332 Command::Build { crate_name } => {
333 build::build(&ctx, crate_name.as_deref())?;
334 }
335 Command::SemverCheck { crate_name } => {
336 let c = ctx.crates.get(&crate_name).unwrap();
337 if !c.publish {
338 bail!("Cannot run semver-check on non-publishable crate '{}'", crate_name);
339 }
340 check_semver(&c)?;
341 }
342 Command::PrepareRelease { crate_name } => {
343 let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
344
345 // Check if the target crate is publishable
346 let start_weight = ctx.graph.node_weight(*start).unwrap();
347 let start_crate = ctx.crates.get(start_weight).unwrap();
348 if !start_crate.publish {
349 bail!("Cannot prepare release for non-publishable crate '{}'", crate_name);
350 }
351
352 let mut rgraph = ctx.graph.clone();
353 rgraph.reverse();
354
355 let mut bfs = Bfs::new(&rgraph, *start);
356
357 while let Some(node) = bfs.next(&rgraph) {
358 let weight = rgraph.node_weight(node).unwrap();
359 println!("Preparing {}", weight);
360 let mut c = ctx.crates.get_mut(weight).unwrap();
361 if c.publish {
362 let ver = semver::Version::parse(&c.version)?;
363 let newver = match check_semver(&c)? {
364 ReleaseType::Major | ReleaseType::Minor => semver::Version::new(ver.major, ver.minor + 1, 0),
365 ReleaseType::Patch => semver::Version::new(ver.major, ver.minor, ver.patch + 1),
366 _ => unreachable!(),
367 };
368
369 println!("Updating {} from {} -> {}", weight, c.version, newver.to_string());
370 let newver = newver.to_string();
371
372 update_version(&mut c, &newver)?;
373 let c = ctx.crates.get(weight).unwrap();
374
375 // Update all nodes further down the tree
376 let mut bfs = Bfs::new(&rgraph, node);
377 while let Some(dep_node) = bfs.next(&rgraph) {
378 let dep_weight = rgraph.node_weight(dep_node).unwrap();
379 let dep = ctx.crates.get(dep_weight).unwrap();
380 update_versions(dep, &c.name, &newver)?;
381 }
382
383 // Update changelog
384 update_changelog(&ctx.root, &c)?;
385 }
386 }
387
388 let weight = rgraph.node_weight(*start).unwrap();
389 let c = ctx.crates.get(weight).unwrap();
390 publish_release(&ctx.root, &c, false)?;
391
392 println!("# Please inspect changes and run the following commands when happy:");
393
394 println!("git commit -a -m 'chore: prepare crate releases'");
395 let mut bfs = Bfs::new(&rgraph, *start);
396 while let Some(node) = bfs.next(&rgraph) {
397 let weight = rgraph.node_weight(node).unwrap();
398 let c = ctx.crates.get(weight).unwrap();
399 if c.publish {
400 println!("git tag {}-v{}", weight, c.version);
401 }
402 }
403
404 println!("");
405 println!("# Run these commands to publish the crate and dependents:");
406
407 let mut bfs = Bfs::new(&rgraph, *start);
408 while let Some(node) = bfs.next(&rgraph) {
409 let weight = rgraph.node_weight(node).unwrap();
410 let c = ctx.crates.get(weight).unwrap();
411
412 let mut args: Vec<String> = vec![
413 "publish".to_string(),
414 "--manifest-path".to_string(),
415 c.path.join("Cargo.toml").display().to_string(),
416 ];
417
418 let config = c.configs.first().unwrap(); // TODO
419 if !config.features.is_empty() {
420 args.push("--features".into());
421 args.push(config.features.join(","));
422 }
423
424 if let Some(target) = &config.target {
425 args.push("--target".into());
426 args.push(target.clone());
427 }
428
429 /*
430 let mut dry_run = args.clone();
431 dry_run.push("--dry-run".to_string());
432
433 println!("cargo {}", dry_run.join(" "));
434 */
435 if c.publish {
436 println!("cargo {}", args.join(" "));
437 }
438 }
439
440 println!("");
441 println!("# Run this command to push changes and tags:");
442 println!("git push --tags");
443 }
444 }
445 Ok(())
446}
447
448fn check_semver(c: &Crate) -> Result<ReleaseType> {
449 let min_version = semver_check::minimum_update(c)?;
450 println!("Version should be bumped to {:?}", min_version);
451 Ok(min_version)
452}
453
454fn update_changelog(repo: &Path, c: &Crate) -> Result<()> {
455 let args: Vec<String> = vec![
456 "release".to_string(),
457 "replace".to_string(),
458 "--config".to_string(),
459 repo.join("release").join("release.toml").display().to_string(),
460 "--manifest-path".to_string(),
461 c.path.join("Cargo.toml").display().to_string(),
462 "--execute".to_string(),
463 "--no-confirm".to_string(),
464 ];
465
466 let status = ProcessCommand::new("cargo").args(&args).output()?;
467
468 println!("{}", core::str::from_utf8(&status.stdout).unwrap());
469 eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap());
470 if !status.status.success() {
471 return Err(anyhow!("release replace failed"));
472 } else {
473 Ok(())
474 }
475}
476
477fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> {
478 let config = c.configs.first().unwrap(); // TODO
479
480 let mut args: Vec<String> = vec![
481 "publish".to_string(),
482 "--manifest-path".to_string(),
483 c.path.join("Cargo.toml").display().to_string(),
484 ];
485
486 args.push("--features".into());
487 args.push(config.features.join(","));
488
489 if let Some(target) = &config.target {
490 args.push("--target".into());
491 args.push(target.clone());
492 }
493
494 if !push {
495 args.push("--dry-run".to_string());
496 args.push("--allow-dirty".to_string());
497 args.push("--keep-going".to_string());
498 }
499
500 let status = ProcessCommand::new("cargo").args(&args).output()?;
501
502 println!("{}", core::str::from_utf8(&status.stdout).unwrap());
503 eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap());
504 if !status.status.success() {
505 return Err(anyhow!("publish failed"));
506 } else {
507 Ok(())
508 }
509}
510
511/// Make the path "Windows"-safe
512pub fn windows_safe_path(path: &Path) -> PathBuf {
513 PathBuf::from(path.to_str().unwrap().to_string().replace("\\\\?\\", ""))
514}