aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUlf Lilleengen <[email protected]>2025-08-19 13:58:25 +0200
committerUlf Lilleengen <[email protected]>2025-08-25 19:44:51 +0200
commitd835b5385734d6211e247b13b7f4b3c53c6b6fff (patch)
tree4d272bff3a8b1864ee084b21fa4f2e700c0ac17a
parentb00496fd299ca1dbc122476f0477b4032d5727c7 (diff)
chore: move tool to separate repo
-rw-r--r--release/Cargo.toml27
-rw-r--r--release/src/build.rs50
-rw-r--r--release/src/cargo.rs220
-rw-r--r--release/src/main.rs514
-rw-r--r--release/src/semver_check.rs178
-rw-r--r--release/src/types.rs60
6 files changed, 0 insertions, 1049 deletions
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 @@
1[package]
2name = "embassy-release"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7clap = { version = "4.5.1", features = ["derive"] }
8walkdir = "2.5.0"
9toml = "0.9.5"
10toml_edit = { version = "0.23.1", features = ["serde"] }
11serde = { version = "1.0.198", features = ["derive"] }
12regex = "1.10.4"
13anyhow = "1"
14petgraph = "0.8.2"
15semver = "1.0.26"
16cargo-semver-checks = "0.43.0"
17log = "0.4"
18simple_logger = "5.0.0"
19temp-file = "0.1.9"
20flate2 = "1.1.1"
21crates-index = "3.11.0"
22tar = "0.4"
23reqwest = { version = "0.12", features = ["blocking"] }
24cargo-manifest = "0.19.1"
25
26[package.metadata.embassy]
27skip = 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 @@
1use anyhow::Result;
2
3use crate::cargo::{CargoArgsBuilder, CargoBatchBuilder};
4
5pub(crate) fn build(ctx: &crate::Context, crate_name: Option<&str>) -> Result<()> {
6 let mut batch_builder = CargoBatchBuilder::new();
7
8 // Process either specific crate or all crates
9 let crates_to_build: Vec<_> = if let Some(name) = crate_name {
10 // Build only the specified crate
11 if let Some(krate) = ctx.crates.get(name) {
12 vec![krate]
13 } else {
14 return Err(anyhow::anyhow!("Crate '{}' not found", name));
15 }
16 } else {
17 // Build all crates
18 ctx.crates.values().collect()
19 };
20
21 // Process selected crates and add their build configurations to the batch
22 for krate in crates_to_build {
23 for config in &krate.configs {
24 let mut args_builder = CargoArgsBuilder::new()
25 .subcommand("build")
26 .arg("--release")
27 .arg(format!("--manifest-path={}/Cargo.toml", krate.path.to_string_lossy()));
28
29 if let Some(ref target) = config.target {
30 args_builder = args_builder.target(target);
31 }
32
33 if !config.features.is_empty() {
34 args_builder = args_builder.features(&config.features);
35 }
36
37 if let Some(ref artifact_dir) = config.artifact_dir {
38 args_builder = args_builder.artifact_dir(artifact_dir);
39 }
40
41 batch_builder.add_command(args_builder.build());
42 }
43 }
44
45 // Execute the cargo batch command
46 let batch_args = batch_builder.build();
47 crate::cargo::run(&batch_args, &ctx.root)?;
48
49 Ok(())
50}
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 @@
1//! Tools for working with Cargo.
2
3use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6
7use anyhow::{bail, Result};
8use serde::{Deserialize, Serialize};
9
10use crate::windows_safe_path;
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct Artifact {
14 pub executable: PathBuf,
15}
16
17/// Execute cargo with the given arguments and from the specified directory.
18pub fn run(args: &[String], cwd: &Path) -> Result<()> {
19 run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], false)?;
20 Ok(())
21}
22
23/// Execute cargo with the given arguments and from the specified directory.
24pub fn run_with_env<I, K, V>(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result<String>
25where
26 I: IntoIterator<Item = (K, V)> + core::fmt::Debug,
27 K: AsRef<OsStr>,
28 V: AsRef<OsStr>,
29{
30 if !cwd.is_dir() {
31 bail!("The `cwd` argument MUST be a directory");
32 }
33
34 // Make sure to not use a UNC as CWD!
35 // That would make `OUT_DIR` a UNC which will trigger things like the one fixed in https://github.com/dtolnay/rustversion/pull/51
36 // While it's fixed in `rustversion` it's not fixed for other crates we are
37 // using now or in future!
38 let cwd = windows_safe_path(cwd);
39
40 println!(
41 "Running `cargo {}` in {:?} - Environment {:?}",
42 args.join(" "),
43 cwd,
44 envs
45 );
46
47 let mut command = Command::new(get_cargo());
48
49 command
50 .args(args)
51 .current_dir(cwd)
52 .envs(envs)
53 .stdout(if capture { Stdio::piped() } else { Stdio::inherit() })
54 .stderr(if capture { Stdio::piped() } else { Stdio::inherit() });
55
56 if args.iter().any(|a| a.starts_with('+')) {
57 // Make sure the right cargo runs
58 command.env_remove("CARGO");
59 }
60
61 let output = command.stdin(Stdio::inherit()).output()?;
62
63 // Make sure that we return an appropriate exit code here, as Github Actions
64 // requires this in order to function correctly:
65 if output.status.success() {
66 Ok(String::from_utf8_lossy(&output.stdout).to_string())
67 } else {
68 bail!("Failed to execute cargo subcommand `cargo {}`", args.join(" "),)
69 }
70}
71
72fn get_cargo() -> String {
73 // On Windows when executed via `cargo run` (e.g. via the xtask alias) the
74 // `cargo` on the search path is NOT the cargo-wrapper but the `cargo` from the
75 // toolchain - that one doesn't understand `+toolchain`
76 #[cfg(target_os = "windows")]
77 let cargo = if let Ok(cargo) = std::env::var("CARGO_HOME") {
78 format!("{cargo}/bin/cargo")
79 } else {
80 String::from("cargo")
81 };
82
83 #[cfg(not(target_os = "windows"))]
84 let cargo = String::from("cargo");
85
86 cargo
87}
88
89#[derive(Debug, Default)]
90pub struct CargoArgsBuilder {
91 toolchain: Option<String>,
92 subcommand: String,
93 target: Option<String>,
94 features: Vec<String>,
95 args: Vec<String>,
96}
97
98impl CargoArgsBuilder {
99 #[must_use]
100 pub fn new() -> Self {
101 Self {
102 toolchain: None,
103 subcommand: String::new(),
104 target: None,
105 features: vec![],
106 args: vec![],
107 }
108 }
109
110 #[must_use]
111 pub fn toolchain<S>(mut self, toolchain: S) -> Self
112 where
113 S: Into<String>,
114 {
115 self.toolchain = Some(toolchain.into());
116 self
117 }
118
119 #[must_use]
120 pub fn subcommand<S>(mut self, subcommand: S) -> Self
121 where
122 S: Into<String>,
123 {
124 self.subcommand = subcommand.into();
125 self
126 }
127
128 #[must_use]
129 pub fn target<S>(mut self, target: S) -> Self
130 where
131 S: Into<String>,
132 {
133 self.target = Some(target.into());
134 self
135 }
136
137 #[must_use]
138 pub fn features(mut self, features: &[String]) -> Self {
139 self.features = features.to_vec();
140 self
141 }
142
143 #[must_use]
144 pub fn artifact_dir<S>(mut self, artifact_dir: S) -> Self
145 where
146 S: Into<String>,
147 {
148 self.args.push(format!("--artifact-dir={}", artifact_dir.into()));
149 self
150 }
151
152 #[must_use]
153 pub fn arg<S>(mut self, arg: S) -> Self
154 where
155 S: Into<String>,
156 {
157 self.args.push(arg.into());
158 self
159 }
160
161 #[must_use]
162 pub fn build(&self) -> Vec<String> {
163 let mut args = vec![];
164
165 if let Some(ref toolchain) = self.toolchain {
166 args.push(format!("+{toolchain}"));
167 }
168
169 args.push(self.subcommand.clone());
170
171 if let Some(ref target) = self.target {
172 args.push(format!("--target={target}"));
173 }
174
175 if !self.features.is_empty() {
176 args.push(format!("--features={}", self.features.join(",")));
177 }
178
179 for arg in self.args.iter() {
180 args.push(arg.clone());
181 }
182
183 args
184 }
185}
186
187#[derive(Debug, Default)]
188pub struct CargoBatchBuilder {
189 commands: Vec<Vec<String>>,
190}
191
192impl CargoBatchBuilder {
193 #[must_use]
194 pub fn new() -> Self {
195 Self { commands: vec![] }
196 }
197
198 #[must_use]
199 pub fn command(mut self, args: Vec<String>) -> Self {
200 self.commands.push(args);
201 self
202 }
203
204 pub fn add_command(&mut self, args: Vec<String>) -> &mut Self {
205 self.commands.push(args);
206 self
207 }
208
209 #[must_use]
210 pub fn build(&self) -> Vec<String> {
211 let mut args = vec!["batch".to_string()];
212
213 for command in &self.commands {
214 args.push("---".to_string());
215 args.extend(command.clone());
216 }
217
218 args
219 }
220}
diff --git a/release/src/main.rs b/release/src/main.rs
deleted file mode 100644
index 22c926b86..000000000
--- a/release/src/main.rs
+++ /dev/null
@@ -1,514 +0,0 @@
1use std::collections::{BTreeMap, HashMap};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command as ProcessCommand;
5
6use anyhow::{anyhow, bail, Result};
7use cargo_semver_checks::ReleaseType;
8use clap::{Parser, Subcommand};
9use petgraph::graph::{Graph, NodeIndex};
10use petgraph::visit::Bfs;
11use petgraph::{Directed, Direction};
12use simple_logger::SimpleLogger;
13use toml_edit::{DocumentMut, Item, Value};
14use types::*;
15
16fn check_publish_dependencies(ctx: &Context) -> Result<()> {
17 for krate in ctx.crates.values() {
18 if krate.publish {
19 for dep_name in &krate.dependencies {
20 if let Some(dep_crate) = ctx.crates.get(dep_name) {
21 if !dep_crate.publish {
22 return Err(anyhow!(
23 "Publishable crate '{}' depends on non-publishable crate '{}'. This is not allowed.",
24 krate.name,
25 dep_name
26 ));
27 }
28 }
29 }
30 }
31 }
32 Ok(())
33}
34
35mod build;
36mod cargo;
37mod semver_check;
38mod types;
39
40/// Tool to traverse and operate on intra-repo Rust crate dependencies
41#[derive(Parser, Debug)]
42#[command(author, version, about)]
43struct Args {
44 /// Command to perform on each crate
45 #[command(subcommand)]
46 command: Command,
47}
48
49#[derive(Debug, Subcommand)]
50enum Command {
51 /// All crates and their direct dependencies
52 List,
53 /// List all dependencies for a crate
54 Dependencies {
55 /// Crate name to print dependencies for.
56 #[arg(value_name = "CRATE")]
57 crate_name: String,
58 },
59 /// List all dependencies for a crate
60 Dependents {
61 /// Crate name to print dependencies for.
62 #[arg(value_name = "CRATE")]
63 crate_name: String,
64 },
65
66 /// Build
67 Build {
68 /// Crate to check. If not specified checks all crates.
69 #[arg(value_name = "CRATE")]
70 crate_name: Option<String>,
71 },
72 /// SemverCheck
73 SemverCheck {
74 /// Specific crate name to check
75 #[arg(value_name = "CRATE")]
76 crate_name: String,
77 },
78 /// Prepare to release a crate and all dependents that needs updating
79 /// - Semver checks
80 /// - Bump versions and commit
81 /// - Create tag.
82 PrepareRelease {
83 /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates.
84 #[arg(value_name = "CRATE")]
85 crate_name: String,
86 },
87}
88
89fn update_version(c: &mut Crate, new_version: &str) -> Result<()> {
90 let path = c.path.join("Cargo.toml");
91 c.version = new_version.to_string();
92 let content = fs::read_to_string(&path)?;
93 let mut doc: DocumentMut = content.parse()?;
94 for section in ["package"] {
95 if let Some(Item::Table(dep_table)) = doc.get_mut(section) {
96 dep_table.insert("version", Item::Value(Value::from(new_version)));
97 }
98 }
99 fs::write(&path, doc.to_string())?;
100 Ok(())
101}
102
103fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> {
104 let path = to_update.path.join("Cargo.toml");
105 let content = fs::read_to_string(&path)?;
106 let mut doc: DocumentMut = content.parse()?;
107 let mut changed = false;
108 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
109 if let Some(Item::Table(dep_table)) = doc.get_mut(section) {
110 if let Some(item) = dep_table.get_mut(&dep) {
111 match item {
112 // e.g., foo = "0.1.0"
113 Item::Value(Value::String(_)) => {
114 *item = Item::Value(Value::from(new_version));
115 changed = true;
116 }
117 // e.g., foo = { version = "...", ... }
118 Item::Value(Value::InlineTable(inline)) => {
119 if inline.contains_key("version") {
120 inline["version"] = Value::from(new_version);
121 changed = true;
122 }
123 }
124 _ => {} // Leave unusual formats untouched
125 }
126 }
127 }
128 }
129
130 if changed {
131 fs::write(&path, doc.to_string())?;
132 println!("🔧 Updated {} to {} in {}", dep, new_version, path.display());
133 }
134 Ok(())
135}
136
137fn list_crates(root: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> {
138 let mut crates = BTreeMap::new();
139 discover_crates(root, &mut crates)?;
140 Ok(crates)
141}
142
143fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap<CrateId, Crate>) -> Result<()> {
144 let d = std::fs::read_dir(dir)?;
145 for c in d {
146 let entry = c?;
147 if entry.file_type()?.is_dir() {
148 let path = dir.join(entry.path());
149 let cargo_toml = path.join("Cargo.toml");
150
151 if cargo_toml.exists() {
152 let content = fs::read_to_string(&cargo_toml)?;
153
154 // Try to parse as a crate, skip if it's a workspace
155 let parsed: Result<ParsedCrate, _> = toml::from_str(&content);
156 if let Ok(parsed) = parsed {
157 let id = parsed.package.name;
158
159 let metadata = &parsed.package.metadata.embassy;
160
161 if metadata.skip {
162 continue;
163 }
164
165 let mut dependencies = Vec::new();
166 for (k, _) in parsed.dependencies {
167 if k.starts_with("embassy-") {
168 dependencies.push(k);
169 }
170 }
171
172 let mut configs = metadata.build.clone();
173 if configs.is_empty() {
174 configs.push(BuildConfig {
175 features: vec![],
176 target: None,
177 artifact_dir: None,
178 })
179 }
180
181 crates.insert(
182 id.clone(),
183 Crate {
184 name: id,
185 version: parsed.package.version,
186 path,
187 dependencies,
188 configs,
189 publish: parsed.package.publish,
190 },
191 );
192 }
193 } else {
194 // Recursively search subdirectories, but only for examples, tests, and docs
195 let file_name = entry.file_name();
196 let dir_name = file_name.to_string_lossy();
197 if dir_name == "examples" || dir_name == "tests" || dir_name == "docs" {
198 discover_crates(&path, crates)?;
199 }
200 }
201 }
202 }
203 Ok(())
204}
205
206fn build_graph(crates: &BTreeMap<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) {
207 let mut graph = Graph::<CrateId, (), Directed>::new();
208 let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new();
209
210 // Helper to insert or get existing node
211 let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| {
212 if let Some(&idx) = map.get(&id) {
213 idx
214 } else {
215 let idx = graph.add_node(id.clone());
216 map.insert(id, idx);
217 idx
218 }
219 };
220
221 for krate in crates.values() {
222 get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices);
223 }
224
225 for krate in crates.values() {
226 // Insert crate node if not exists
227 let crate_idx = get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices);
228
229 // Insert dependencies and connect edges
230 for dep in krate.dependencies.iter() {
231 let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices);
232 graph.add_edge(crate_idx, dep_idx, ());
233 }
234 }
235
236 (graph, node_indices)
237}
238
239struct Context {
240 root: PathBuf,
241 crates: BTreeMap<String, Crate>,
242 graph: Graph<String, ()>,
243 indices: HashMap<String, NodeIndex>,
244}
245
246fn find_repo_root() -> Result<PathBuf> {
247 let mut path = std::env::current_dir()?.canonicalize()?;
248
249 loop {
250 // Check if this directory contains a .git directory
251 if path.join(".git").exists() {
252 return Ok(path);
253 }
254
255 // Move to parent directory
256 match path.parent() {
257 Some(parent) => path = parent.to_path_buf(),
258 None => break,
259 }
260 }
261
262 Err(anyhow!(
263 "Could not find repository root. Make sure you're running this tool from within the embassy repository."
264 ))
265}
266
267fn load_context() -> Result<Context> {
268 let root = find_repo_root()?;
269 let crates = list_crates(&root)?;
270 let (graph, indices) = build_graph(&crates);
271
272 let ctx = Context {
273 root,
274 crates,
275 graph,
276 indices,
277 };
278
279 // Check for publish dependency conflicts
280 check_publish_dependencies(&ctx)?;
281
282 Ok(ctx)
283}
284
285fn main() -> Result<()> {
286 SimpleLogger::new().init().unwrap();
287 let args = Args::parse();
288 let mut ctx = load_context()?;
289
290 match args.command {
291 Command::List => {
292 let ordered = petgraph::algo::toposort(&ctx.graph, None).unwrap();
293 for node in ordered.iter() {
294 let start = ctx.graph.node_weight(*node).unwrap();
295 let mut bfs = Bfs::new(&ctx.graph, *node);
296 while let Some(node) = bfs.next(&ctx.graph) {
297 let weight = ctx.graph.node_weight(node).unwrap();
298 let c = ctx.crates.get(weight).unwrap();
299 if weight == start {
300 println!("+ {}-{}", weight, c.version);
301 } else {
302 println!("|- {}-{}", weight, c.version);
303 }
304 }
305 println!("");
306 }
307 }
308 Command::Dependencies { crate_name } => {
309 let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
310 let mut bfs = Bfs::new(&ctx.graph, *idx);
311 while let Some(node) = bfs.next(&ctx.graph) {
312 let weight = ctx.graph.node_weight(node).unwrap();
313 let crt = ctx.crates.get(weight).unwrap();
314 if *weight == crate_name {
315 println!("+ {}-{}", weight, crt.version);
316 } else {
317 println!("|- {}-{}", weight, crt.version);
318 }
319 }
320 }
321 Command::Dependents { crate_name } => {
322 let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
323 let weight = ctx.graph.node_weight(*idx).unwrap();
324 let crt = ctx.crates.get(weight).unwrap();
325 println!("+ {}-{}", weight, crt.version);
326 for parent in ctx.graph.neighbors_directed(*idx, Direction::Incoming) {
327 let weight = ctx.graph.node_weight(parent).unwrap();
328 let crt = ctx.crates.get(weight).unwrap();
329 println!("|- {}-{}", weight, crt.version);
330 }
331 }
332 Command::Build { crate_name } => {
333 build::build(&ctx, crate_name.as_deref())?;
334 }
335 Command::SemverCheck { crate_name } => {
336 let c = ctx.crates.get(&crate_name).unwrap();
337 if !c.publish {
338 bail!("Cannot run semver-check on non-publishable crate '{}'", crate_name);
339 }
340 check_semver(&c)?;
341 }
342 Command::PrepareRelease { crate_name } => {
343 let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree");
344
345 // Check if the target crate is publishable
346 let start_weight = ctx.graph.node_weight(*start).unwrap();
347 let start_crate = ctx.crates.get(start_weight).unwrap();
348 if !start_crate.publish {
349 bail!("Cannot prepare release for non-publishable crate '{}'", crate_name);
350 }
351
352 let mut rgraph = ctx.graph.clone();
353 rgraph.reverse();
354
355 let mut bfs = Bfs::new(&rgraph, *start);
356
357 while let Some(node) = bfs.next(&rgraph) {
358 let weight = rgraph.node_weight(node).unwrap();
359 println!("Preparing {}", weight);
360 let mut c = ctx.crates.get_mut(weight).unwrap();
361 if c.publish {
362 let ver = semver::Version::parse(&c.version)?;
363 let newver = match check_semver(&c)? {
364 ReleaseType::Major | ReleaseType::Minor => semver::Version::new(ver.major, ver.minor + 1, 0),
365 ReleaseType::Patch => semver::Version::new(ver.major, ver.minor, ver.patch + 1),
366 _ => unreachable!(),
367 };
368
369 println!("Updating {} from {} -> {}", weight, c.version, newver.to_string());
370 let newver = newver.to_string();
371
372 update_version(&mut c, &newver)?;
373 let c = ctx.crates.get(weight).unwrap();
374
375 // Update all nodes further down the tree
376 let mut bfs = Bfs::new(&rgraph, node);
377 while let Some(dep_node) = bfs.next(&rgraph) {
378 let dep_weight = rgraph.node_weight(dep_node).unwrap();
379 let dep = ctx.crates.get(dep_weight).unwrap();
380 update_versions(dep, &c.name, &newver)?;
381 }
382
383 // Update changelog
384 update_changelog(&ctx.root, &c)?;
385 }
386 }
387
388 let weight = rgraph.node_weight(*start).unwrap();
389 let c = ctx.crates.get(weight).unwrap();
390 publish_release(&ctx.root, &c, false)?;
391
392 println!("# Please inspect changes and run the following commands when happy:");
393
394 println!("git commit -a -m 'chore: prepare crate releases'");
395 let mut bfs = Bfs::new(&rgraph, *start);
396 while let Some(node) = bfs.next(&rgraph) {
397 let weight = rgraph.node_weight(node).unwrap();
398 let c = ctx.crates.get(weight).unwrap();
399 if c.publish {
400 println!("git tag {}-v{}", weight, c.version);
401 }
402 }
403
404 println!("");
405 println!("# Run these commands to publish the crate and dependents:");
406
407 let mut bfs = Bfs::new(&rgraph, *start);
408 while let Some(node) = bfs.next(&rgraph) {
409 let weight = rgraph.node_weight(node).unwrap();
410 let c = ctx.crates.get(weight).unwrap();
411
412 let mut args: Vec<String> = vec![
413 "publish".to_string(),
414 "--manifest-path".to_string(),
415 c.path.join("Cargo.toml").display().to_string(),
416 ];
417
418 let config = c.configs.first().unwrap(); // TODO
419 if !config.features.is_empty() {
420 args.push("--features".into());
421 args.push(config.features.join(","));
422 }
423
424 if let Some(target) = &config.target {
425 args.push("--target".into());
426 args.push(target.clone());
427 }
428
429 /*
430 let mut dry_run = args.clone();
431 dry_run.push("--dry-run".to_string());
432
433 println!("cargo {}", dry_run.join(" "));
434 */
435 if c.publish {
436 println!("cargo {}", args.join(" "));
437 }
438 }
439
440 println!("");
441 println!("# Run this command to push changes and tags:");
442 println!("git push --tags");
443 }
444 }
445 Ok(())
446}
447
448fn check_semver(c: &Crate) -> Result<ReleaseType> {
449 let min_version = semver_check::minimum_update(c)?;
450 println!("Version should be bumped to {:?}", min_version);
451 Ok(min_version)
452}
453
454fn update_changelog(repo: &Path, c: &Crate) -> Result<()> {
455 let args: Vec<String> = vec![
456 "release".to_string(),
457 "replace".to_string(),
458 "--config".to_string(),
459 repo.join("release").join("release.toml").display().to_string(),
460 "--manifest-path".to_string(),
461 c.path.join("Cargo.toml").display().to_string(),
462 "--execute".to_string(),
463 "--no-confirm".to_string(),
464 ];
465
466 let status = ProcessCommand::new("cargo").args(&args).output()?;
467
468 println!("{}", core::str::from_utf8(&status.stdout).unwrap());
469 eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap());
470 if !status.status.success() {
471 return Err(anyhow!("release replace failed"));
472 } else {
473 Ok(())
474 }
475}
476
477fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> {
478 let config = c.configs.first().unwrap(); // TODO
479
480 let mut args: Vec<String> = vec![
481 "publish".to_string(),
482 "--manifest-path".to_string(),
483 c.path.join("Cargo.toml").display().to_string(),
484 ];
485
486 args.push("--features".into());
487 args.push(config.features.join(","));
488
489 if let Some(target) = &config.target {
490 args.push("--target".into());
491 args.push(target.clone());
492 }
493
494 if !push {
495 args.push("--dry-run".to_string());
496 args.push("--allow-dirty".to_string());
497 args.push("--keep-going".to_string());
498 }
499
500 let status = ProcessCommand::new("cargo").args(&args).output()?;
501
502 println!("{}", core::str::from_utf8(&status.stdout).unwrap());
503 eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap());
504 if !status.status.success() {
505 return Err(anyhow!("publish failed"));
506 } else {
507 Ok(())
508 }
509}
510
511/// Make the path "Windows"-safe
512pub fn windows_safe_path(path: &Path) -> PathBuf {
513 PathBuf::from(path.to_str().unwrap().to_string().replace("\\\\?\\", ""))
514}
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 @@
1use std::collections::HashSet;
2use std::env;
3use std::path::PathBuf;
4
5use anyhow::anyhow;
6use cargo_semver_checks::{Check, GlobalConfig, ReleaseType, Rustdoc};
7use flate2::read::GzDecoder;
8use tar::Archive;
9
10use crate::cargo::CargoArgsBuilder;
11use crate::types::{BuildConfig, Crate};
12
13/// Return the minimum required bump for the next release.
14/// Even if nothing changed this will be [ReleaseType::Patch]
15pub fn minimum_update(krate: &Crate) -> Result<ReleaseType, anyhow::Error> {
16 let config = krate.configs.first().unwrap(); // TODO
17
18 let package_name = krate.name.clone();
19 let baseline_path = download_baseline(&package_name, &krate.version)?;
20 let mut baseline_krate = krate.clone();
21 baseline_krate.path = baseline_path;
22
23 // Compare features as it's not covered by semver-checks
24 if compare_features(&baseline_krate, &krate)? {
25 return Ok(ReleaseType::Minor);
26 }
27 let baseline_path = build_doc_json(&baseline_krate, config)?;
28 let current_path = build_doc_json(krate, config)?;
29
30 let baseline = Rustdoc::from_path(&baseline_path);
31 let doc = Rustdoc::from_path(&current_path);
32 let mut semver_check = Check::new(doc);
33 semver_check.with_default_features();
34 semver_check.set_baseline(baseline);
35 semver_check.set_packages(vec![package_name]);
36 let extra_current_features = config.features.clone();
37 let extra_baseline_features = config.features.clone();
38 semver_check.set_extra_features(extra_current_features, extra_baseline_features);
39 if let Some(target) = &config.target {
40 semver_check.set_build_target(target.clone());
41 }
42 let mut cfg = GlobalConfig::new();
43 cfg.set_log_level(Some(log::Level::Info));
44
45 let result = semver_check.check_release(&mut cfg)?;
46
47 let mut min_required_update = ReleaseType::Patch;
48 for (_, report) in result.crate_reports() {
49 if let Some(required_bump) = report.required_bump() {
50 let required_is_stricter =
51 (min_required_update == ReleaseType::Patch) || (required_bump == ReleaseType::Major);
52 if required_is_stricter {
53 min_required_update = required_bump;
54 }
55 }
56 }
57
58 Ok(min_required_update)
59}
60
61fn compare_features(old: &Crate, new: &Crate) -> Result<bool, anyhow::Error> {
62 let mut old = read_features(&old.path)?;
63 let new = read_features(&new.path)?;
64
65 old.retain(|r| !new.contains(r));
66 log::info!("Features removed in new: {:?}", old);
67 Ok(!old.is_empty())
68}
69
70fn download_baseline(name: &str, version: &str) -> Result<PathBuf, anyhow::Error> {
71 let config = crates_index::IndexConfig {
72 dl: "https://crates.io/api/v1/crates".to_string(),
73 api: Some("https://crates.io".to_string()),
74 };
75
76 let url =
77 config
78 .download_url(name, version)
79 .ok_or(anyhow!("unable to download baseline for {}-{}", name, version))?;
80
81 let parent_dir = env::var("RELEASER_CACHE").map_err(|_| anyhow!("RELEASER_CACHE not set"))?;
82
83 let extract_path = PathBuf::from(&parent_dir).join(format!("{}-{}", name, version));
84
85 if extract_path.exists() {
86 return Ok(extract_path);
87 }
88
89 let response = reqwest::blocking::get(url)?;
90 let bytes = response.bytes()?;
91
92 let decoder = GzDecoder::new(&bytes[..]);
93 let mut archive = Archive::new(decoder);
94 archive.unpack(&parent_dir)?;
95
96 Ok(extract_path)
97}
98
99fn read_features(crate_path: &PathBuf) -> Result<HashSet<String>, anyhow::Error> {
100 let cargo_toml_path = crate_path.join("Cargo.toml");
101
102 if !cargo_toml_path.exists() {
103 return Err(anyhow!("Cargo.toml not found at {:?}", cargo_toml_path));
104 }
105
106 let manifest = cargo_manifest::Manifest::from_path(&cargo_toml_path)?;
107
108 let mut set = HashSet::new();
109 if let Some(features) = manifest.features {
110 for f in features.keys() {
111 set.insert(f.clone());
112 }
113 }
114 if let Some(deps) = manifest.dependencies {
115 for (k, v) in deps.iter() {
116 if v.optional() {
117 set.insert(k.clone());
118 }
119 }
120 }
121
122 Ok(set)
123}
124
125fn build_doc_json(krate: &Crate, config: &BuildConfig) -> Result<PathBuf, anyhow::Error> {
126 let target_dir = std::env::var("CARGO_TARGET_DIR");
127
128 let target_path = if let Ok(target) = target_dir {
129 PathBuf::from(target)
130 } else {
131 PathBuf::from(&krate.path).join("target")
132 };
133
134 let current_path = target_path;
135 let current_path = if let Some(target) = &config.target {
136 current_path.join(target.clone())
137 } else {
138 current_path
139 };
140 let current_path = current_path
141 .join("doc")
142 .join(format!("{}.json", krate.name.to_string().replace("-", "_")));
143
144 std::fs::remove_file(&current_path).ok();
145 let features = config.features.clone();
146
147 log::info!("Building doc json for {} with features: {:?}", krate.name, features);
148
149 let envs = vec![(
150 "RUSTDOCFLAGS",
151 "--cfg docsrs --cfg not_really_docsrs --cfg semver_checks",
152 )];
153
154 // always use `specific nightly` toolchain so we don't have to deal with potentially
155 // different versions of the doc-json
156 let cargo_builder = CargoArgsBuilder::default()
157 .toolchain("nightly-2025-06-29")
158 .subcommand("rustdoc")
159 .features(&features);
160 let cargo_builder = if let Some(target) = &config.target {
161 cargo_builder.target(target.clone())
162 } else {
163 cargo_builder
164 };
165
166 let cargo_builder = cargo_builder
167 .arg("-Zunstable-options")
168 .arg("-Zhost-config")
169 .arg("-Ztarget-applies-to-host")
170 .arg("--lib")
171 .arg("--output-format=json")
172 .arg("-Zbuild-std=alloc,core")
173 .arg("--config=host.rustflags=[\"--cfg=instability_disable_unstable_docs\"]");
174 let cargo_args = cargo_builder.build();
175 log::debug!("{cargo_args:#?}");
176 crate::cargo::run_with_env(&cargo_args, &krate.path, envs, false)?;
177 Ok(current_path)
178}
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 @@
1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::Deserialize;
5
6#[derive(Debug, Deserialize)]
7pub struct ParsedCrate {
8 pub package: ParsedPackage,
9 pub dependencies: BTreeMap<String, toml::Value>,
10}
11
12#[derive(Debug, Deserialize)]
13pub struct ParsedPackage {
14 pub name: String,
15 pub version: String,
16 #[serde(default = "default_publish")]
17 pub publish: bool,
18 #[serde(default)]
19 pub metadata: Metadata,
20}
21
22fn default_publish() -> bool {
23 true
24}
25
26#[derive(Debug, Deserialize, Default)]
27pub struct Metadata {
28 #[serde(default)]
29 pub embassy: MetadataEmbassy,
30}
31
32#[allow(dead_code)]
33#[derive(Debug, Deserialize, Default)]
34pub struct MetadataEmbassy {
35 #[serde(default)]
36 pub skip: bool,
37 #[serde(default)]
38 pub build: Vec<BuildConfig>,
39}
40
41#[derive(Debug, Clone, Deserialize)]
42pub struct BuildConfig {
43 #[serde(default)]
44 pub features: Vec<String>,
45 pub target: Option<String>,
46 #[serde(rename = "artifact-dir")]
47 pub artifact_dir: Option<String>,
48}
49
50pub type CrateId = String;
51
52#[derive(Debug, Clone)]
53pub struct Crate {
54 pub name: String,
55 pub version: String,
56 pub path: PathBuf,
57 pub dependencies: Vec<CrateId>,
58 pub configs: Vec<BuildConfig>,
59 pub publish: bool,
60}