#!/usr/bin/env rust-script
use std::env;
use std::fs;
use std::path::Path;
use std::process::exit;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq)]
enum BumpType {
Major,
Minor,
Patch,
}
impl BumpType {
fn from_str(s: &str) -> Option<BumpType> {
match s.to_lowercase().as_str() {
"major" => Some(BumpType::Major),
"minor" => Some(BumpType::Minor),
"patch" => Some(BumpType::Patch),
_ => None,
}
}
}
struct Version {
major: u32,
minor: u32,
patch: u32,
pre_release: Option<String>,
}
impl Version {
fn bump(&self, bump_type: BumpType) -> String {
match bump_type {
BumpType::Major => format!("{}.0.0", self.major + 1),
BumpType::Minor => format!("{}.{}.0", self.major, self.minor + 1),
BumpType::Patch => format!("{}.{}.{}", self.major, self.minor, self.patch + 1),
}
}
fn to_string(&self) -> String {
match &self.pre_release {
Some(pre) => format!("{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
None => format!("{}.{}.{}", self.major, self.minor, self.patch),
}
}
}
fn get_arg(name: &str) -> Option<String> {
let args: Vec<String> = env::args().collect();
let flag = format!("--{}", name);
if let Some(idx) = args.iter().position(|a| a == &flag) {
return args.get(idx + 1).cloned();
}
let env_name = name.to_uppercase().replace('-', "_");
env::var(&env_name).ok().filter(|s| !s.is_empty())
}
fn has_flag(name: &str) -> bool {
let args: Vec<String> = env::args().collect();
let flag = format!("--{}", name);
args.contains(&flag)
}
fn get_rust_root() -> String {
if let Some(root) = get_arg("rust-root") {
return root;
}
if Path::new("./Cargo.toml").exists() {
eprintln!("Detected single-language repository (Cargo.toml in root)");
return ".".to_string();
}
if Path::new("./rust/Cargo.toml").exists() {
eprintln!("Detected multi-language repository (Cargo.toml in rust/)");
return "rust".to_string();
}
eprintln!("Error: Could not find Cargo.toml in expected locations");
exit(1);
}
fn get_cargo_toml_path(rust_root: &str) -> String {
if rust_root == "." {
"./Cargo.toml".to_string()
} else {
format!("{}/Cargo.toml", rust_root)
}
}
fn get_current_version(cargo_toml_path: &str) -> Result<Version, String> {
let content = fs::read_to_string(cargo_toml_path)
.map_err(|e| format!("Failed to read {}: {}", cargo_toml_path, e))?;
let re = Regex::new(r#"(?m)^version\s*=\s*"(\d+)\.(\d+)\.(\d+)(?:-([^"]+))?""#).unwrap();
if let Some(caps) = re.captures(&content) {
let major: u32 = caps.get(1).unwrap().as_str().parse().unwrap();
let minor: u32 = caps.get(2).unwrap().as_str().parse().unwrap();
let patch: u32 = caps.get(3).unwrap().as_str().parse().unwrap();
let pre_release = caps.get(4).map(|m| m.as_str().to_string());
Ok(Version { major, minor, patch, pre_release })
} else {
Err(format!("Could not parse version from {}", cargo_toml_path))
}
}
fn update_cargo_toml(cargo_toml_path: &str, new_version: &str) -> Result<(), String> {
let content = fs::read_to_string(cargo_toml_path)
.map_err(|e| format!("Failed to read {}: {}", cargo_toml_path, e))?;
let re = Regex::new(r#"(?m)^(version\s*=\s*")[^"]+(")"#).unwrap();
let new_content = re.replace(&content, format!("${{1}}{}${{2}}", new_version).as_str());
fs::write(cargo_toml_path, new_content.as_ref())
.map_err(|e| format!("Failed to write {}: {}", cargo_toml_path, e))?;
Ok(())
}
fn main() {
let bump_type_str = match get_arg("bump-type") {
Some(s) => s,
None => {
eprintln!("Usage: rust-script scripts/bump-version.rs --bump-type <major|minor|patch> [--dry-run] [--rust-root <path>]");
exit(1);
}
};
let bump_type = match BumpType::from_str(&bump_type_str) {
Some(bt) => bt,
None => {
eprintln!("Invalid bump type: {}. Must be major, minor, or patch.", bump_type_str);
exit(1);
}
};
let dry_run = has_flag("dry-run");
let rust_root = get_rust_root();
let cargo_toml = get_cargo_toml_path(&rust_root);
let current = match get_current_version(&cargo_toml) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {}", e);
exit(1);
}
};
let new_version = current.bump(bump_type);
println!("Current version: {}", current.to_string());
println!("New version: {}", new_version);
if dry_run {
println!("Dry run - no changes made");
} else {
if let Err(e) = update_cargo_toml(&cargo_toml, &new_version) {
eprintln!("Error: {}", e);
exit(1);
}
println!("Updated {}", cargo_toml);
}
}