chore: move tool to separate repo

This commit is contained in:
Ulf Lilleengen 2025-08-19 13:58:25 +02:00
parent b00496fd29
commit d835b53857
6 changed files with 0 additions and 1049 deletions

View File

@ -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

View File

@ -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(())
}

View File

@ -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<I, K, V>(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result<String>
where
I: IntoIterator<Item = (K, V)> + core::fmt::Debug,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
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<String>,
subcommand: String,
target: Option<String>,
features: Vec<String>,
args: Vec<String>,
}
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<S>(mut self, toolchain: S) -> Self
where
S: Into<String>,
{
self.toolchain = Some(toolchain.into());
self
}
#[must_use]
pub fn subcommand<S>(mut self, subcommand: S) -> Self
where
S: Into<String>,
{
self.subcommand = subcommand.into();
self
}
#[must_use]
pub fn target<S>(mut self, target: S) -> Self
where
S: Into<String>,
{
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<S>(mut self, artifact_dir: S) -> Self
where
S: Into<String>,
{
self.args.push(format!("--artifact-dir={}", artifact_dir.into()));
self
}
#[must_use]
pub fn arg<S>(mut self, arg: S) -> Self
where
S: Into<String>,
{
self.args.push(arg.into());
self
}
#[must_use]
pub fn build(&self) -> Vec<String> {
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<Vec<String>>,
}
impl CargoBatchBuilder {
#[must_use]
pub fn new() -> Self {
Self { commands: vec![] }
}
#[must_use]
pub fn command(mut self, args: Vec<String>) -> Self {
self.commands.push(args);
self
}
pub fn add_command(&mut self, args: Vec<String>) -> &mut Self {
self.commands.push(args);
self
}
#[must_use]
pub fn build(&self) -> Vec<String> {
let mut args = vec!["batch".to_string()];
for command in &self.commands {
args.push("---".to_string());
args.extend(command.clone());
}
args
}
}

View File

@ -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<String>,
},
/// 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<BTreeMap<CrateId, Crate>> {
let mut crates = BTreeMap::new();
discover_crates(root, &mut crates)?;
Ok(crates)
}
fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap<CrateId, Crate>) -> 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<ParsedCrate, _> = 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<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) {
let mut graph = Graph::<CrateId, (), Directed>::new();
let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new();
// Helper to insert or get existing node
let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| {
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<String, Crate>,
graph: Graph<String, ()>,
indices: HashMap<String, NodeIndex>,
}
fn find_repo_root() -> Result<PathBuf> {
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<Context> {
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<String> = 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<ReleaseType> {
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<String> = 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<String> = 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("\\\\?\\", ""))
}

View File

@ -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<ReleaseType, anyhow::Error> {
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(&current_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<bool, anyhow::Error> {
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<PathBuf, anyhow::Error> {
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<HashSet<String>, 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<PathBuf, anyhow::Error> {
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(&current_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)
}

View File

@ -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<String, toml::Value>,
}
#[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<BuildConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BuildConfig {
#[serde(default)]
pub features: Vec<String>,
pub target: Option<String>,
#[serde(rename = "artifact-dir")]
pub artifact_dir: Option<String>,
}
pub type CrateId = String;
#[derive(Debug, Clone)]
pub struct Crate {
pub name: String,
pub version: String,
pub path: PathBuf,
pub dependencies: Vec<CrateId>,
pub configs: Vec<BuildConfig>,
pub publish: bool,
}