diff options
Diffstat (limited to '.run/src')
| -rw-r--r-- | .run/src/bin/RELEASE-WORKSPACE.rs | 145 | ||||
| -rw-r--r-- | .run/src/bin/display-dependency-order.rs | 12 | ||||
| -rw-r--r-- | .run/src/dependency_order.rs | 183 | ||||
| -rw-r--r-- | .run/src/lib.rs | 33 |
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 ¤t { + 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"); |
