mirror of
https://github.com/embassy-rs/embassy.git
synced 2025-09-26 20:00:27 +00:00
chore: move tool to separate repo
This commit is contained in:
parent
b00496fd29
commit
d835b53857
@ -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
|
@ -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(())
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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("\\\\?\\", ""))
|
||||
}
|
@ -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(¤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<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(¤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)
|
||||
}
|
@ -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,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user