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 --- release/Cargo.toml | 15 ++ release/config.toml | 45 +++++ release/release.toml | 5 + release/src/main.rs | 519 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 584 insertions(+) create mode 100644 release/Cargo.toml create mode 100644 release/release.toml create mode 100644 release/src/main.rs (limited to 'release') 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 From 3d004734a2a1db07d0e990462bb3fd5f04d3c7a0 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Fri, 15 Aug 2025 09:39:32 +0200 Subject: chore: cleanup --- release/Cargo.toml | 3 +- release/src/main.rs | 184 ++++++++++++--------------------------------------- release/src/types.rs | 33 +++++++++ 3 files changed, 76 insertions(+), 144 deletions(-) create mode 100644 release/src/types.rs (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index cc1b155e2..52ca01c2e 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" [dependencies] clap = { version = "4.5.1", features = ["derive"] } walkdir = "2.5.0" -toml = "0.8.8" +toml = "0.9.5" 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" +cargo-semver-checks = "0.43.0" diff --git a/release/src/main.rs b/release/src/main.rs index 38bb728a8..769611c78 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -8,8 +8,10 @@ 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}; +use types::*; + +mod types; /// Tool to traverse and operate on intra-repo Rust crate dependencies #[derive(Parser, Debug)] @@ -58,41 +60,6 @@ enum Command { }, } -#[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() { @@ -104,7 +71,7 @@ fn load_release_config(repo: &Path) -> ReleaseConfig { fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { let path = &c.path; - c.id.version = new_version.to_string(); + c.version = new_version.to_string(); let content = fs::read_to_string(&path)?; let mut doc: DocumentMut = content.parse()?; for section in ["package"] { @@ -123,7 +90,7 @@ fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Resul 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) { + if let Some(item) = dep_table.get_mut(&dep) { match item { // e.g., foo = "0.1.0" Item::Value(Value::String(_)) => { @@ -145,38 +112,11 @@ fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Resul if changed { fs::write(&path, doc.to_string())?; - println!("🔧 Updated {} to {} in {}", dep.name, new_version, path.display()); + println!("🔧 Updated {} to {} in {}", dep, 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); @@ -187,41 +127,24 @@ fn list_crates(path: &PathBuf) -> Result> { 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 content = fs::read_to_string(&entry)?; + let parsed: ParsedCrate = toml::from_str(&content)?; + let id = parsed.package.name; 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() - } - }, - }); - } + for (k, _) in parsed.dependencies { + if k.starts_with("embassy-") { + dependencies.push(k); } } let path = path.join(entry); - if let Some(config) = release_config.get(&p.name) { + if let Some(config) = release_config.get(&id) { crates.insert( id.clone(), Crate { - id, + name: id, + version: parsed.package.version, path, dependencies, config: config.clone(), @@ -250,12 +173,12 @@ fn build_graph(crates: &BTreeMap) -> (Graph, HashMa }; for krate in crates.values() { - get_or_insert_node(krate.id.clone(), &mut graph, &mut node_indices); + get_or_insert_node(krate.name.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); + let crate_idx = get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices); // Insert dependencies and connect edges for dep in krate.dependencies.iter() { @@ -270,14 +193,10 @@ fn build_graph(crates: &BTreeMap) -> (Graph, HashMa fn main() -> Result<()> { let args = Args::parse(); - let root = args.repo.canonicalize()?; //.expect("Invalid root crate path"); + let root = args.repo.canonicalize()?; 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(); @@ -287,10 +206,11 @@ fn main() -> Result<()> { 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); + let c = crates.get(weight).unwrap(); + if weight == start { + println!("+ {}-{}", weight, c.version); } else { - println!("|- {}-{}", weight.name, weight.version); + println!("|- {}-{}", weight, c.version); } } println!(""); @@ -298,62 +218,44 @@ fn main() -> Result<()> { } } Command::Dependencies { crate_name } => { - let idx = indices - .get(&CrateId { - name: crate_name.clone(), - version: "".to_string(), - }) - .expect("unable to find crate in tree"); + let idx = indices.get(&crate_name).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); + let crt = crates.get(weight).unwrap(); + if *weight == crate_name { + println!("+ {}-{}", weight, crt.version); } else { - println!("|- {}-{}", weight.name, weight.version); + println!("|- {}-{}", weight, crt.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); + let idx = indices.get(&crate_name).expect("unable to find crate in tree"); + let weight = graph.node_weight(*idx).unwrap(); + let crt = crates.get(weight).unwrap(); + println!("+ {}-{}", weight, crt.version); for parent in graph.neighbors_directed(*idx, Direction::Incoming) { let weight = graph.node_weight(parent).unwrap(); - println!("|- {}-{}", weight.name, weight.version); + let crt = crates.get(weight).unwrap(); + println!("|- {}-{}", weight, crt.version); } } Command::SemverCheck { crate_name } => { - let c = crates - .get(&CrateId { - name: crate_name.to_string(), - version: "".to_string(), - }) - .unwrap(); + let c = crates.get(&crate_name).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"); - + let start = indices.get(&crate_name).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); + println!("Preparing {}", weight); let mut c = crates.get_mut(weight).unwrap(); - let ver = semver::Version::parse(&c.id.version)?; + let ver = semver::Version::parse(&c.version)?; let newver = if let Err(_) = check_semver(&c) { println!("Semver check failed, bumping minor!"); semver::Version::new(ver.major, ver.minor + 1, 0) @@ -361,12 +263,7 @@ fn main() -> Result<()> { semver::Version::new(ver.major, ver.minor, ver.patch + 1) }; - println!( - "Updating {} from {} -> {}", - weight.name, - c.id.version, - newver.to_string() - ); + println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); let newver = newver.to_string(); update_version(&mut c, &newver)?; @@ -377,7 +274,7 @@ fn main() -> Result<()> { 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_versions(dep, &c.name, &newver)?; } // Update changelog @@ -394,7 +291,8 @@ fn main() -> Result<()> { 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); + let c = crates.get(weight).unwrap(); + println!("git tag {}-v{}", weight, c.version); } println!(""); diff --git a/release/src/types.rs b/release/src/types.rs new file mode 100644 index 000000000..56a886e6f --- /dev/null +++ b/release/src/types.rs @@ -0,0 +1,33 @@ +use serde::Deserialize; +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; + +#[derive(Debug, Deserialize)] +pub struct ParsedCrate { + pub package: ParsedPackage, + pub dependencies: BTreeMap, +} + +#[derive(Debug, Deserialize)] +pub struct ParsedPackage { + pub name: String, + pub version: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CrateConfig { + pub features: Option>, + pub target: Option, +} + +pub type ReleaseConfig = HashMap; +pub type CrateId = String; + +#[derive(Debug, Clone)] +pub struct Crate { + pub name: String, + pub version: String, + pub path: PathBuf, + pub config: CrateConfig, + pub dependencies: Vec, +} -- cgit From 864d29bfe1475d52e7da6385ee8123b41184c833 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Fri, 15 Aug 2025 13:30:21 +0200 Subject: fix: update --- release/Cargo.toml | 7 ++ release/config.toml | 2 +- release/src/cargo.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++ release/src/main.rs | 52 +++++------- release/src/semver_check.rs | 110 +++++++++++++++++++++++++ 5 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 release/src/cargo.rs create mode 100644 release/src/semver_check.rs (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index 52ca01c2e..461021e0a 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -14,3 +14,10 @@ anyhow = "1" petgraph = "0.8.2" semver = "1.0.26" cargo-semver-checks = "0.43.0" +log = "0.4" +simple_logger = "5.0.0" +temp-file = "0.1.9" +flate2 = "1.1.1" + +[patch.crates-io] +cargo-semver-checks = { path = "../../cargo-semver-checks" } diff --git a/release/config.toml b/release/config.toml index 6b23217fa..f572b450c 100644 --- a/release/config.toml +++ b/release/config.toml @@ -27,7 +27,7 @@ embassy-net-ppp = { } embassy-net-esp-hosted = {} embassy-net-driver-channel = {} embassy-net-wiznet = {} -embassy-net-nrf91 = { features = ["defmt", "nrf9160"] } +embassy-net-nrf91 = { features = ["defmt", "nrf-pac/nrf9160"] } embassy-net-driver = {} embassy-net-tuntap = {} embassy-net-adin1110 = {} diff --git a/release/src/cargo.rs b/release/src/cargo.rs new file mode 100644 index 000000000..1a4f79f20 --- /dev/null +++ b/release/src/cargo.rs @@ -0,0 +1,194 @@ +//! Tools for working with Cargo. + +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::{bail, Context as _, Result}; +use clap::ValueEnum as _; +use serde::{Deserialize, Serialize}; +use toml_edit::{DocumentMut, Formatted, Item, Value}; + +use crate::{windows_safe_path, Crate}; + +#[derive(Clone, Debug, PartialEq)] +pub enum CargoAction { + Build(PathBuf), + Run, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Artifact { + pub executable: PathBuf, +} + +/// Execute cargo with the given arguments and from the specified directory. +pub fn run(args: &[String], cwd: &Path) -> Result<()> { + run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], false)?; + Ok(()) +} + +/// Execute cargo with the given arguments and from the specified directory. +pub fn run_with_env(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result +where + I: IntoIterator + core::fmt::Debug, + K: AsRef, + V: AsRef, +{ + if !cwd.is_dir() { + bail!("The `cwd` argument MUST be a directory"); + } + + // Make sure to not use a UNC as CWD! + // That would make `OUT_DIR` a UNC which will trigger things like the one fixed in https://github.com/dtolnay/rustversion/pull/51 + // While it's fixed in `rustversion` it's not fixed for other crates we are + // using now or in future! + let cwd = windows_safe_path(cwd); + + println!( + "Running `cargo {}` in {:?} - Environment {:?}", + args.join(" "), + cwd, + envs + ); + + let mut command = Command::new(get_cargo()); + + command + .args(args) + .current_dir(cwd) + .envs(envs) + .stdout(if capture { Stdio::piped() } else { Stdio::inherit() }) + .stderr(if capture { Stdio::piped() } else { Stdio::inherit() }); + + if args.iter().any(|a| a.starts_with('+')) { + // Make sure the right cargo runs + command.env_remove("CARGO"); + } + + let output = command.stdin(Stdio::inherit()).output()?; + + // Make sure that we return an appropriate exit code here, as Github Actions + // requires this in order to function correctly: + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + bail!("Failed to execute cargo subcommand `cargo {}`", args.join(" "),) + } +} + +fn get_cargo() -> String { + // On Windows when executed via `cargo run` (e.g. via the xtask alias) the + // `cargo` on the search path is NOT the cargo-wrapper but the `cargo` from the + // toolchain - that one doesn't understand `+toolchain` + #[cfg(target_os = "windows")] + let cargo = if let Ok(cargo) = std::env::var("CARGO_HOME") { + format!("{cargo}/bin/cargo") + } else { + String::from("cargo") + }; + + #[cfg(not(target_os = "windows"))] + let cargo = String::from("cargo"); + + cargo +} + +#[derive(Debug, Default)] +pub struct CargoArgsBuilder { + toolchain: Option, + subcommand: String, + target: Option, + features: Vec, + args: Vec, +} + +impl CargoArgsBuilder { + #[must_use] + pub fn toolchain(mut self, toolchain: S) -> Self + where + S: Into, + { + self.toolchain = Some(toolchain.into()); + self + } + + #[must_use] + pub fn subcommand(mut self, subcommand: S) -> Self + where + S: Into, + { + self.subcommand = subcommand.into(); + self + } + + #[must_use] + pub fn target(mut self, target: S) -> Self + where + S: Into, + { + self.target = Some(target.into()); + self + } + + #[must_use] + pub fn features(mut self, features: &[String]) -> Self { + self.features = features.to_vec(); + self + } + + #[must_use] + pub fn arg(mut self, arg: S) -> Self + where + S: Into, + { + self.args.push(arg.into()); + self + } + + #[must_use] + pub fn args(mut self, args: &[S]) -> Self + where + S: Clone + Into, + { + for arg in args { + self.args.push(arg.clone().into()); + } + self + } + + pub fn add_arg(&mut self, arg: S) -> &mut Self + where + S: Into, + { + self.args.push(arg.into()); + self + } + + #[must_use] + pub fn build(&self) -> Vec { + let mut args = vec![]; + + if let Some(ref toolchain) = self.toolchain { + args.push(format!("+{toolchain}")); + } + + args.push(self.subcommand.clone()); + + if let Some(ref target) = self.target { + args.push(format!("--target={target}")); + } + + if !self.features.is_empty() { + args.push(format!("--features={}", self.features.join(","))); + } + + for arg in self.args.iter() { + args.push(arg.clone()); + } + + args + } +} diff --git a/release/src/main.rs b/release/src/main.rs index 769611c78..0dbcc5d45 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -1,3 +1,4 @@ +use simple_logger::SimpleLogger; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::path::{Path, PathBuf}; @@ -11,6 +12,8 @@ use petgraph::{Directed, Direction}; use toml_edit::{DocumentMut, Item, Value}; use types::*; +mod cargo; +mod semver_check; mod types; /// Tool to traverse and operate on intra-repo Rust crate dependencies @@ -70,7 +73,7 @@ fn load_release_config(repo: &Path) -> ReleaseConfig { } fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { - let path = &c.path; + let path = c.path.join("Cargo.toml"); c.version = new_version.to_string(); let content = fs::read_to_string(&path)?; let mut doc: DocumentMut = content.parse()?; @@ -84,7 +87,7 @@ fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { } fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> { - let path = &to_update.path; + let path = to_update.path.join("Cargo.toml"); let content = fs::read_to_string(&path)?; let mut doc: DocumentMut = content.parse()?; let mut changed = false; @@ -117,15 +120,16 @@ fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Resul Ok(()) } -fn list_crates(path: &PathBuf) -> Result> { - let d = std::fs::read_dir(path)?; - let release_config = load_release_config(path); +fn list_crates(root: &PathBuf) -> Result> { + let d = std::fs::read_dir(root)?; + let release_config = load_release_config(root); 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"); + let path = root.join(entry.path()); + let entry = path.join("Cargo.toml"); if entry.exists() { let content = fs::read_to_string(&entry)?; let parsed: ParsedCrate = toml::from_str(&content)?; @@ -138,7 +142,6 @@ fn list_crates(path: &PathBuf) -> Result> { } } - let path = path.join(entry); if let Some(config) = release_config.get(&id) { crates.insert( id.clone(), @@ -191,6 +194,7 @@ fn build_graph(crates: &BTreeMap) -> (Graph, HashMa } fn main() -> Result<()> { + SimpleLogger::new().init().unwrap(); let args = Args::parse(); let root = args.repo.canonicalize()?; @@ -306,7 +310,7 @@ fn main() -> Result<()> { let mut args: Vec = vec![ "publish".to_string(), "--manifest-path".to_string(), - c.path.display().to_string(), + c.path.join("Cargo.toml").display().to_string(), ]; if let Some(features) = &c.config.features { @@ -337,26 +341,9 @@ fn main() -> Result<()> { } 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(()) - } + let min_version = semver_check::minimum_update(c)?; + println!("Version should be bumped to {:?}", min_version); + Ok(()) } fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { @@ -366,7 +353,7 @@ fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { "--config".to_string(), repo.join("release").join("release.toml").display().to_string(), "--manifest-path".to_string(), - c.path.display().to_string(), + c.path.join("Cargo.toml").display().to_string(), "--execute".to_string(), "--no-confirm".to_string(), ]; @@ -386,7 +373,7 @@ 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(), + c.path.join("Cargo.toml").display().to_string(), ]; if let Some(features) = &c.config.features { @@ -415,3 +402,8 @@ fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { Ok(()) } } + +/// Make the path "Windows"-safe +pub fn windows_safe_path(path: &Path) -> PathBuf { + PathBuf::from(path.to_str().unwrap().to_string().replace("\\\\?\\", "")) +} diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs new file mode 100644 index 000000000..b43164334 --- /dev/null +++ b/release/src/semver_check.rs @@ -0,0 +1,110 @@ +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc}; + +use crate::cargo::CargoArgsBuilder; +use crate::types::Crate; +use crate::windows_safe_path; + +/// Return the minimum required bump for the next release. +/// Even if nothing changed this will be [ReleaseType::Patch] +pub fn minimum_update(krate: &Crate) -> Result { + println!("Crate = {:?}", krate); + + let package_name = krate.name.clone(); + let package_path = krate.path.clone(); + let current_path = build_doc_json(krate)?; + + let baseline = Rustdoc::from_registry_latest_crate_version(); + let doc = Rustdoc::from_path(¤t_path); + let mut semver_check = Check::new(doc); + semver_check.with_default_features(); + semver_check.set_baseline(baseline); + semver_check.set_packages(vec![package_name]); + if let Some(features) = &krate.config.features { + let extra_current_features = features.clone(); + let extra_baseline_features = features.clone(); + semver_check.set_extra_features(extra_current_features, extra_baseline_features); + } + if let Some(target) = &krate.config.target { + semver_check.set_build_target(target.clone()); + } + let mut cfg = GlobalConfig::new(); + cfg.set_log_level(Some(log::Level::Trace)); + let result = semver_check.check_release(&mut cfg)?; + log::info!("Result {:?}", result); + + let mut min_required_update = ReleaseType::Patch; + for (_, report) in result.crate_reports() { + if let Some(required_bump) = report.required_bump() { + let required_is_stricter = + (min_required_update == ReleaseType::Patch) || (required_bump == ReleaseType::Major); + if required_is_stricter { + min_required_update = required_bump; + } + } + } + + Ok(min_required_update) +} + +pub(crate) fn build_doc_json(krate: &Crate) -> Result { + let target_dir = std::env::var("CARGO_TARGET_DIR"); + + let target_path = if let Ok(target) = target_dir { + PathBuf::from(target) + } else { + PathBuf::from(&krate.path).join("target") + }; + + let current_path = target_path; + let current_path = if let Some(target) = &krate.config.target { + current_path.join(target.clone()) + } else { + current_path + }; + let current_path = current_path + .join("doc") + .join(format!("{}.json", krate.name.to_string().replace("-", "_"))); + + std::fs::remove_file(¤t_path).ok(); + let features = if let Some(features) = &krate.config.features { + features.clone() + } else { + vec![] + }; + + log::info!("Building doc json for {} with features: {:?}", krate.name, features); + + let envs = vec![( + "RUSTDOCFLAGS", + "--cfg docsrs --cfg not_really_docsrs --cfg semver_checks", + )]; + + // always use `specific nightly` toolchain so we don't have to deal with potentially + // different versions of the doc-json + let cargo_builder = CargoArgsBuilder::default() + .toolchain("nightly-2025-06-29") + .subcommand("rustdoc") + .features(&features); + let cargo_builder = if let Some(target) = &krate.config.target { + cargo_builder.target(target.clone()) + } else { + cargo_builder + }; + + let cargo_builder = cargo_builder + .arg("-Zunstable-options") + .arg("-Zhost-config") + .arg("-Ztarget-applies-to-host") + .arg("--lib") + .arg("--output-format=json") + .arg("-Zbuild-std=alloc,core") + .arg("--config=host.rustflags=[\"--cfg=instability_disable_unstable_docs\"]"); + let cargo_args = cargo_builder.build(); + log::debug!("{cargo_args:#?}"); + crate::cargo::run_with_env(&cargo_args, &krate.path, envs, false)?; + Ok(current_path) +} -- cgit From a434401322e4e7af64ea7077ce2e4af685eac3d5 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Fri, 15 Aug 2025 14:28:02 +0200 Subject: fix: reference project dir --- release/src/semver_check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'release') diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index b43164334..7b2e50672 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -18,7 +18,7 @@ pub fn minimum_update(krate: &Crate) -> Result { let current_path = build_doc_json(krate)?; let baseline = Rustdoc::from_registry_latest_crate_version(); - let doc = Rustdoc::from_path(¤t_path); + let doc = Rustdoc::from_root(&package_path); let mut semver_check = Check::new(doc); semver_check.with_default_features(); semver_check.set_baseline(baseline); -- cgit From 7579ca4ac7de39a4d48cfa3dca0011ebe56eeb35 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Fri, 15 Aug 2025 14:29:13 +0200 Subject: fix: reenable --- release/config.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'release') diff --git a/release/config.toml b/release/config.toml index f572b450c..014c73a91 100644 --- a/release/config.toml +++ b/release/config.toml @@ -4,12 +4,12 @@ embassy-nrf = { features = ["nrf52840", "time", "defmt", "unstable-pac", "gpiote 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" } +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-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 = { } -- cgit From 9f12852c389d65a8b2e252e027f69dfef2383736 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 15:30:14 +0200 Subject: Read crate configs from metadata. --- release/Cargo.toml | 7 ++++-- release/config.toml | 46 ------------------------------------- release/src/main.rs | 55 ++++++++++++++++++--------------------------- release/src/semver_check.rs | 14 ++++-------- release/src/types.rs | 27 +++++++++++++++++----- 5 files changed, 53 insertions(+), 96 deletions(-) delete mode 100644 release/config.toml (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index 461021e0a..3e4094eed 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -19,5 +19,8 @@ simple_logger = "5.0.0" temp-file = "0.1.9" flate2 = "1.1.1" -[patch.crates-io] -cargo-semver-checks = { path = "../../cargo-semver-checks" } +#[patch.crates-io] +#cargo-semver-checks = { path = "../../cargo-semver-checks" } + +[package.metadata.embassy] +skip = true diff --git a/release/config.toml b/release/config.toml deleted file mode 100644 index 014c73a91..000000000 --- a/release/config.toml +++ /dev/null @@ -1,46 +0,0 @@ - -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", "nrf-pac/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/src/main.rs b/release/src/main.rs index 0dbcc5d45..a49d8f36c 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -1,4 +1,3 @@ -use simple_logger::SimpleLogger; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::path::{Path, PathBuf}; @@ -6,9 +5,11 @@ use std::process::Command as ProcessCommand; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; +use log::info; use petgraph::graph::{Graph, NodeIndex}; use petgraph::visit::Bfs; use petgraph::{Directed, Direction}; +use simple_logger::SimpleLogger; use toml_edit::{DocumentMut, Item, Value}; use types::*; @@ -63,15 +64,6 @@ enum Command { }, } -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.join("Cargo.toml"); c.version = new_version.to_string(); @@ -122,12 +114,10 @@ fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Resul fn list_crates(root: &PathBuf) -> Result> { let d = std::fs::read_dir(root)?; - let release_config = load_release_config(root); 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-") { + if entry.file_type()?.is_dir() { let path = root.join(entry.path()); let entry = path.join("Cargo.toml"); if entry.exists() { @@ -135,6 +125,8 @@ fn list_crates(root: &PathBuf) -> Result> { let parsed: ParsedCrate = toml::from_str(&content)?; let id = parsed.package.name; + let metadata = &parsed.package.metadata.embassy; + let mut dependencies = Vec::new(); for (k, _) in parsed.dependencies { if k.starts_with("embassy-") { @@ -142,18 +134,19 @@ fn list_crates(root: &PathBuf) -> Result> { } } - if let Some(config) = release_config.get(&id) { - crates.insert( - id.clone(), - Crate { - name: id, - version: parsed.package.version, - path, - dependencies, - config: config.clone(), - }, - ); - } + crates.insert( + id.clone(), + Crate { + name: id, + version: parsed.package.version, + path, + dependencies, + config: metadata.build.first().cloned().unwrap_or_else(|| BuildConfig { + features: vec![], + target: None, + }), + }, + ); } } } @@ -313,10 +306,8 @@ fn main() -> Result<()> { c.path.join("Cargo.toml").display().to_string(), ]; - if let Some(features) = &c.config.features { - args.push("--features".into()); - args.push(features.join(",")); - } + args.push("--features".into()); + args.push(c.config.features.join(",")); if let Some(target) = &c.config.target { args.push("--target".into()); @@ -376,10 +367,8 @@ fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { c.path.join("Cargo.toml").display().to_string(), ]; - if let Some(features) = &c.config.features { - args.push("--features".into()); - args.push(features.join(",")); - } + args.push("--features".into()); + args.push(c.config.features.join(",")); if let Some(target) = &c.config.target { args.push("--target".into()); diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index 7b2e50672..96f8ebe2e 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -23,11 +23,9 @@ pub fn minimum_update(krate: &Crate) -> Result { semver_check.with_default_features(); semver_check.set_baseline(baseline); semver_check.set_packages(vec![package_name]); - if let Some(features) = &krate.config.features { - let extra_current_features = features.clone(); - let extra_baseline_features = features.clone(); - semver_check.set_extra_features(extra_current_features, extra_baseline_features); - } + let extra_current_features = krate.config.features.clone(); + let extra_baseline_features = krate.config.features.clone(); + semver_check.set_extra_features(extra_current_features, extra_baseline_features); if let Some(target) = &krate.config.target { semver_check.set_build_target(target.clone()); } @@ -70,11 +68,7 @@ pub(crate) fn build_doc_json(krate: &Crate) -> Result { .join(format!("{}.json", krate.name.to_string().replace("-", "_"))); std::fs::remove_file(¤t_path).ok(); - let features = if let Some(features) = &krate.config.features { - features.clone() - } else { - vec![] - }; + let features = krate.config.features.clone(); log::info!("Building doc json for {} with features: {:?}", krate.name, features); diff --git a/release/src/types.rs b/release/src/types.rs index 56a886e6f..39e8e9f48 100644 --- a/release/src/types.rs +++ b/release/src/types.rs @@ -1,7 +1,8 @@ -use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; +use serde::Deserialize; + #[derive(Debug, Deserialize)] pub struct ParsedCrate { pub package: ParsedPackage, @@ -12,15 +13,31 @@ pub struct ParsedCrate { pub struct ParsedPackage { pub name: String, pub version: String, + #[serde(default)] + pub metadata: Metadata, +} + +#[derive(Debug, Deserialize, Default)] +pub struct Metadata { + #[serde(default)] + pub embassy: MetadataEmbassy, +} + +#[derive(Debug, Deserialize, Default)] +pub struct MetadataEmbassy { + #[serde(default)] + pub skip: bool, + #[serde(default)] + pub build: Vec, } #[derive(Debug, Clone, Deserialize)] -pub struct CrateConfig { - pub features: Option>, +pub struct BuildConfig { + #[serde(default)] + pub features: Vec, pub target: Option, } -pub type ReleaseConfig = HashMap; pub type CrateId = String; #[derive(Debug, Clone)] @@ -28,6 +45,6 @@ pub struct Crate { pub name: String, pub version: String, pub path: PathBuf, - pub config: CrateConfig, pub dependencies: Vec, + pub config: BuildConfig, // TODO make this a vec. } -- cgit From 6df077be0663cf4e45d9573ea8fc685a9f9dcd99 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Fri, 15 Aug 2025 15:32:26 +0200 Subject: Revert "fix: reference project dir" This reverts commit 1605a71c11aa9cd995206867abb6ba7409223b18. --- release/src/semver_check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'release') diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index 96f8ebe2e..e20bc8574 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -18,7 +18,7 @@ pub fn minimum_update(krate: &Crate) -> Result { let current_path = build_doc_json(krate)?; let baseline = Rustdoc::from_registry_latest_crate_version(); - let doc = Rustdoc::from_root(&package_path); + let doc = Rustdoc::from_path(¤t_path); let mut semver_check = Check::new(doc); semver_check.with_default_features(); semver_check.set_baseline(baseline); -- cgit From 2f540a4d2449234367e8d18c9e3fc8dc740d3ee5 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 16:11:35 +0200 Subject: Add Context struct. --- release/src/build.rs | 5 ++ release/src/main.rs | 130 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 83 insertions(+), 52 deletions(-) create mode 100644 release/src/build.rs (limited to 'release') diff --git a/release/src/build.rs b/release/src/build.rs new file mode 100644 index 000000000..adf251b4d --- /dev/null +++ b/release/src/build.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub(crate) fn build(ctx: &crate::Context) -> Result<()> { + todo!() +} diff --git a/release/src/main.rs b/release/src/main.rs index a49d8f36c..25f31486a 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -13,6 +13,7 @@ use simple_logger::SimpleLogger; use toml_edit::{DocumentMut, Item, Value}; use types::*; +mod build; mod cargo; mod semver_check; mod types; @@ -47,6 +48,12 @@ enum Command { crate_name: String, }, + /// Build + Build { + /// Crate to check. If not specified checks all crates. + #[arg(value_name = "CRATE")] + crate_name: Option, + }, /// SemverCheck SemverCheck { /// Crate to check. Will traverse that crate an it's dependents. If not specified checks all crates. @@ -186,40 +193,55 @@ fn build_graph(crates: &BTreeMap) -> (Graph, HashMa (graph, node_indices) } +struct Context { + root: PathBuf, + crates: BTreeMap, + graph: Graph, + indices: HashMap, +} + +fn load_context(args: &Args) -> Result { + let root = args.repo.canonicalize()?; + let crates = list_crates(&root)?; + let (graph, indices) = build_graph(&crates); + + Ok(Context { + root, + crates, + graph, + indices, + }) +} + fn main() -> Result<()> { SimpleLogger::new().init().unwrap(); let args = Args::parse(); - - let root = args.repo.canonicalize()?; - let mut crates = list_crates(&root)?; - let (mut graph, indices) = build_graph(&crates); + let mut ctx = load_context(&args)?; match args.command { Command::List => { - let ordered = petgraph::algo::toposort(&graph, None).unwrap(); + let ordered = petgraph::algo::toposort(&ctx.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(); - let c = crates.get(weight).unwrap(); - if weight == start { - println!("+ {}-{}", weight, c.version); - } else { - println!("|- {}-{}", weight, c.version); - } + let start = ctx.graph.node_weight(*node).unwrap(); + let mut bfs = Bfs::new(&ctx.graph, *node); + while let Some(node) = bfs.next(&ctx.graph) { + let weight = ctx.graph.node_weight(node).unwrap(); + let c = ctx.crates.get(weight).unwrap(); + if weight == start { + println!("+ {}-{}", weight, c.version); + } else { + println!("|- {}-{}", weight, c.version); } - println!(""); } + println!(""); } } Command::Dependencies { crate_name } => { - let idx = indices.get(&crate_name).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(); - let crt = crates.get(weight).unwrap(); + let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); + let mut bfs = Bfs::new(&ctx.graph, *idx); + while let Some(node) = bfs.next(&ctx.graph) { + let weight = ctx.graph.node_weight(node).unwrap(); + let crt = ctx.crates.get(weight).unwrap(); if *weight == crate_name { println!("+ {}-{}", weight, crt.version); } else { @@ -228,30 +250,34 @@ fn main() -> Result<()> { } } Command::Dependents { crate_name } => { - let idx = indices.get(&crate_name).expect("unable to find crate in tree"); - let weight = graph.node_weight(*idx).unwrap(); - let crt = crates.get(weight).unwrap(); + let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); + let weight = ctx.graph.node_weight(*idx).unwrap(); + let crt = ctx.crates.get(weight).unwrap(); println!("+ {}-{}", weight, crt.version); - for parent in graph.neighbors_directed(*idx, Direction::Incoming) { - let weight = graph.node_weight(parent).unwrap(); - let crt = crates.get(weight).unwrap(); + for parent in ctx.graph.neighbors_directed(*idx, Direction::Incoming) { + let weight = ctx.graph.node_weight(parent).unwrap(); + let crt = ctx.crates.get(weight).unwrap(); println!("|- {}-{}", weight, crt.version); } } + Command::Build { crate_name } => { + build::build(&ctx)?; + } Command::SemverCheck { crate_name } => { - let c = crates.get(&crate_name).unwrap(); + let c = ctx.crates.get(&crate_name).unwrap(); check_semver(&c)?; } Command::PrepareRelease { crate_name } => { - let start = indices.get(&crate_name).expect("unable to find crate in tree"); - graph.reverse(); + let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); + let mut rgraph = ctx.graph.clone(); + rgraph.reverse(); - let mut bfs = Bfs::new(&graph, *start); + let mut bfs = Bfs::new(&rgraph, *start); - while let Some(node) = bfs.next(&graph) { - let weight = graph.node_weight(node).unwrap(); + while let Some(node) = bfs.next(&rgraph) { + let weight = rgraph.node_weight(node).unwrap(); println!("Preparing {}", weight); - let mut c = crates.get_mut(weight).unwrap(); + let mut c = ctx.crates.get_mut(weight).unwrap(); let ver = semver::Version::parse(&c.version)?; let newver = if let Err(_) = check_semver(&c) { println!("Semver check failed, bumping minor!"); @@ -264,41 +290,41 @@ fn main() -> Result<()> { let newver = newver.to_string(); update_version(&mut c, &newver)?; - let c = crates.get(weight).unwrap(); + let c = ctx.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(); + let mut bfs = Bfs::new(&rgraph, node); + while let Some(dep_node) = bfs.next(&rgraph) { + let dep_weight = rgraph.node_weight(dep_node).unwrap(); + let dep = ctx.crates.get(dep_weight).unwrap(); update_versions(dep, &c.name, &newver)?; } // Update changelog - update_changelog(&root, &c)?; + update_changelog(&ctx.root, &c)?; } - let weight = graph.node_weight(*start).unwrap(); - let c = crates.get(weight).unwrap(); - publish_release(&root, &c, false)?; + let weight = rgraph.node_weight(*start).unwrap(); + let c = ctx.crates.get(weight).unwrap(); + publish_release(&ctx.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(); - let c = crates.get(weight).unwrap(); + let mut bfs = Bfs::new(&rgraph, *start); + while let Some(node) = bfs.next(&rgraph) { + let weight = rgraph.node_weight(node).unwrap(); + let c = ctx.crates.get(weight).unwrap(); println!("git tag {}-v{}", weight, c.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 bfs = Bfs::new(&rgraph, *start); + while let Some(node) = bfs.next(&rgraph) { + let weight = rgraph.node_weight(node).unwrap(); + let c = ctx.crates.get(weight).unwrap(); let mut args: Vec = vec![ "publish".to_string(), -- cgit From 648938b6623954a71ecee2990d57ce9df2e197b7 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 16:14:34 +0200 Subject: Make crate configs a vec. --- release/src/main.rs | 24 ++++++++++++++++-------- release/src/semver_check.rs | 25 ++++++++++++------------- release/src/types.rs | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index 25f31486a..7850bbb8d 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -141,6 +141,14 @@ fn list_crates(root: &PathBuf) -> Result> { } } + let mut configs = metadata.build.clone(); + if configs.is_empty() { + configs.push(BuildConfig { + features: vec![], + target: None, + }) + } + crates.insert( id.clone(), Crate { @@ -148,10 +156,7 @@ fn list_crates(root: &PathBuf) -> Result> { version: parsed.package.version, path, dependencies, - config: metadata.build.first().cloned().unwrap_or_else(|| BuildConfig { - features: vec![], - target: None, - }), + configs, }, ); } @@ -332,10 +337,11 @@ fn main() -> Result<()> { c.path.join("Cargo.toml").display().to_string(), ]; + let config = c.configs.first().unwrap(); // TODO args.push("--features".into()); - args.push(c.config.features.join(",")); + args.push(config.features.join(",")); - if let Some(target) = &c.config.target { + if let Some(target) = &config.target { args.push("--target".into()); args.push(target.clone()); } @@ -387,6 +393,8 @@ fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { } fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { + let config = c.configs.first().unwrap(); // TODO + let mut args: Vec = vec![ "publish".to_string(), "--manifest-path".to_string(), @@ -394,9 +402,9 @@ fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { ]; args.push("--features".into()); - args.push(c.config.features.join(",")); + args.push(config.features.join(",")); - if let Some(target) = &c.config.target { + if let Some(target) = &config.target { args.push("--target".into()); args.push(target.clone()); } diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index e20bc8574..a70c56376 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -1,21 +1,20 @@ -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc}; use crate::cargo::CargoArgsBuilder; -use crate::types::Crate; -use crate::windows_safe_path; +use crate::types::{BuildConfig, Crate}; /// Return the minimum required bump for the next release. /// Even if nothing changed this will be [ReleaseType::Patch] pub fn minimum_update(krate: &Crate) -> Result { println!("Crate = {:?}", krate); + let config = krate.configs.first().unwrap(); // TODO + let package_name = krate.name.clone(); let package_path = krate.path.clone(); - let current_path = build_doc_json(krate)?; + let current_path = build_doc_json(krate, config)?; let baseline = Rustdoc::from_registry_latest_crate_version(); let doc = Rustdoc::from_path(¤t_path); @@ -23,10 +22,10 @@ pub fn minimum_update(krate: &Crate) -> Result { semver_check.with_default_features(); semver_check.set_baseline(baseline); semver_check.set_packages(vec![package_name]); - let extra_current_features = krate.config.features.clone(); - let extra_baseline_features = krate.config.features.clone(); + let extra_current_features = config.features.clone(); + let extra_baseline_features = config.features.clone(); semver_check.set_extra_features(extra_current_features, extra_baseline_features); - if let Some(target) = &krate.config.target { + if let Some(target) = &config.target { semver_check.set_build_target(target.clone()); } let mut cfg = GlobalConfig::new(); @@ -48,7 +47,7 @@ pub fn minimum_update(krate: &Crate) -> Result { Ok(min_required_update) } -pub(crate) fn build_doc_json(krate: &Crate) -> Result { +pub(crate) fn build_doc_json(krate: &Crate, config: &BuildConfig) -> Result { let target_dir = std::env::var("CARGO_TARGET_DIR"); let target_path = if let Ok(target) = target_dir { @@ -58,7 +57,7 @@ pub(crate) fn build_doc_json(krate: &Crate) -> Result { }; let current_path = target_path; - let current_path = if let Some(target) = &krate.config.target { + let current_path = if let Some(target) = &config.target { current_path.join(target.clone()) } else { current_path @@ -68,7 +67,7 @@ pub(crate) fn build_doc_json(krate: &Crate) -> Result { .join(format!("{}.json", krate.name.to_string().replace("-", "_"))); std::fs::remove_file(¤t_path).ok(); - let features = krate.config.features.clone(); + let features = config.features.clone(); log::info!("Building doc json for {} with features: {:?}", krate.name, features); @@ -83,7 +82,7 @@ pub(crate) fn build_doc_json(krate: &Crate) -> Result { .toolchain("nightly-2025-06-29") .subcommand("rustdoc") .features(&features); - let cargo_builder = if let Some(target) = &krate.config.target { + let cargo_builder = if let Some(target) = &config.target { cargo_builder.target(target.clone()) } else { cargo_builder diff --git a/release/src/types.rs b/release/src/types.rs index 39e8e9f48..c5b774977 100644 --- a/release/src/types.rs +++ b/release/src/types.rs @@ -46,5 +46,5 @@ pub struct Crate { pub version: String, pub path: PathBuf, pub dependencies: Vec, - pub config: BuildConfig, // TODO make this a vec. + pub configs: Vec, } -- cgit From a34e0b1ec57350cfa1d61aa6fc2eced077be5623 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 17:05:45 +0200 Subject: Add build command. --- release/src/build.rs | 45 +++++++++++++++++++++++++++++++++++++++++-- release/src/cargo.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++----- release/src/main.rs | 6 +++++- 3 files changed, 97 insertions(+), 8 deletions(-) (limited to 'release') diff --git a/release/src/build.rs b/release/src/build.rs index adf251b4d..d1abb2aa1 100644 --- a/release/src/build.rs +++ b/release/src/build.rs @@ -1,5 +1,46 @@ use anyhow::Result; -pub(crate) fn build(ctx: &crate::Context) -> Result<()> { - todo!() +use crate::cargo::{CargoArgsBuilder, CargoBatchBuilder}; + +pub(crate) fn build(ctx: &crate::Context, crate_name: Option<&str>) -> Result<()> { + let mut batch_builder = CargoBatchBuilder::new(); + + // Process either specific crate or all crates + let crates_to_build: Vec<_> = if let Some(name) = crate_name { + // Build only the specified crate + if let Some(krate) = ctx.crates.get(name) { + vec![krate] + } else { + return Err(anyhow::anyhow!("Crate '{}' not found", name)); + } + } else { + // Build all crates + ctx.crates.values().collect() + }; + + // Process selected crates and add their build configurations to the batch + for krate in crates_to_build { + for config in &krate.configs { + let mut args_builder = CargoArgsBuilder::new() + .subcommand("build") + .arg("--release") + .arg(format!("--manifest-path={}/Cargo.toml", krate.path.to_string_lossy())); + + if let Some(ref target) = config.target { + args_builder = args_builder.target(target); + } + + if !config.features.is_empty() { + args_builder = args_builder.features(&config.features); + } + + batch_builder.add_command(args_builder.build()); + } + } + + // Execute the cargo batch command + let batch_args = batch_builder.build(); + crate::cargo::run(&batch_args, &ctx.root)?; + + Ok(()) } diff --git a/release/src/cargo.rs b/release/src/cargo.rs index 1a4f79f20..0b28458e9 100644 --- a/release/src/cargo.rs +++ b/release/src/cargo.rs @@ -1,10 +1,8 @@ //! Tools for working with Cargo. -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; use anyhow::{bail, Context as _, Result}; use clap::ValueEnum as _; @@ -106,6 +104,17 @@ pub struct CargoArgsBuilder { } impl CargoArgsBuilder { + #[must_use] + pub fn new() -> Self { + Self { + toolchain: None, + subcommand: String::new(), + target: None, + features: vec![], + args: vec![], + } + } + #[must_use] pub fn toolchain(mut self, toolchain: S) -> Self where @@ -192,3 +201,38 @@ impl CargoArgsBuilder { args } } + +#[derive(Debug, Default)] +pub struct CargoBatchBuilder { + commands: Vec>, +} + +impl CargoBatchBuilder { + #[must_use] + pub fn new() -> Self { + Self { commands: vec![] } + } + + #[must_use] + pub fn command(mut self, args: Vec) -> Self { + self.commands.push(args); + self + } + + pub fn add_command(&mut self, args: Vec) -> &mut Self { + self.commands.push(args); + self + } + + #[must_use] + pub fn build(&self) -> Vec { + let mut args = vec!["batch".to_string()]; + + for command in &self.commands { + args.push("---".to_string()); + args.extend(command.clone()); + } + + args + } +} diff --git a/release/src/main.rs b/release/src/main.rs index 7850bbb8d..b1bc17255 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -134,6 +134,10 @@ fn list_crates(root: &PathBuf) -> Result> { let metadata = &parsed.package.metadata.embassy; + if metadata.skip { + continue; + } + let mut dependencies = Vec::new(); for (k, _) in parsed.dependencies { if k.starts_with("embassy-") { @@ -266,7 +270,7 @@ fn main() -> Result<()> { } } Command::Build { crate_name } => { - build::build(&ctx)?; + build::build(&ctx, crate_name.as_deref())?; } Command::SemverCheck { crate_name } => { let c = ctx.crates.get(&crate_name).unwrap(); -- cgit From 3a6ea3a31c90179fb3cbd30c18e3a310e2ee647c Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 19:01:56 +0200 Subject: Load all crates in the graph, honor the "publish" flag to prevent publishing examples/tests. --- release/src/build.rs | 4 ++ release/src/cargo.rs | 9 ++++ release/src/main.rs | 130 ++++++++++++++++++++++++++++++++++++--------------- release/src/types.rs | 9 ++++ 4 files changed, 114 insertions(+), 38 deletions(-) (limited to 'release') diff --git a/release/src/build.rs b/release/src/build.rs index d1abb2aa1..7c777d36c 100644 --- a/release/src/build.rs +++ b/release/src/build.rs @@ -34,6 +34,10 @@ pub(crate) fn build(ctx: &crate::Context, crate_name: Option<&str>) -> Result<() args_builder = args_builder.features(&config.features); } + if let Some(ref artifact_dir) = config.artifact_dir { + args_builder = args_builder.artifact_dir(artifact_dir); + } + batch_builder.add_command(args_builder.build()); } } diff --git a/release/src/cargo.rs b/release/src/cargo.rs index 0b28458e9..826a3a8b2 100644 --- a/release/src/cargo.rs +++ b/release/src/cargo.rs @@ -148,6 +148,15 @@ impl CargoArgsBuilder { self } + #[must_use] + pub fn artifact_dir(mut self, artifact_dir: S) -> Self + where + S: Into, + { + self.args.push(format!("--artifact-dir={}", artifact_dir.into())); + self + } + #[must_use] pub fn arg(mut self, arg: S) -> Self where diff --git a/release/src/main.rs b/release/src/main.rs index b1bc17255..5605ca332 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command as ProcessCommand; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use clap::{Parser, Subcommand}; use log::info; use petgraph::graph::{Graph, NodeIndex}; @@ -13,6 +13,25 @@ use simple_logger::SimpleLogger; use toml_edit::{DocumentMut, Item, Value}; use types::*; +fn check_publish_dependencies(ctx: &Context) -> Result<()> { + for krate in ctx.crates.values() { + if krate.publish { + for dep_name in &krate.dependencies { + if let Some(dep_crate) = ctx.crates.get(dep_name) { + if !dep_crate.publish { + return Err(anyhow!( + "Publishable crate '{}' depends on non-publishable crate '{}'. This is not allowed.", + krate.name, + dep_name + )); + } + } + } + } + } + Ok(()) +} + mod build; mod cargo; mod semver_check; @@ -56,7 +75,7 @@ enum Command { }, /// SemverCheck SemverCheck { - /// Crate to check. Will traverse that crate an it's dependents. If not specified checks all crates. + /// Specific crate name to check #[arg(value_name = "CRATE")] crate_name: String, }, @@ -120,53 +139,72 @@ fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Resul } fn list_crates(root: &PathBuf) -> Result> { - let d = std::fs::read_dir(root)?; let mut crates = BTreeMap::new(); + discover_crates(root, &mut crates)?; + Ok(crates) +} + +fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap) -> Result<()> { + let d = std::fs::read_dir(dir)?; for c in d { let entry = c?; if entry.file_type()?.is_dir() { - let path = root.join(entry.path()); - let entry = path.join("Cargo.toml"); - if entry.exists() { - let content = fs::read_to_string(&entry)?; - let parsed: ParsedCrate = toml::from_str(&content)?; - let id = parsed.package.name; + let path = dir.join(entry.path()); + let cargo_toml = path.join("Cargo.toml"); - let metadata = &parsed.package.metadata.embassy; + if cargo_toml.exists() { + let content = fs::read_to_string(&cargo_toml)?; - if metadata.skip { - continue; - } + // Try to parse as a crate, skip if it's a workspace + let parsed: Result = toml::from_str(&content); + if let Ok(parsed) = parsed { + let id = parsed.package.name; - let mut dependencies = Vec::new(); - for (k, _) in parsed.dependencies { - if k.starts_with("embassy-") { - dependencies.push(k); + let metadata = &parsed.package.metadata.embassy; + + if metadata.skip { + continue; } - } - let mut configs = metadata.build.clone(); - if configs.is_empty() { - configs.push(BuildConfig { - features: vec![], - target: None, - }) - } + let mut dependencies = Vec::new(); + for (k, _) in parsed.dependencies { + if k.starts_with("embassy-") { + dependencies.push(k); + } + } + + let mut configs = metadata.build.clone(); + if configs.is_empty() { + configs.push(BuildConfig { + features: vec![], + target: None, + artifact_dir: None, + }) + } - crates.insert( - id.clone(), - Crate { - name: id, - version: parsed.package.version, - path, - dependencies, - configs, - }, - ); + crates.insert( + id.clone(), + Crate { + name: id, + version: parsed.package.version, + path, + dependencies, + configs, + publish: parsed.package.publish, + }, + ); + } + } else { + // Recursively search subdirectories, but only for examples, tests, and docs + let file_name = entry.file_name(); + let dir_name = file_name.to_string_lossy(); + if dir_name == "examples" || dir_name == "tests" || dir_name == "docs" { + discover_crates(&path, crates)?; + } } } } - Ok(crates) + Ok(()) } fn build_graph(crates: &BTreeMap) -> (Graph, HashMap) { @@ -214,12 +252,17 @@ fn load_context(args: &Args) -> Result { let crates = list_crates(&root)?; let (graph, indices) = build_graph(&crates); - Ok(Context { + let ctx = Context { root, crates, graph, indices, - }) + }; + + // Check for publish dependency conflicts + check_publish_dependencies(&ctx)?; + + Ok(ctx) } fn main() -> Result<()> { @@ -274,10 +317,21 @@ fn main() -> Result<()> { } Command::SemverCheck { crate_name } => { let c = ctx.crates.get(&crate_name).unwrap(); + if !c.publish { + bail!("Cannot run semver-check on non-publishable crate '{}'", crate_name); + } check_semver(&c)?; } Command::PrepareRelease { crate_name } => { let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); + + // Check if the target crate is publishable + let start_weight = ctx.graph.node_weight(*start).unwrap(); + let start_crate = ctx.crates.get(start_weight).unwrap(); + if !start_crate.publish { + bail!("Cannot prepare release for non-publishable crate '{}'", crate_name); + } + let mut rgraph = ctx.graph.clone(); rgraph.reverse(); diff --git a/release/src/types.rs b/release/src/types.rs index c5b774977..4d9d440d8 100644 --- a/release/src/types.rs +++ b/release/src/types.rs @@ -13,10 +13,16 @@ pub struct ParsedCrate { pub struct ParsedPackage { pub name: String, pub version: String, + #[serde(default = "default_publish")] + pub publish: bool, #[serde(default)] pub metadata: Metadata, } +fn default_publish() -> bool { + true +} + #[derive(Debug, Deserialize, Default)] pub struct Metadata { #[serde(default)] @@ -36,6 +42,8 @@ pub struct BuildConfig { #[serde(default)] pub features: Vec, pub target: Option, + #[serde(rename = "artifact-dir")] + pub artifact_dir: Option, } pub type CrateId = String; @@ -47,4 +55,5 @@ pub struct Crate { pub path: PathBuf, pub dependencies: Vec, pub configs: Vec, + pub publish: bool, } -- cgit From 0d8f9614a1af5a45b695e6f63b0337fc3bccba76 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Fri, 15 Aug 2025 19:06:51 +0200 Subject: Autodetect repo root. --- release/src/main.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index 5605ca332..f1949dd37 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -41,10 +41,6 @@ mod types; #[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, @@ -247,8 +243,29 @@ struct Context { indices: HashMap, } -fn load_context(args: &Args) -> Result { - let root = args.repo.canonicalize()?; +fn find_repo_root() -> Result { + let mut path = std::env::current_dir()?.canonicalize()?; + + loop { + // Check if this directory contains a .git directory + if path.join(".git").exists() { + return Ok(path); + } + + // Move to parent directory + match path.parent() { + Some(parent) => path = parent.to_path_buf(), + None => break, + } + } + + Err(anyhow!( + "Could not find repository root. Make sure you're running this tool from within the embassy repository." + )) +} + +fn load_context() -> Result { + let root = find_repo_root()?; let crates = list_crates(&root)?; let (graph, indices) = build_graph(&crates); @@ -268,7 +285,7 @@ fn load_context(args: &Args) -> Result { fn main() -> Result<()> { SimpleLogger::new().init().unwrap(); let args = Args::parse(); - let mut ctx = load_context(&args)?; + let mut ctx = load_context()?; match args.command { Command::List => { -- cgit From 1f229f745c0ccee926ae5c4cf6732d41c4beb531 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 11:14:15 +0200 Subject: fix: use patched versions --- release/Cargo.toml | 4 ++-- release/src/cargo.rs | 37 ++----------------------------------- release/src/main.rs | 1 - release/src/semver_check.rs | 11 ++++++----- release/src/types.rs | 3 ++- 5 files changed, 12 insertions(+), 44 deletions(-) (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index 3e4094eed..452e39ca6 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -19,8 +19,8 @@ simple_logger = "5.0.0" temp-file = "0.1.9" flate2 = "1.1.1" -#[patch.crates-io] -#cargo-semver-checks = { path = "../../cargo-semver-checks" } +[patch.crates-io] +cargo-semver-checks = { git = "https://github.com/lulf/cargo-semver-checks.git", rev="385f274edcbb6bf5156e30a94315852b27a527e6" } [package.metadata.embassy] skip = true diff --git a/release/src/cargo.rs b/release/src/cargo.rs index 826a3a8b2..498dfeb97 100644 --- a/release/src/cargo.rs +++ b/release/src/cargo.rs @@ -4,30 +4,16 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use anyhow::{bail, Context as _, Result}; -use clap::ValueEnum as _; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; -use toml_edit::{DocumentMut, Formatted, Item, Value}; -use crate::{windows_safe_path, Crate}; - -#[derive(Clone, Debug, PartialEq)] -pub enum CargoAction { - Build(PathBuf), - Run, -} +use crate::windows_safe_path; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Artifact { pub executable: PathBuf, } -/// Execute cargo with the given arguments and from the specified directory. -pub fn run(args: &[String], cwd: &Path) -> Result<()> { - run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], false)?; - Ok(()) -} - /// Execute cargo with the given arguments and from the specified directory. pub fn run_with_env(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result where @@ -166,25 +152,6 @@ impl CargoArgsBuilder { self } - #[must_use] - pub fn args(mut self, args: &[S]) -> Self - where - S: Clone + Into, - { - for arg in args { - self.args.push(arg.clone().into()); - } - self - } - - pub fn add_arg(&mut self, arg: S) -> &mut Self - where - S: Into, - { - self.args.push(arg.into()); - self - } - #[must_use] pub fn build(&self) -> Vec { let mut args = vec![]; diff --git a/release/src/main.rs b/release/src/main.rs index f1949dd37..cd3e094f1 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -5,7 +5,6 @@ use std::process::Command as ProcessCommand; use anyhow::{anyhow, bail, Result}; use clap::{Parser, Subcommand}; -use log::info; use petgraph::graph::{Graph, NodeIndex}; use petgraph::visit::Bfs; use petgraph::{Directed, Direction}; diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index a70c56376..9a4edd09a 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc}; +use cargo_semver_checks::{Check, GlobalConfig, LintConfig, LintLevel, ReleaseType, RequiredSemverUpdate, Rustdoc}; use crate::cargo::CargoArgsBuilder; use crate::types::{BuildConfig, Crate}; @@ -8,12 +8,9 @@ use crate::types::{BuildConfig, Crate}; /// Return the minimum required bump for the next release. /// Even if nothing changed this will be [ReleaseType::Patch] pub fn minimum_update(krate: &Crate) -> Result { - println!("Crate = {:?}", krate); - let config = krate.configs.first().unwrap(); // TODO let package_name = krate.name.clone(); - let package_path = krate.path.clone(); let current_path = build_doc_json(krate, config)?; let baseline = Rustdoc::from_registry_latest_crate_version(); @@ -30,8 +27,12 @@ pub fn minimum_update(krate: &Crate) -> Result { } let mut cfg = GlobalConfig::new(); cfg.set_log_level(Some(log::Level::Trace)); + + let mut lint_cfg = LintConfig::new(); + // Disable this lint because we provide the rustdoc json only, so it can't do feature comparison. + lint_cfg.set("feature_missing", LintLevel::Allow, RequiredSemverUpdate::Minor, 0); + cfg.set_lint_config(lint_cfg); let result = semver_check.check_release(&mut cfg)?; - log::info!("Result {:?}", result); let mut min_required_update = ReleaseType::Patch; for (_, report) in result.crate_reports() { diff --git a/release/src/types.rs b/release/src/types.rs index 4d9d440d8..be0a883f1 100644 --- a/release/src/types.rs +++ b/release/src/types.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use std::path::PathBuf; use serde::Deserialize; @@ -29,6 +29,7 @@ pub struct Metadata { pub embassy: MetadataEmbassy, } +#[allow(dead_code)] #[derive(Debug, Deserialize, Default)] pub struct MetadataEmbassy { #[serde(default)] -- cgit From cd7a7f97c0a5e928bac7299019d4320f4efc9ba6 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 11:16:38 +0200 Subject: fix: add back missing --- release/src/cargo.rs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'release') diff --git a/release/src/cargo.rs b/release/src/cargo.rs index 498dfeb97..c1ed4118f 100644 --- a/release/src/cargo.rs +++ b/release/src/cargo.rs @@ -14,6 +14,12 @@ pub struct Artifact { pub executable: PathBuf, } +/// Execute cargo with the given arguments and from the specified directory. +pub fn run(args: &[String], cwd: &Path) -> Result<()> { + run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], false)?; + Ok(()) +} + /// Execute cargo with the given arguments and from the specified directory. pub fn run_with_env(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result where -- cgit From 1f9452e0763613ab9fa3c42425f0835e9f7c8201 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 12:24:14 +0200 Subject: fix: add workaround for current compiler version --- release/Cargo.toml | 1 + release/src/semver_check.rs | 3 +++ 2 files changed, 4 insertions(+) (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index 452e39ca6..7875d088a 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -21,6 +21,7 @@ flate2 = "1.1.1" [patch.crates-io] cargo-semver-checks = { git = "https://github.com/lulf/cargo-semver-checks.git", rev="385f274edcbb6bf5156e30a94315852b27a527e6" } +#cargo-semver-checks = { path = "../../cargo-semver-checks" } [package.metadata.embassy] skip = true diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index 9a4edd09a..a4a9e77b5 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -13,6 +13,9 @@ pub fn minimum_update(krate: &Crate) -> Result { let package_name = krate.name.clone(); let current_path = build_doc_json(krate, config)?; + // TODO: Prevent compiler panic on current compiler version + std::env::set_var("RUSTFLAGS", "--cap-lints=warn"); + let baseline = Rustdoc::from_registry_latest_crate_version(); let doc = Rustdoc::from_path(¤t_path); let mut semver_check = Check::new(doc); -- cgit From ce42dd5da5b4495195778e57eeac324c3ca8c54f Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 13:13:07 +0200 Subject: update version only for un-publishable crates --- release/src/main.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index cd3e094f1..4d4cd37ed 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -368,19 +368,23 @@ fn main() -> Result<()> { println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); let newver = newver.to_string(); - update_version(&mut c, &newver)?; - let c = ctx.crates.get(weight).unwrap(); + if c.publish { + update_version(&mut c, &newver)?; + let c = ctx.crates.get(weight).unwrap(); - // Update all nodes further down the tree - let mut bfs = Bfs::new(&rgraph, node); - while let Some(dep_node) = bfs.next(&rgraph) { - let dep_weight = rgraph.node_weight(dep_node).unwrap(); - let dep = ctx.crates.get(dep_weight).unwrap(); - update_versions(dep, &c.name, &newver)?; - } + // Update all nodes further down the tree + let mut bfs = Bfs::new(&rgraph, node); + while let Some(dep_node) = bfs.next(&rgraph) { + let dep_weight = rgraph.node_weight(dep_node).unwrap(); + let dep = ctx.crates.get(dep_weight).unwrap(); + update_versions(dep, &c.name, &newver)?; + } - // Update changelog - update_changelog(&ctx.root, &c)?; + // Update changelog + update_changelog(&ctx.root, &c)?; + } else { + update_version(&mut c, &newver)?; + } } let weight = rgraph.node_weight(*start).unwrap(); -- cgit From 7357810814225e961eced5d0f2b0fe54f46e3970 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 13:17:54 +0200 Subject: only semvercheck for publishable --- release/src/main.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index 4d4cd37ed..9c650b697 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -357,18 +357,18 @@ fn main() -> Result<()> { let weight = rgraph.node_weight(node).unwrap(); println!("Preparing {}", weight); let mut c = ctx.crates.get_mut(weight).unwrap(); - let ver = semver::Version::parse(&c.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) - }; + if c.publish { + let ver = semver::Version::parse(&c.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, c.version, newver.to_string()); - let newver = newver.to_string(); + println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); + let newver = newver.to_string(); - if c.publish { update_version(&mut c, &newver)?; let c = ctx.crates.get(weight).unwrap(); @@ -382,8 +382,6 @@ fn main() -> Result<()> { // Update changelog update_changelog(&ctx.root, &c)?; - } else { - update_version(&mut c, &newver)?; } } -- cgit From 90baa19b591b5d09f7fefe6273f1d1337e7c41df Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Sat, 16 Aug 2025 17:50:50 +0200 Subject: fix: version bump --- release/src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index 9c650b697..488dc9b6a 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use std::process::Command as ProcessCommand; use anyhow::{anyhow, bail, Result}; +use cargo_semver_checks::ReleaseType; use clap::{Parser, Subcommand}; use petgraph::graph::{Graph, NodeIndex}; use petgraph::visit::Bfs; @@ -359,11 +360,10 @@ fn main() -> Result<()> { let mut c = ctx.crates.get_mut(weight).unwrap(); if c.publish { let ver = semver::Version::parse(&c.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) + let newver = match check_semver(&c)? { + ReleaseType::Major | ReleaseType::Minor => semver::Version::new(ver.major, ver.minor + 1, 0), + ReleaseType::Patch => semver::Version::new(ver.major, ver.minor, ver.patch + 1), + _ => unreachable!(), }; println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); @@ -439,10 +439,10 @@ fn main() -> Result<()> { Ok(()) } -fn check_semver(c: &Crate) -> Result<()> { +fn check_semver(c: &Crate) -> Result { let min_version = semver_check::minimum_update(c)?; println!("Version should be bumped to {:?}", min_version); - Ok(()) + Ok(min_version) } fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { -- cgit From 1c2f87b4fef0377afb47a09b0c51a08cb73d349e Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Mon, 18 Aug 2025 14:21:02 +0200 Subject: fix: build rustdoc for baseline as well --- release/Cargo.toml | 8 ++-- release/src/semver_check.rs | 93 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 15 deletions(-) (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml index 7875d088a..9701a76e5 100644 --- a/release/Cargo.toml +++ b/release/Cargo.toml @@ -18,10 +18,10 @@ log = "0.4" simple_logger = "5.0.0" temp-file = "0.1.9" flate2 = "1.1.1" - -[patch.crates-io] -cargo-semver-checks = { git = "https://github.com/lulf/cargo-semver-checks.git", rev="385f274edcbb6bf5156e30a94315852b27a527e6" } -#cargo-semver-checks = { path = "../../cargo-semver-checks" } +crates-index = "3.11.0" +tar = "0.4" +reqwest = { version = "0.12", features = ["blocking"] } +cargo-manifest = "0.19.1" [package.metadata.embassy] skip = true diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index a4a9e77b5..4cfa26ec0 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -1,6 +1,11 @@ +use std::collections::HashSet; use std::path::PathBuf; +use std::{env, fs}; -use cargo_semver_checks::{Check, GlobalConfig, LintConfig, LintLevel, ReleaseType, RequiredSemverUpdate, Rustdoc}; +use anyhow::anyhow; +use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, RequiredSemverUpdate, Rustdoc}; +use flate2::read::GzDecoder; +use tar::Archive; use crate::cargo::CargoArgsBuilder; use crate::types::{BuildConfig, Crate}; @@ -11,12 +16,18 @@ pub fn minimum_update(krate: &Crate) -> Result { let config = krate.configs.first().unwrap(); // TODO let package_name = krate.name.clone(); - let current_path = build_doc_json(krate, config)?; + let baseline_path = download_baseline(&package_name, &krate.version)?; + let mut baseline_krate = krate.clone(); + baseline_krate.path = baseline_path; - // TODO: Prevent compiler panic on current compiler version - std::env::set_var("RUSTFLAGS", "--cap-lints=warn"); + // Compare features as it's not covered by semver-checks + if compare_features(&baseline_krate, &krate)? { + return Ok(ReleaseType::Minor); + } + let baseline_path = build_doc_json(&baseline_krate, config)?; + let current_path = build_doc_json(krate, config)?; - let baseline = Rustdoc::from_registry_latest_crate_version(); + let baseline = Rustdoc::from_path(&baseline_path); let doc = Rustdoc::from_path(¤t_path); let mut semver_check = Check::new(doc); semver_check.with_default_features(); @@ -29,12 +40,8 @@ pub fn minimum_update(krate: &Crate) -> Result { semver_check.set_build_target(target.clone()); } let mut cfg = GlobalConfig::new(); - cfg.set_log_level(Some(log::Level::Trace)); + cfg.set_log_level(Some(log::Level::Info)); - let mut lint_cfg = LintConfig::new(); - // Disable this lint because we provide the rustdoc json only, so it can't do feature comparison. - lint_cfg.set("feature_missing", LintLevel::Allow, RequiredSemverUpdate::Minor, 0); - cfg.set_lint_config(lint_cfg); let result = semver_check.check_release(&mut cfg)?; let mut min_required_update = ReleaseType::Patch; @@ -51,7 +58,71 @@ pub fn minimum_update(krate: &Crate) -> Result { Ok(min_required_update) } -pub(crate) fn build_doc_json(krate: &Crate, config: &BuildConfig) -> Result { +fn compare_features(old: &Crate, new: &Crate) -> Result { + let mut old = read_features(&old.path)?; + let new = read_features(&new.path)?; + + old.retain(|r| !new.contains(r)); + log::info!("Features removed in new: {:?}", old); + Ok(!old.is_empty()) +} + +fn download_baseline(name: &str, version: &str) -> Result { + let config = crates_index::IndexConfig { + dl: "https://crates.io/api/v1/crates".to_string(), + api: Some("https://crates.io".to_string()), + }; + + let url = + config + .download_url(name, version) + .ok_or(anyhow!("unable to download baseline for {}-{}", name, version))?; + + let parent_dir = env::var("RELEASER_CACHE").map_err(|_| anyhow!("RELEASER_CACHE not set"))?; + + let extract_path = PathBuf::from(&parent_dir).join(format!("{}-{}", name, version)); + + if extract_path.exists() { + return Ok(extract_path); + } + + let response = reqwest::blocking::get(url)?; + let bytes = response.bytes()?; + + let decoder = GzDecoder::new(&bytes[..]); + let mut archive = Archive::new(decoder); + archive.unpack(&parent_dir)?; + + Ok(extract_path) +} + +fn read_features(crate_path: &PathBuf) -> Result, anyhow::Error> { + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + return Err(anyhow!("Cargo.toml not found at {:?}", cargo_toml_path)); + } + + let manifest = cargo_manifest::Manifest::from_path(&cargo_toml_path)?; + + let mut set = HashSet::new(); + if let Some(features) = manifest.features { + for f in features.keys() { + set.insert(f.clone()); + } + } + if let Some(deps) = manifest.dependencies { + for (k, v) in deps.iter() { + if v.optional() { + set.insert(k.clone()); + } + } + } + + Ok(set) +} + +fn build_doc_json(krate: &Crate, config: &BuildConfig) -> Result { let target_dir = std::env::var("CARGO_TARGET_DIR"); let target_path = if let Ok(target) = target_dir { -- cgit From b00496fd299ca1dbc122476f0477b4032d5727c7 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Mon, 18 Aug 2025 15:00:22 +0200 Subject: fix: write instructions only for publishing crates --- release/src/main.rs | 14 ++++++++++---- release/src/semver_check.rs | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'release') diff --git a/release/src/main.rs b/release/src/main.rs index 488dc9b6a..22c926b86 100644 --- a/release/src/main.rs +++ b/release/src/main.rs @@ -396,7 +396,9 @@ fn main() -> Result<()> { while let Some(node) = bfs.next(&rgraph) { let weight = rgraph.node_weight(node).unwrap(); let c = ctx.crates.get(weight).unwrap(); - println!("git tag {}-v{}", weight, c.version); + if c.publish { + println!("git tag {}-v{}", weight, c.version); + } } println!(""); @@ -414,8 +416,10 @@ fn main() -> Result<()> { ]; let config = c.configs.first().unwrap(); // TODO - args.push("--features".into()); - args.push(config.features.join(",")); + if !config.features.is_empty() { + args.push("--features".into()); + args.push(config.features.join(",")); + } if let Some(target) = &config.target { args.push("--target".into()); @@ -428,7 +432,9 @@ fn main() -> Result<()> { println!("cargo {}", dry_run.join(" ")); */ - println!("cargo {}", args.join(" ")); + if c.publish { + println!("cargo {}", args.join(" ")); + } } println!(""); diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs index 4cfa26ec0..6255260f3 100644 --- a/release/src/semver_check.rs +++ b/release/src/semver_check.rs @@ -1,9 +1,9 @@ use std::collections::HashSet; +use std::env; use std::path::PathBuf; -use std::{env, fs}; use anyhow::anyhow; -use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, RequiredSemverUpdate, Rustdoc}; +use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc}; use flate2::read::GzDecoder; use tar::Archive; -- cgit From d835b5385734d6211e247b13b7f4b3c53c6b6fff Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 19 Aug 2025 13:58:25 +0200 Subject: chore: move tool to separate repo --- release/Cargo.toml | 27 --- release/src/build.rs | 50 ----- release/src/cargo.rs | 220 ------------------- release/src/main.rs | 514 -------------------------------------------- release/src/semver_check.rs | 178 --------------- release/src/types.rs | 60 ------ 6 files changed, 1049 deletions(-) delete mode 100644 release/Cargo.toml delete mode 100644 release/src/build.rs delete mode 100644 release/src/cargo.rs delete mode 100644 release/src/main.rs delete mode 100644 release/src/semver_check.rs delete mode 100644 release/src/types.rs (limited to 'release') diff --git a/release/Cargo.toml b/release/Cargo.toml deleted file mode 100644 index 9701a76e5..000000000 --- a/release/Cargo.toml +++ /dev/null @@ -1,27 +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.9.5" -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" -cargo-semver-checks = "0.43.0" -log = "0.4" -simple_logger = "5.0.0" -temp-file = "0.1.9" -flate2 = "1.1.1" -crates-index = "3.11.0" -tar = "0.4" -reqwest = { version = "0.12", features = ["blocking"] } -cargo-manifest = "0.19.1" - -[package.metadata.embassy] -skip = true diff --git a/release/src/build.rs b/release/src/build.rs deleted file mode 100644 index 7c777d36c..000000000 --- a/release/src/build.rs +++ /dev/null @@ -1,50 +0,0 @@ -use anyhow::Result; - -use crate::cargo::{CargoArgsBuilder, CargoBatchBuilder}; - -pub(crate) fn build(ctx: &crate::Context, crate_name: Option<&str>) -> Result<()> { - let mut batch_builder = CargoBatchBuilder::new(); - - // Process either specific crate or all crates - let crates_to_build: Vec<_> = if let Some(name) = crate_name { - // Build only the specified crate - if let Some(krate) = ctx.crates.get(name) { - vec![krate] - } else { - return Err(anyhow::anyhow!("Crate '{}' not found", name)); - } - } else { - // Build all crates - ctx.crates.values().collect() - }; - - // Process selected crates and add their build configurations to the batch - for krate in crates_to_build { - for config in &krate.configs { - let mut args_builder = CargoArgsBuilder::new() - .subcommand("build") - .arg("--release") - .arg(format!("--manifest-path={}/Cargo.toml", krate.path.to_string_lossy())); - - if let Some(ref target) = config.target { - args_builder = args_builder.target(target); - } - - if !config.features.is_empty() { - args_builder = args_builder.features(&config.features); - } - - if let Some(ref artifact_dir) = config.artifact_dir { - args_builder = args_builder.artifact_dir(artifact_dir); - } - - batch_builder.add_command(args_builder.build()); - } - } - - // Execute the cargo batch command - let batch_args = batch_builder.build(); - crate::cargo::run(&batch_args, &ctx.root)?; - - Ok(()) -} diff --git a/release/src/cargo.rs b/release/src/cargo.rs deleted file mode 100644 index c1ed4118f..000000000 --- a/release/src/cargo.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Tools for working with Cargo. - -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -use anyhow::{bail, Result}; -use serde::{Deserialize, Serialize}; - -use crate::windows_safe_path; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Artifact { - pub executable: PathBuf, -} - -/// Execute cargo with the given arguments and from the specified directory. -pub fn run(args: &[String], cwd: &Path) -> Result<()> { - run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], false)?; - Ok(()) -} - -/// Execute cargo with the given arguments and from the specified directory. -pub fn run_with_env(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result -where - I: IntoIterator + core::fmt::Debug, - K: AsRef, - V: AsRef, -{ - if !cwd.is_dir() { - bail!("The `cwd` argument MUST be a directory"); - } - - // Make sure to not use a UNC as CWD! - // That would make `OUT_DIR` a UNC which will trigger things like the one fixed in https://github.com/dtolnay/rustversion/pull/51 - // While it's fixed in `rustversion` it's not fixed for other crates we are - // using now or in future! - let cwd = windows_safe_path(cwd); - - println!( - "Running `cargo {}` in {:?} - Environment {:?}", - args.join(" "), - cwd, - envs - ); - - let mut command = Command::new(get_cargo()); - - command - .args(args) - .current_dir(cwd) - .envs(envs) - .stdout(if capture { Stdio::piped() } else { Stdio::inherit() }) - .stderr(if capture { Stdio::piped() } else { Stdio::inherit() }); - - if args.iter().any(|a| a.starts_with('+')) { - // Make sure the right cargo runs - command.env_remove("CARGO"); - } - - let output = command.stdin(Stdio::inherit()).output()?; - - // Make sure that we return an appropriate exit code here, as Github Actions - // requires this in order to function correctly: - if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } else { - bail!("Failed to execute cargo subcommand `cargo {}`", args.join(" "),) - } -} - -fn get_cargo() -> String { - // On Windows when executed via `cargo run` (e.g. via the xtask alias) the - // `cargo` on the search path is NOT the cargo-wrapper but the `cargo` from the - // toolchain - that one doesn't understand `+toolchain` - #[cfg(target_os = "windows")] - let cargo = if let Ok(cargo) = std::env::var("CARGO_HOME") { - format!("{cargo}/bin/cargo") - } else { - String::from("cargo") - }; - - #[cfg(not(target_os = "windows"))] - let cargo = String::from("cargo"); - - cargo -} - -#[derive(Debug, Default)] -pub struct CargoArgsBuilder { - toolchain: Option, - subcommand: String, - target: Option, - features: Vec, - args: Vec, -} - -impl CargoArgsBuilder { - #[must_use] - pub fn new() -> Self { - Self { - toolchain: None, - subcommand: String::new(), - target: None, - features: vec![], - args: vec![], - } - } - - #[must_use] - pub fn toolchain(mut self, toolchain: S) -> Self - where - S: Into, - { - self.toolchain = Some(toolchain.into()); - self - } - - #[must_use] - pub fn subcommand(mut self, subcommand: S) -> Self - where - S: Into, - { - self.subcommand = subcommand.into(); - self - } - - #[must_use] - pub fn target(mut self, target: S) -> Self - where - S: Into, - { - self.target = Some(target.into()); - self - } - - #[must_use] - pub fn features(mut self, features: &[String]) -> Self { - self.features = features.to_vec(); - self - } - - #[must_use] - pub fn artifact_dir(mut self, artifact_dir: S) -> Self - where - S: Into, - { - self.args.push(format!("--artifact-dir={}", artifact_dir.into())); - self - } - - #[must_use] - pub fn arg(mut self, arg: S) -> Self - where - S: Into, - { - self.args.push(arg.into()); - self - } - - #[must_use] - pub fn build(&self) -> Vec { - let mut args = vec![]; - - if let Some(ref toolchain) = self.toolchain { - args.push(format!("+{toolchain}")); - } - - args.push(self.subcommand.clone()); - - if let Some(ref target) = self.target { - args.push(format!("--target={target}")); - } - - if !self.features.is_empty() { - args.push(format!("--features={}", self.features.join(","))); - } - - for arg in self.args.iter() { - args.push(arg.clone()); - } - - args - } -} - -#[derive(Debug, Default)] -pub struct CargoBatchBuilder { - commands: Vec>, -} - -impl CargoBatchBuilder { - #[must_use] - pub fn new() -> Self { - Self { commands: vec![] } - } - - #[must_use] - pub fn command(mut self, args: Vec) -> Self { - self.commands.push(args); - self - } - - pub fn add_command(&mut self, args: Vec) -> &mut Self { - self.commands.push(args); - self - } - - #[must_use] - pub fn build(&self) -> Vec { - let mut args = vec!["batch".to_string()]; - - for command in &self.commands { - args.push("---".to_string()); - args.extend(command.clone()); - } - - args - } -} 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 @@ -use std::collections::{BTreeMap, HashMap}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command as ProcessCommand; - -use anyhow::{anyhow, bail, Result}; -use cargo_semver_checks::ReleaseType; -use clap::{Parser, Subcommand}; -use petgraph::graph::{Graph, NodeIndex}; -use petgraph::visit::Bfs; -use petgraph::{Directed, Direction}; -use simple_logger::SimpleLogger; -use toml_edit::{DocumentMut, Item, Value}; -use types::*; - -fn check_publish_dependencies(ctx: &Context) -> Result<()> { - for krate in ctx.crates.values() { - if krate.publish { - for dep_name in &krate.dependencies { - if let Some(dep_crate) = ctx.crates.get(dep_name) { - if !dep_crate.publish { - return Err(anyhow!( - "Publishable crate '{}' depends on non-publishable crate '{}'. This is not allowed.", - krate.name, - dep_name - )); - } - } - } - } - } - Ok(()) -} - -mod build; -mod cargo; -mod semver_check; -mod types; - -/// Tool to traverse and operate on intra-repo Rust crate dependencies -#[derive(Parser, Debug)] -#[command(author, version, about)] -struct Args { - /// 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, - }, - - /// Build - Build { - /// Crate to check. If not specified checks all crates. - #[arg(value_name = "CRATE")] - crate_name: Option, - }, - /// SemverCheck - SemverCheck { - /// Specific crate name to check - #[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, - }, -} - -fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { - let path = c.path.join("Cargo.toml"); - c.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.join("Cargo.toml"); - 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) { - 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, new_version, path.display()); - } - Ok(()) -} - -fn list_crates(root: &PathBuf) -> Result> { - let mut crates = BTreeMap::new(); - discover_crates(root, &mut crates)?; - Ok(crates) -} - -fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap) -> Result<()> { - let d = std::fs::read_dir(dir)?; - for c in d { - let entry = c?; - if entry.file_type()?.is_dir() { - let path = dir.join(entry.path()); - let cargo_toml = path.join("Cargo.toml"); - - if cargo_toml.exists() { - let content = fs::read_to_string(&cargo_toml)?; - - // Try to parse as a crate, skip if it's a workspace - let parsed: Result = toml::from_str(&content); - if let Ok(parsed) = parsed { - let id = parsed.package.name; - - let metadata = &parsed.package.metadata.embassy; - - if metadata.skip { - continue; - } - - let mut dependencies = Vec::new(); - for (k, _) in parsed.dependencies { - if k.starts_with("embassy-") { - dependencies.push(k); - } - } - - let mut configs = metadata.build.clone(); - if configs.is_empty() { - configs.push(BuildConfig { - features: vec![], - target: None, - artifact_dir: None, - }) - } - - crates.insert( - id.clone(), - Crate { - name: id, - version: parsed.package.version, - path, - dependencies, - configs, - publish: parsed.package.publish, - }, - ); - } - } else { - // Recursively search subdirectories, but only for examples, tests, and docs - let file_name = entry.file_name(); - let dir_name = file_name.to_string_lossy(); - if dir_name == "examples" || dir_name == "tests" || dir_name == "docs" { - discover_crates(&path, crates)?; - } - } - } - } - Ok(()) -} - -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.name.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.name.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) -} - -struct Context { - root: PathBuf, - crates: BTreeMap, - graph: Graph, - indices: HashMap, -} - -fn find_repo_root() -> Result { - let mut path = std::env::current_dir()?.canonicalize()?; - - loop { - // Check if this directory contains a .git directory - if path.join(".git").exists() { - return Ok(path); - } - - // Move to parent directory - match path.parent() { - Some(parent) => path = parent.to_path_buf(), - None => break, - } - } - - Err(anyhow!( - "Could not find repository root. Make sure you're running this tool from within the embassy repository." - )) -} - -fn load_context() -> Result { - let root = find_repo_root()?; - let crates = list_crates(&root)?; - let (graph, indices) = build_graph(&crates); - - let ctx = Context { - root, - crates, - graph, - indices, - }; - - // Check for publish dependency conflicts - check_publish_dependencies(&ctx)?; - - Ok(ctx) -} - -fn main() -> Result<()> { - SimpleLogger::new().init().unwrap(); - let args = Args::parse(); - let mut ctx = load_context()?; - - match args.command { - Command::List => { - let ordered = petgraph::algo::toposort(&ctx.graph, None).unwrap(); - for node in ordered.iter() { - let start = ctx.graph.node_weight(*node).unwrap(); - let mut bfs = Bfs::new(&ctx.graph, *node); - while let Some(node) = bfs.next(&ctx.graph) { - let weight = ctx.graph.node_weight(node).unwrap(); - let c = ctx.crates.get(weight).unwrap(); - if weight == start { - println!("+ {}-{}", weight, c.version); - } else { - println!("|- {}-{}", weight, c.version); - } - } - println!(""); - } - } - Command::Dependencies { crate_name } => { - let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); - let mut bfs = Bfs::new(&ctx.graph, *idx); - while let Some(node) = bfs.next(&ctx.graph) { - let weight = ctx.graph.node_weight(node).unwrap(); - let crt = ctx.crates.get(weight).unwrap(); - if *weight == crate_name { - println!("+ {}-{}", weight, crt.version); - } else { - println!("|- {}-{}", weight, crt.version); - } - } - } - Command::Dependents { crate_name } => { - let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); - let weight = ctx.graph.node_weight(*idx).unwrap(); - let crt = ctx.crates.get(weight).unwrap(); - println!("+ {}-{}", weight, crt.version); - for parent in ctx.graph.neighbors_directed(*idx, Direction::Incoming) { - let weight = ctx.graph.node_weight(parent).unwrap(); - let crt = ctx.crates.get(weight).unwrap(); - println!("|- {}-{}", weight, crt.version); - } - } - Command::Build { crate_name } => { - build::build(&ctx, crate_name.as_deref())?; - } - Command::SemverCheck { crate_name } => { - let c = ctx.crates.get(&crate_name).unwrap(); - if !c.publish { - bail!("Cannot run semver-check on non-publishable crate '{}'", crate_name); - } - check_semver(&c)?; - } - Command::PrepareRelease { crate_name } => { - let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); - - // Check if the target crate is publishable - let start_weight = ctx.graph.node_weight(*start).unwrap(); - let start_crate = ctx.crates.get(start_weight).unwrap(); - if !start_crate.publish { - bail!("Cannot prepare release for non-publishable crate '{}'", crate_name); - } - - let mut rgraph = ctx.graph.clone(); - rgraph.reverse(); - - let mut bfs = Bfs::new(&rgraph, *start); - - while let Some(node) = bfs.next(&rgraph) { - let weight = rgraph.node_weight(node).unwrap(); - println!("Preparing {}", weight); - let mut c = ctx.crates.get_mut(weight).unwrap(); - if c.publish { - let ver = semver::Version::parse(&c.version)?; - let newver = match check_semver(&c)? { - ReleaseType::Major | ReleaseType::Minor => semver::Version::new(ver.major, ver.minor + 1, 0), - ReleaseType::Patch => semver::Version::new(ver.major, ver.minor, ver.patch + 1), - _ => unreachable!(), - }; - - println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); - let newver = newver.to_string(); - - update_version(&mut c, &newver)?; - let c = ctx.crates.get(weight).unwrap(); - - // Update all nodes further down the tree - let mut bfs = Bfs::new(&rgraph, node); - while let Some(dep_node) = bfs.next(&rgraph) { - let dep_weight = rgraph.node_weight(dep_node).unwrap(); - let dep = ctx.crates.get(dep_weight).unwrap(); - update_versions(dep, &c.name, &newver)?; - } - - // Update changelog - update_changelog(&ctx.root, &c)?; - } - } - - let weight = rgraph.node_weight(*start).unwrap(); - let c = ctx.crates.get(weight).unwrap(); - publish_release(&ctx.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(&rgraph, *start); - while let Some(node) = bfs.next(&rgraph) { - let weight = rgraph.node_weight(node).unwrap(); - let c = ctx.crates.get(weight).unwrap(); - if c.publish { - println!("git tag {}-v{}", weight, c.version); - } - } - - println!(""); - println!("# Run these commands to publish the crate and dependents:"); - - let mut bfs = Bfs::new(&rgraph, *start); - while let Some(node) = bfs.next(&rgraph) { - let weight = rgraph.node_weight(node).unwrap(); - let c = ctx.crates.get(weight).unwrap(); - - let mut args: Vec = vec![ - "publish".to_string(), - "--manifest-path".to_string(), - c.path.join("Cargo.toml").display().to_string(), - ]; - - let config = c.configs.first().unwrap(); // TODO - if !config.features.is_empty() { - args.push("--features".into()); - args.push(config.features.join(",")); - } - - if let Some(target) = &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(" ")); - */ - if c.publish { - 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 min_version = semver_check::minimum_update(c)?; - println!("Version should be bumped to {:?}", min_version); - Ok(min_version) -} - -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.join("Cargo.toml").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 config = c.configs.first().unwrap(); // TODO - - let mut args: Vec = vec![ - "publish".to_string(), - "--manifest-path".to_string(), - c.path.join("Cargo.toml").display().to_string(), - ]; - - args.push("--features".into()); - args.push(config.features.join(",")); - - if let Some(target) = &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(()) - } -} - -/// Make the path "Windows"-safe -pub fn windows_safe_path(path: &Path) -> PathBuf { - PathBuf::from(path.to_str().unwrap().to_string().replace("\\\\?\\", "")) -} diff --git a/release/src/semver_check.rs b/release/src/semver_check.rs deleted file mode 100644 index 6255260f3..000000000 --- a/release/src/semver_check.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::collections::HashSet; -use std::env; -use std::path::PathBuf; - -use anyhow::anyhow; -use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc}; -use flate2::read::GzDecoder; -use tar::Archive; - -use crate::cargo::CargoArgsBuilder; -use crate::types::{BuildConfig, Crate}; - -/// Return the minimum required bump for the next release. -/// Even if nothing changed this will be [ReleaseType::Patch] -pub fn minimum_update(krate: &Crate) -> Result { - let config = krate.configs.first().unwrap(); // TODO - - let package_name = krate.name.clone(); - let baseline_path = download_baseline(&package_name, &krate.version)?; - let mut baseline_krate = krate.clone(); - baseline_krate.path = baseline_path; - - // Compare features as it's not covered by semver-checks - if compare_features(&baseline_krate, &krate)? { - return Ok(ReleaseType::Minor); - } - let baseline_path = build_doc_json(&baseline_krate, config)?; - let current_path = build_doc_json(krate, config)?; - - let baseline = Rustdoc::from_path(&baseline_path); - let doc = Rustdoc::from_path(¤t_path); - let mut semver_check = Check::new(doc); - semver_check.with_default_features(); - semver_check.set_baseline(baseline); - semver_check.set_packages(vec![package_name]); - let extra_current_features = config.features.clone(); - let extra_baseline_features = config.features.clone(); - semver_check.set_extra_features(extra_current_features, extra_baseline_features); - if let Some(target) = &config.target { - semver_check.set_build_target(target.clone()); - } - let mut cfg = GlobalConfig::new(); - cfg.set_log_level(Some(log::Level::Info)); - - let result = semver_check.check_release(&mut cfg)?; - - let mut min_required_update = ReleaseType::Patch; - for (_, report) in result.crate_reports() { - if let Some(required_bump) = report.required_bump() { - let required_is_stricter = - (min_required_update == ReleaseType::Patch) || (required_bump == ReleaseType::Major); - if required_is_stricter { - min_required_update = required_bump; - } - } - } - - Ok(min_required_update) -} - -fn compare_features(old: &Crate, new: &Crate) -> Result { - let mut old = read_features(&old.path)?; - let new = read_features(&new.path)?; - - old.retain(|r| !new.contains(r)); - log::info!("Features removed in new: {:?}", old); - Ok(!old.is_empty()) -} - -fn download_baseline(name: &str, version: &str) -> Result { - let config = crates_index::IndexConfig { - dl: "https://crates.io/api/v1/crates".to_string(), - api: Some("https://crates.io".to_string()), - }; - - let url = - config - .download_url(name, version) - .ok_or(anyhow!("unable to download baseline for {}-{}", name, version))?; - - let parent_dir = env::var("RELEASER_CACHE").map_err(|_| anyhow!("RELEASER_CACHE not set"))?; - - let extract_path = PathBuf::from(&parent_dir).join(format!("{}-{}", name, version)); - - if extract_path.exists() { - return Ok(extract_path); - } - - let response = reqwest::blocking::get(url)?; - let bytes = response.bytes()?; - - let decoder = GzDecoder::new(&bytes[..]); - let mut archive = Archive::new(decoder); - archive.unpack(&parent_dir)?; - - Ok(extract_path) -} - -fn read_features(crate_path: &PathBuf) -> Result, anyhow::Error> { - let cargo_toml_path = crate_path.join("Cargo.toml"); - - if !cargo_toml_path.exists() { - return Err(anyhow!("Cargo.toml not found at {:?}", cargo_toml_path)); - } - - let manifest = cargo_manifest::Manifest::from_path(&cargo_toml_path)?; - - let mut set = HashSet::new(); - if let Some(features) = manifest.features { - for f in features.keys() { - set.insert(f.clone()); - } - } - if let Some(deps) = manifest.dependencies { - for (k, v) in deps.iter() { - if v.optional() { - set.insert(k.clone()); - } - } - } - - Ok(set) -} - -fn build_doc_json(krate: &Crate, config: &BuildConfig) -> Result { - let target_dir = std::env::var("CARGO_TARGET_DIR"); - - let target_path = if let Ok(target) = target_dir { - PathBuf::from(target) - } else { - PathBuf::from(&krate.path).join("target") - }; - - let current_path = target_path; - let current_path = if let Some(target) = &config.target { - current_path.join(target.clone()) - } else { - current_path - }; - let current_path = current_path - .join("doc") - .join(format!("{}.json", krate.name.to_string().replace("-", "_"))); - - std::fs::remove_file(¤t_path).ok(); - let features = config.features.clone(); - - log::info!("Building doc json for {} with features: {:?}", krate.name, features); - - let envs = vec![( - "RUSTDOCFLAGS", - "--cfg docsrs --cfg not_really_docsrs --cfg semver_checks", - )]; - - // always use `specific nightly` toolchain so we don't have to deal with potentially - // different versions of the doc-json - let cargo_builder = CargoArgsBuilder::default() - .toolchain("nightly-2025-06-29") - .subcommand("rustdoc") - .features(&features); - let cargo_builder = if let Some(target) = &config.target { - cargo_builder.target(target.clone()) - } else { - cargo_builder - }; - - let cargo_builder = cargo_builder - .arg("-Zunstable-options") - .arg("-Zhost-config") - .arg("-Ztarget-applies-to-host") - .arg("--lib") - .arg("--output-format=json") - .arg("-Zbuild-std=alloc,core") - .arg("--config=host.rustflags=[\"--cfg=instability_disable_unstable_docs\"]"); - let cargo_args = cargo_builder.build(); - log::debug!("{cargo_args:#?}"); - crate::cargo::run_with_env(&cargo_args, &krate.path, envs, false)?; - Ok(current_path) -} diff --git a/release/src/types.rs b/release/src/types.rs deleted file mode 100644 index be0a883f1..000000000 --- a/release/src/types.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; - -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct ParsedCrate { - pub package: ParsedPackage, - pub dependencies: BTreeMap, -} - -#[derive(Debug, Deserialize)] -pub struct ParsedPackage { - pub name: String, - pub version: String, - #[serde(default = "default_publish")] - pub publish: bool, - #[serde(default)] - pub metadata: Metadata, -} - -fn default_publish() -> bool { - true -} - -#[derive(Debug, Deserialize, Default)] -pub struct Metadata { - #[serde(default)] - pub embassy: MetadataEmbassy, -} - -#[allow(dead_code)] -#[derive(Debug, Deserialize, Default)] -pub struct MetadataEmbassy { - #[serde(default)] - pub skip: bool, - #[serde(default)] - pub build: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct BuildConfig { - #[serde(default)] - pub features: Vec, - pub target: Option, - #[serde(rename = "artifact-dir")] - pub artifact_dir: Option, -} - -pub type CrateId = String; - -#[derive(Debug, Clone)] -pub struct Crate { - pub name: String, - pub version: String, - pub path: PathBuf, - pub dependencies: Vec, - pub configs: Vec, - pub publish: bool, -} -- cgit