#!/usr/bin/env rust-script
#![allow(dead_code)]
use regex::Regex;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageInfo {
pub name: String,
pub version: String,
}
pub fn get_rust_root(explicit_root: Option<&str>, verbose: bool) -> Result<String, String> {
if let Some(root) = explicit_root {
if verbose {
eprintln!("Using explicitly configured Rust root: {root}");
}
return Ok(root.to_string());
}
let args: Vec<String> = env::args().collect();
if let Some(idx) = args.iter().position(|a| a == "--rust-root") {
if let Some(root) = args.get(idx + 1) {
if verbose {
eprintln!("Using CLI configured Rust root: {root}");
}
return Ok(root.clone());
}
}
if let Ok(root) = env::var("RUST_ROOT") {
if !root.is_empty() {
if verbose {
eprintln!("Using environment configured Rust root: {root}");
}
return Ok(root);
}
}
if Path::new("./Cargo.toml").exists() {
if verbose {
eprintln!("Detected single-language repository (Cargo.toml in root)");
}
return Ok(".".to_string());
}
if Path::new("./rust/Cargo.toml").exists() {
if verbose {
eprintln!("Detected multi-language repository (Cargo.toml in rust/)");
}
return Ok("rust".to_string());
}
Err("Could not find Cargo.toml in expected locations.\n\
Searched in:\n \
- ./Cargo.toml (single-language repository)\n \
- ./rust/Cargo.toml (multi-language repository)\n\n\
To fix this, either:\n \
1. Run the script from the repository root\n \
2. Explicitly configure the Rust root using --rust-root option\n \
3. Set the RUST_ROOT environment variable"
.to_string())
}
pub fn get_cargo_toml_path(rust_root: &str) -> PathBuf {
if rust_root == "." {
PathBuf::from("./Cargo.toml")
} else {
PathBuf::from(rust_root).join("Cargo.toml")
}
}
pub fn get_cargo_lock_path(rust_root: &str) -> PathBuf {
if rust_root == "." {
PathBuf::from("./Cargo.lock")
} else {
PathBuf::from(rust_root).join("Cargo.lock")
}
}
pub fn get_changelog_dir(rust_root: &str) -> PathBuf {
if rust_root == "." {
PathBuf::from("./changelog.d")
} else {
PathBuf::from(rust_root).join("changelog.d")
}
}
pub fn get_changelog_path(rust_root: &str) -> PathBuf {
if rust_root == "." {
PathBuf::from("./CHANGELOG.md")
} else {
PathBuf::from(rust_root).join("CHANGELOG.md")
}
}
pub fn needs_cd(rust_root: &str) -> bool {
rust_root != "."
}
pub fn parse_rust_root_from_args() -> Option<String> {
let args: Vec<String> = env::args().collect();
if let Some(idx) = args.iter().position(|a| a == "--rust-root") {
return args.get(idx + 1).cloned();
}
env::var("RUST_ROOT").ok().filter(|s| !s.is_empty())
}
pub fn get_package_manifest_path(root_manifest: &Path) -> Result<PathBuf, String> {
let content = fs::read_to_string(root_manifest)
.map_err(|e| format!("Failed to read {}: {}", root_manifest.display(), e))?;
if has_package_section(&content) {
return Ok(root_manifest.to_path_buf());
}
if has_workspace_section(&content) {
return resolve_workspace_member_manifest(root_manifest, &content);
}
Err(format!(
"Could not find [package] or [workspace] in {}",
root_manifest.display()
))
}
pub fn read_package_info(manifest_path: &Path) -> Result<PackageInfo, String> {
let content = fs::read_to_string(manifest_path)
.map_err(|e| format!("Failed to read {}: {}", manifest_path.display(), e))?;
let name = find_manifest_value(&content, "name")
.ok_or_else(|| format!("Could not find name in {}", manifest_path.display()))?;
let version = find_manifest_value(&content, "version")
.ok_or_else(|| format!("Could not find version in {}", manifest_path.display()))?;
Ok(PackageInfo { name, version })
}
fn has_package_section(content: &str) -> bool {
Regex::new(r"(?m)^\[package\]\s*$")
.unwrap()
.is_match(content)
}
fn has_workspace_section(content: &str) -> bool {
Regex::new(r"(?m)^\[workspace\]\s*$")
.unwrap()
.is_match(content)
}
fn resolve_workspace_member_manifest(
root_manifest: &Path,
content: &str,
) -> Result<PathBuf, String> {
let members = parse_workspace_members(content).ok_or_else(|| {
format!(
"Could not find workspace members in {}",
root_manifest.display()
)
})?;
let base_dir = root_manifest.parent().ok_or_else(|| {
format!(
"Could not determine parent directory for {}",
root_manifest.display()
)
})?;
for member in members {
let manifest = base_dir.join(member).join("Cargo.toml");
let Ok(member_content) = fs::read_to_string(&manifest) else {
continue;
};
if !has_package_section(&member_content) || is_publish_false(&member_content) {
continue;
}
return Ok(manifest);
}
Err(format!(
"No publishable workspace members found in {}",
root_manifest.display()
))
}
fn parse_workspace_members(content: &str) -> Option<Vec<String>> {
let re = Regex::new(r"(?s)members\s*=\s*\[(.*?)\]").unwrap();
let captures = re.captures(content)?;
let body = captures.get(1)?.as_str();
let members = body
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| entry.trim_matches('"').to_string())
.filter(|entry| !entry.is_empty())
.collect::<Vec<_>>();
if members.is_empty() {
None
} else {
Some(members)
}
}
fn is_publish_false(content: &str) -> bool {
Regex::new(r"(?m)^publish\s*=\s*false\s*$")
.unwrap()
.is_match(content)
}
fn find_manifest_value(content: &str, key: &str) -> Option<String> {
let re = Regex::new(&format!(r#"(?m)^{}\s*=\s*"([^"]+)""#, regex::escape(key))).unwrap();
re.captures(content)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
#[cfg(not(test))]
fn main() {
match get_rust_root(None, true) {
Ok(root) => {
println!("Rust root: {root}");
println!("Cargo.toml: {}", get_cargo_toml_path(&root).display());
println!("Changelog dir: {}", get_changelog_dir(&root).display());
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}