aboutsummaryrefslogtreecommitdiff
path: root/.run/src
diff options
context:
space:
mode:
Diffstat (limited to '.run/src')
-rw-r--r--.run/src/bin/RELEASE-WORKSPACE.rs145
-rw-r--r--.run/src/bin/display-dependency-order.rs12
-rw-r--r--.run/src/dependency_order.rs183
-rw-r--r--.run/src/lib.rs33
4 files changed, 370 insertions, 3 deletions
diff --git a/.run/src/bin/RELEASE-WORKSPACE.rs b/.run/src/bin/RELEASE-WORKSPACE.rs
new file mode 100644
index 0000000..8cb4d1b
--- /dev/null
+++ b/.run/src/bin/RELEASE-WORKSPACE.rs
@@ -0,0 +1,145 @@
+use std::path::{Path, PathBuf};
+use std::process;
+use tools::dependency_order::{display_dependency_order, find_workspace_root};
+use tools::{
+ eprintln_cargo_style, println_cargo_style, run_cmd_capture_with_dir, run_cmd_with_dir,
+};
+
+/// Read the version from `[workspace.package].version` in the root Cargo.toml.
+fn read_workspace_version(workspace_root: &Path) -> Option<String> {
+ let content = std::fs::read_to_string(workspace_root.join("Cargo.toml")).ok()?;
+ let value: toml::Value = content.parse().ok()?;
+ value
+ .get("workspace")?
+ .get("package")?
+ .get("version")?
+ .as_str()
+ .map(String::from)
+}
+
+/// Replace `path = "crate_name"` with `version = "VERSION"` in the file content.
+fn update_path_to_version(content: &str, crate_name: &str, version: &str) -> String {
+ let old = format!("path = \"{}\"", crate_name);
+ let new = format!("version = \"{}\"", version);
+ content.replace(&old, &new)
+}
+
+/// Restore the backup and abort.
+fn abort(toml_path: &Path) -> ! {
+ let backup_path = toml_path.with_extension("toml.bkup");
+ if backup_path.exists() {
+ if let Err(e) = std::fs::copy(&backup_path, toml_path) {
+ eprintln_cargo_style!("failed to restore Cargo.toml from backup: {}", e);
+ } else {
+ eprintln_cargo_style!("restored Cargo.toml from backup");
+ }
+ }
+ process::exit(1);
+}
+
+fn main() {
+ // 1. Compute dependency order
+ let order = display_dependency_order();
+ if order.is_empty() {
+ eprintln_cargo_style!("could not find workspace root or mingling crates");
+ process::exit(1);
+ }
+
+ // 2. Locate workspace root
+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+ let workspace_root = match find_workspace_root(&cwd) {
+ Some(r) => r,
+ None => {
+ eprintln_cargo_style!("could not find workspace root");
+ process::exit(1);
+ }
+ };
+
+ // 3. Read current version
+ let version = match read_workspace_version(&workspace_root) {
+ Some(v) => v,
+ None => {
+ eprintln_cargo_style!("could not read workspace.package.version from Cargo.toml");
+ process::exit(1);
+ }
+ };
+
+ println_cargo_style!("Version: detected version {}", version);
+
+ let toml_path = workspace_root.join("Cargo.toml");
+ let backup_path = workspace_root.join("Cargo.toml.bkup");
+
+ // 4. Backup Cargo.toml
+ if let Err(e) = std::fs::copy(&toml_path, &backup_path) {
+ eprintln_cargo_style!("failed to backup Cargo.toml: {}", e);
+ process::exit(1);
+ }
+ println_cargo_style!("Backup: created Cargo.toml.bkup");
+
+ // 5. Iterate crates in dependency order
+ for crate_name in &order {
+ let name = match crate_name.to_str() {
+ Some(n) => n,
+ None => {
+ eprintln_cargo_style!("invalid crate path: {}", crate_name.display());
+ abort(&toml_path);
+ }
+ };
+
+ let crate_dir = workspace_root.join(crate_name);
+
+ // 5a. Publish (capture output to check for warnings)
+ println_cargo_style!("Publish: {}", name);
+ let output = run_cmd_capture_with_dir("cargo publish", &crate_dir);
+ let output_str = match output {
+ Ok(o) => o,
+ Err((code, _)) => {
+ eprintln_cargo_style!("{} publish failed (exit code {})", name, code);
+ abort(&toml_path);
+ }
+ };
+
+ // Reject any warnings
+ let warnings: Vec<&str> = output_str
+ .lines()
+ .filter(|l| l.to_lowercase().contains("warning:"))
+ .collect();
+ if !warnings.is_empty() {
+ for w in &warnings {
+ eprintln_cargo_style!("warning detected: {}", w);
+ }
+ eprintln_cargo_style!(
+ "{} publish rejected due to {} warning(s)",
+ name,
+ warnings.len()
+ );
+ abort(&toml_path);
+ }
+
+ // 5b. Replace path with version in Cargo.toml
+ let content = match std::fs::read_to_string(&toml_path) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln_cargo_style!("failed to read Cargo.toml: {}", e);
+ abort(&toml_path);
+ }
+ };
+ let new_content = update_path_to_version(&content, name, &version);
+ if let Err(e) = std::fs::write(&toml_path, &new_content) {
+ eprintln_cargo_style!("failed to write Cargo.toml: {}", e);
+ abort(&toml_path);
+ }
+ println_cargo_style!("Dependency: {} path -> version \"{}\"", name, version);
+
+ // 5c. Verify workspace compiles
+ println_cargo_style!("Check: workspace");
+ if let Err(code) = run_cmd_with_dir("cargo check --workspace", &workspace_root) {
+ eprintln_cargo_style!("cargo check --workspace failed (exit code {})", code);
+ abort(&toml_path);
+ }
+
+ println_cargo_style!("Done: `{}` published", name);
+ }
+
+ println_cargo_style!("Done: all crates published successfully");
+}
diff --git a/.run/src/bin/display-dependency-order.rs b/.run/src/bin/display-dependency-order.rs
new file mode 100644
index 0000000..a31c67a
--- /dev/null
+++ b/.run/src/bin/display-dependency-order.rs
@@ -0,0 +1,12 @@
+use tools::{dependency_order::display_dependency_order, eprintln_cargo_style};
+
+fn main() {
+ let order = display_dependency_order();
+ if order.is_empty() {
+ eprintln_cargo_style!("could not find workspace root or mingling crates");
+ std::process::exit(1);
+ }
+ for path in order {
+ println!("{}", path.display());
+ }
+}
diff --git a/.run/src/dependency_order.rs b/.run/src/dependency_order.rs
new file mode 100644
index 0000000..7235f12
--- /dev/null
+++ b/.run/src/dependency_order.rs
@@ -0,0 +1,183 @@
+use std::collections::{HashMap, HashSet};
+use std::path::PathBuf;
+
+/// Parse Cargo.toml content and return dependency names that start with `prefix`.
+fn parse_mingling_deps(content: &str, prefix: &str) -> Vec<String> {
+ let value: toml::Value = match content.parse() {
+ Ok(v) => v,
+ Err(_) => return Vec::new(),
+ };
+
+ let mut names = Vec::new();
+
+ // Check [dependencies]
+ if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) {
+ for key in deps.keys() {
+ if key.starts_with(prefix) {
+ names.push(key.clone());
+ }
+ }
+ }
+
+ // Check [build-dependencies]
+ if let Some(deps) = value.get("build-dependencies").and_then(|d| d.as_table()) {
+ for key in deps.keys() {
+ if key.starts_with(prefix) {
+ names.push(key.clone());
+ }
+ }
+ }
+
+ names
+}
+
+/// Read workspace members from the root Cargo.toml.
+fn get_workspace_members(workspace_root: &std::path::Path) -> Vec<String> {
+ let cargo_path = workspace_root.join("Cargo.toml");
+ let content = match std::fs::read_to_string(&cargo_path) {
+ Ok(c) => c,
+ Err(_) => return Vec::new(),
+ };
+
+ let value: toml::Value = match content.parse() {
+ Ok(v) => v,
+ Err(_) => return Vec::new(),
+ };
+
+ value
+ .get("workspace")
+ .and_then(|w| w.get("members"))
+ .and_then(|m| m.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
+/// Hierarchical topological sort (process layer by layer, sort siblings alphabetically).
+///
+/// `dep_map` maps each crate to the list of crates it depends on.
+/// Returns the dependency order (dependent crates come first, dependents come later),
+/// with crates at the same layer (which can be built in parallel) sorted alphabetically.
+fn topological_sort(
+ all_crates: &HashSet<String>,
+ dep_map: &HashMap<String, Vec<String>>,
+) -> Vec<String> {
+ // in_degree[crate] = number of remaining mingling_* dependencies not yet processed
+ let mut in_degree: HashMap<&str, usize> = HashMap::new();
+ // reverse[dependency] = list of crates that depend on it
+ let mut reverse: HashMap<&str, Vec<&str>> = HashMap::new();
+
+ for name in all_crates {
+ in_degree.entry(name.as_str()).or_insert(0);
+ reverse.entry(name.as_str()).or_default();
+ }
+
+ for (crate_name, deps) in dep_map {
+ for dep in deps {
+ if all_crates.contains(dep.as_str()) {
+ reverse
+ .get_mut(dep.as_str())
+ .unwrap()
+ .push(crate_name.as_str());
+ *in_degree.get_mut(crate_name.as_str()).unwrap() += 1;
+ }
+ }
+ }
+
+ let mut result: Vec<String> = Vec::new();
+
+ // Process layer by layer: all crates with in_degree == 0 in one batch form a layer
+ loop {
+ let mut current: Vec<&str> = all_crates
+ .iter()
+ .filter(|n| in_degree.get(n.as_str()).copied().unwrap_or(0) == 0)
+ .filter(|n| !result.iter().any(|r| r.as_str() == n.as_str()))
+ .map(|s| s.as_str())
+ .collect();
+
+ if current.is_empty() {
+ break;
+ }
+
+ current.sort();
+ result.extend(current.iter().map(|s| s.to_string()));
+
+ for &node in &current {
+ if let Some(dependents) = reverse.get(node) {
+ for &dependent in dependents {
+ if let Some(degree) = in_degree.get_mut(dependent) {
+ *degree -= 1;
+ }
+ }
+ }
+ }
+ }
+
+ result
+}
+
+/// Find the workspace root by looking for a Cargo.toml with `[workspace]` members.
+/// Starts from `start` and walks up the directory tree.
+pub fn find_workspace_root(start: &std::path::Path) -> Option<PathBuf> {
+ let mut current = Some(std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()));
+ while let Some(dir) = current {
+ let members = get_workspace_members(&dir);
+ if !members.is_empty() {
+ return Some(dir);
+ }
+ current = dir.parent().map(|p| p.to_path_buf());
+ }
+ None
+}
+
+/// Output all crate paths in dependency order
+#[allow(unused)]
+pub fn display_dependency_order() -> Vec<PathBuf> {
+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+
+ let workspace_root = match find_workspace_root(&cwd) {
+ Some(root) => root,
+ None => return Vec::new(),
+ };
+
+ // Read workspace members from root Cargo.toml
+ let members = get_workspace_members(&workspace_root);
+
+ // Filter to crates starting with "mingling"
+ let mingling_crates: HashSet<String> = members
+ .into_iter()
+ .filter(|m| m.starts_with("mingling"))
+ .collect();
+
+ if mingling_crates.is_empty() {
+ return Vec::new();
+ }
+
+ // Build dependency graph
+ let mut dep_map: HashMap<String, Vec<String>> = HashMap::new();
+
+ for crate_name in &mingling_crates {
+ let cargo_path = workspace_root.join(crate_name).join("Cargo.toml");
+ let content = match std::fs::read_to_string(&cargo_path) {
+ Ok(c) => c,
+ Err(_) => {
+ dep_map.insert(crate_name.clone(), Vec::new());
+ continue;
+ }
+ };
+ let deps = parse_mingling_deps(&content, "mingling");
+ // Only keep deps that are actually in our set
+ let filtered: Vec<String> = deps
+ .into_iter()
+ .filter(|d| mingling_crates.contains(d.as_str()))
+ .collect();
+ dep_map.insert(crate_name.clone(), filtered);
+ }
+
+ let sorted = topological_sort(&mingling_crates, &dep_map);
+
+ sorted.into_iter().map(PathBuf::from).collect()
+}
diff --git a/.run/src/lib.rs b/.run/src/lib.rs
index 232afb3..d52c7fd 100644
--- a/.run/src/lib.rs
+++ b/.run/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod dependency_order;
pub mod verify;
use colored::Colorize;
@@ -124,7 +125,7 @@ pub fn wprintln_cargo_style(str: impl Into<String>) {
);
}
-/// Run a shell command and return its exit status.
+/// Run a shell command in the current directory and return its exit status.
///
/// # Panics
///
@@ -134,6 +135,20 @@ pub fn wprintln_cargo_style(str: impl Into<String>) {
///
/// Returns `Err` with the exit code if the command finishes with a non-zero exit code.
pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> {
+ let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
+ run_cmd_with_dir(cmd.into(), &cwd)
+}
+
+/// Run a shell command in the specified directory and return its exit status.
+///
+/// # Panics
+///
+/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found).
+///
+/// # Errors
+///
+/// Returns `Err` with the exit code if the command finishes with a non-zero exit code.
+pub fn run_cmd_with_dir(cmd: impl Into<String>, dir: &std::path::Path) -> Result<(), i32> {
let shell = if cfg!(target_os = "windows") {
"powershell"
} else {
@@ -142,7 +157,7 @@ pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> {
let status = std::process::Command::new(shell)
.arg("-c")
.arg(cmd.into())
- .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
+ .current_dir(dir)
.status()
.expect("failed to execute command");
@@ -159,6 +174,18 @@ pub fn run_cmd(cmd: impl Into<String>) -> Result<(), i32> {
/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`.
/// Stderr falls back to stdout if stderr is empty.
pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)> {
+ let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
+ run_cmd_capture_with_dir(cmd.into(), &cwd)
+}
+
+/// Run a shell command in the specified directory and capture its combined stdout+stderr output.
+///
+/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`.
+/// Stderr falls back to stdout if stderr is empty.
+pub fn run_cmd_capture_with_dir(
+ cmd: impl Into<String>,
+ dir: &std::path::Path,
+) -> Result<String, (i32, String)> {
let shell = if cfg!(target_os = "windows") {
"powershell"
} else {
@@ -167,7 +194,7 @@ pub fn run_cmd_capture(cmd: impl Into<String>) -> Result<String, (i32, String)>
let output = std::process::Command::new(shell)
.arg("-c")
.arg(cmd.into())
- .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
+ .current_dir(dir)
.output()
.expect("failed to execute command");