use crate::core::config::{CitationAuthor, CitationConfig, Language, ResolvedCrateConfig};
use anyhow::Context as _;
use std::sync::LazyLock;
use tracing::{debug, info, warn};
use super::helpers::{run_command, run_optional};
use super::{extract, readme};
static CARGO_VERSION_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"(?m)^(version\s*=\s*)"[^"]*""#).expect("valid regex"));
static SEMVER_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\d+\.\d+\.\d+(-[a-zA-Z0-9._]+)*").expect("valid regex"));
pub(crate) fn read_version(version_from: &str) -> anyhow::Result<String> {
let content =
std::fs::read_to_string(version_from).with_context(|| format!("failed to read version file {version_from}"))?;
let value: toml::Value =
toml::from_str(&content).with_context(|| format!("failed to parse TOML in {version_from}"))?;
if let Some(v) = value
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
{
return Ok(v.to_string());
}
if let Some(v) = value
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
{
return Ok(v.to_string());
}
anyhow::bail!("Could not find version in {version_from}")
}
fn bump_version(version: &str, component: &str) -> anyhow::Result<String> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
anyhow::bail!("Invalid semver version: {version}");
}
let mut major: u64 = parts[0]
.parse()
.with_context(|| format!("Invalid major version component: {}", parts[0]))?;
let mut minor: u64 = parts[1]
.parse()
.with_context(|| format!("Invalid minor version component: {}", parts[1]))?;
let mut patch: u64 = parts[2]
.parse()
.with_context(|| format!("Invalid patch version component: {}", parts[2]))?;
match component {
"major" => {
major += 1;
minor = 0;
patch = 0;
}
"minor" => {
minor += 1;
patch = 0;
}
"patch" => {
patch += 1;
}
other => anyhow::bail!("Unknown bump component '{other}': expected major, minor, or patch"),
}
Ok(format!("{major}.{minor}.{patch}"))
}
fn write_version_to_cargo_toml(cargo_toml_path: &str, new_version: &str) -> anyhow::Result<()> {
let content =
std::fs::read_to_string(cargo_toml_path).with_context(|| format!("Failed to read {cargo_toml_path}"))?;
let new_content = CARGO_VERSION_RE
.replace(&content, format!(r#"version = "{new_version}""#).as_str())
.to_string();
if new_content == content {
anyhow::bail!("Could not find a `version = \"...\"` field to update in {cargo_toml_path}");
}
std::fs::write(cargo_toml_path, new_content)
.with_context(|| format!("Failed to write updated version to {cargo_toml_path}"))?;
Ok(())
}
fn to_pep440(version: &str) -> String {
let Some((base, pre)) = version.split_once('-') else {
return version.to_string();
};
let mut out = String::with_capacity(base.len() + pre.len());
out.push_str(base);
let pre_norm = if let Some(rest) = pre.strip_prefix("alpha.").or_else(|| pre.strip_prefix("alpha")) {
out.push('a');
rest
} else if let Some(rest) = pre.strip_prefix("beta.").or_else(|| pre.strip_prefix("beta")) {
out.push('b');
rest
} else if let Some(rest) = pre.strip_prefix("rc.").or_else(|| pre.strip_prefix("rc")) {
out.push_str("rc");
rest
} else {
pre
};
for c in pre_norm.chars() {
if c != '.' {
out.push(c);
}
}
out
}
use crate::core::version::{to_r_version, to_rubygems_prerelease};
pub(crate) fn patch_workspace_dep_versions(
cargo_toml_path: &str,
new_version: &str,
workspace_members: &std::collections::HashSet<String>,
) -> anyhow::Result<bool> {
use toml_edit::{DocumentMut, Item};
let content =
std::fs::read_to_string(cargo_toml_path).with_context(|| format!("failed to read {cargo_toml_path}"))?;
let mut doc: DocumentMut = content
.parse()
.with_context(|| format!("failed to parse TOML in {cargo_toml_path}"))?;
let mut changed = false;
fn patch_dep_table(
dep_table: &mut Item,
new_version: &str,
workspace_members: &std::collections::HashSet<String>,
) -> bool {
let Some(table) = dep_table.as_table_like_mut() else {
return false;
};
let mut any = false;
for (key, item) in table.iter_mut() {
if !workspace_members.contains(key.get()) {
continue;
}
if let Some(inline) = item.as_table_like_mut() {
if let Some(ver_item) = inline.get_mut("version") {
if ver_item.as_str() != Some(new_version) {
*ver_item = toml_edit::value(new_version);
any = true;
}
}
}
}
any
}
for table_key in &["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(item) = doc.get_mut(table_key) {
if patch_dep_table(item, new_version, workspace_members) {
changed = true;
}
}
}
if let Some(workspace) = doc.get_mut("workspace") {
if let Some(ws_table) = workspace.as_table_like_mut() {
if let Some(deps) = ws_table.get_mut("dependencies") {
if patch_dep_table(deps, new_version, workspace_members) {
changed = true;
}
}
}
}
if let Some(target_item) = doc.get_mut("target") {
if let Some(target_table) = target_item.as_table_like_mut() {
let cfg_keys: Vec<String> = target_table.iter().map(|(k, _)| k.to_string()).collect();
for cfg_key in cfg_keys {
if let Some(cfg_item) = target_table.get_mut(&cfg_key) {
if let Some(cfg_table) = cfg_item.as_table_like_mut() {
for dep_key in &["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(dep_item) = cfg_table.get_mut(dep_key) {
if patch_dep_table(dep_item, new_version, workspace_members) {
changed = true;
}
}
}
}
}
}
}
}
if changed {
std::fs::write(cargo_toml_path, doc.to_string())
.with_context(|| format!("failed to write updated dep versions to {cargo_toml_path}"))?;
}
Ok(changed)
}
pub fn verify_versions(config: &ResolvedCrateConfig) -> anyhow::Result<Vec<String>> {
let expected = read_version(&config.version_from)?;
let expected_pep440 = to_pep440(&expected);
let expected_rubygems = to_rubygems_prerelease(&expected);
let mut mismatches = Vec::new();
fn extract_version(path: &str, pattern: &str) -> Option<String> {
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
static CACHE: OnceLock<Mutex<HashMap<String, regex::Regex>>> = OnceLock::new();
let content = std::fs::read_to_string(path).ok()?;
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut guard = cache.lock().ok()?;
let re = match guard.get(pattern) {
Some(re) => re.clone(),
None => {
let re = regex::Regex::new(pattern).ok()?;
guard.insert(pattern.to_string(), re.clone());
re
}
};
drop(guard);
re.captures(&content)?.get(1).map(|m| m.as_str().to_string())
}
if let Some(found) = extract_version("packages/python/pyproject.toml", r#"version\s*=\s*"([^"]*)""#) {
if found != expected_pep440 {
mismatches.push(format!(
"packages/python/pyproject.toml: found {found}, expected {expected_pep440}"
));
}
}
if let Some(found) = extract_version("packages/typescript/package.json", r#""version"\s*:\s*"([^"]*)""#) {
if found != expected {
mismatches.push(format!(
"packages/typescript/package.json: found {found}, expected {expected}"
));
}
}
if let Some(found) = extract_version("packages/java/pom.xml", r"<version>([^<]*)</version>") {
if found != expected {
mismatches.push(format!("packages/java/pom.xml: found {found}, expected {expected}"));
}
}
if let Some(found) = extract_version("packages/elixir/mix.exs", r#"version:\s*"([^"]*)""#)
.or_else(|| extract_version("packages/elixir/mix.exs", r#"@version\s*"([^"]*)""#))
{
if found != expected {
mismatches.push(format!("packages/elixir/mix.exs: found {found}, expected {expected}"));
}
}
if let Ok(entries) = std::fs::read_dir("packages/ruby") {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "gemspec") {
if let Some(found) = extract_version(
&path.to_string_lossy(),
r"spec\.version\s*=\s*['\x22]([^'\x22]*)['\x22]",
) {
if found != expected_rubygems {
mismatches.push(format!(
"{}: found {found}, expected {expected_rubygems}",
path.display()
));
}
}
}
}
}
for pattern in &[
"packages/ruby/lib/*/version.rb",
"packages/ruby/ext/*/src/*/version.rb",
"packages/ruby/ext/*/native/src/*/version.rb",
] {
if let Ok(entries) = glob::glob(pattern) {
for entry in entries.flatten() {
if let Some(found) = extract_version(&entry.to_string_lossy(), r#"VERSION\s*=\s*["']([^"']*)["']"#) {
if found != expected_rubygems {
mismatches.push(format!(
"{}: found {found}, expected {expected_rubygems}",
entry.display()
));
}
}
}
}
}
if let Some(found) = extract_version(
"packages/csharp/SampleCrawler/SampleCrawler.csproj",
r"<Version>([^<]*)</Version>",
) {
if found != expected {
mismatches.push(format!("packages/csharp: found {found}, expected {expected}"));
}
}
if let Some(found) = extract_version("packages/php/composer.json", r#""version"\s*:\s*"([^"]*)""#) {
if found != expected {
mismatches.push(format!(
"packages/php/composer.json: found {found}, expected {expected}"
));
}
}
if let Some(found) = extract_version("packages/dart/pubspec.yaml", r"(?m)^version:\s*([^\s#\n]+)") {
if found != expected {
mismatches.push(format!(
"packages/dart/pubspec.yaml: found {found}, expected {expected}"
));
}
}
if let Some(found) = extract_version("packages/zig/build.zig.zon", r#"(?m)^\s*\.version\s*=\s*"([^"]*)""#) {
if found != expected {
mismatches.push(format!(
"packages/zig/build.zig.zon: found {found}, expected {expected}"
));
}
}
if let Some(found) = extract_version(
"Package.swift",
r#"releases/download/v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9._]+)*)/"#,
) {
if found != expected {
mismatches.push(format!("Package.swift: found {found}, expected {expected}"));
}
}
Ok(mismatches)
}
pub fn set_version(config: &ResolvedCrateConfig, version: &str) -> anyhow::Result<()> {
write_version_to_cargo_toml(&config.version_from, version)
.with_context(|| format!("failed to set version to {version}"))?;
info!("Set version to {version} in {}", config.version_from);
Ok(())
}
pub fn sync_versions(
config: &ResolvedCrateConfig,
config_path: &std::path::Path,
bump: Option<&str>,
no_regen: bool,
skip_swift_checksum: bool,
) -> anyhow::Result<()> {
if let Some(component) = bump {
let current = read_version(&config.version_from)?;
let bumped = bump_version(¤t, component)?;
info!("Bumping version {current} -> {bumped} ({component})");
write_version_to_cargo_toml(&config.version_from, &bumped).context("failed to sync versions")?;
info!("Updated {} with bumped version {bumped}", config.version_from);
}
let version = read_version(&config.version_from)?;
let last_path = std::path::Path::new(".alef").join("last_synced_version");
info!("Syncing version {version}");
let mut updated = vec![];
let mut any_node_pkg_modified = false;
let mut any_cargo_toml_modified = false;
let mut any_composer_json_modified = false;
let mut any_mix_exs_modified = false;
let mut text_replacement_paths: std::collections::HashSet<std::path::PathBuf> = std::collections::HashSet::new();
if let Ok(root_content) = std::fs::read_to_string("Cargo.toml") {
if let Ok(root_toml) = root_content.parse::<toml::Table>() {
let empty_vec = vec![];
let members = root_toml
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.unwrap_or(&empty_vec);
let excludes = root_toml
.get("workspace")
.and_then(|w| w.get("exclude"))
.and_then(|m| m.as_array())
.unwrap_or(&empty_vec);
let workspace_member_names: std::collections::HashSet<String> =
crate::publish::workspace::workspace_member_crates(std::path::Path::new("."))
.map(|m| m.names.into_iter().collect())
.unwrap_or_default();
let mut cargo_toml_paths: Vec<String> = vec![];
for pattern_val in members.iter().chain(excludes.iter()) {
if let Some(pattern) = pattern_val.as_str() {
if let Ok(paths) = glob::glob(&format!("{pattern}/Cargo.toml")) {
for entry in paths.flatten() {
cargo_toml_paths.push(entry.to_string_lossy().to_string());
}
}
}
}
for entry in glob::glob("packages/*/rust/Cargo.toml").into_iter().flatten().flatten() {
let path_str = entry.to_string_lossy().to_string();
if !cargo_toml_paths.contains(&path_str) {
cargo_toml_paths.push(path_str);
}
}
for path_str in &cargo_toml_paths {
if write_version_to_cargo_toml(path_str, &version).is_ok() && !updated.contains(path_str) {
updated.push(path_str.clone());
any_cargo_toml_modified = true;
}
if !workspace_member_names.is_empty() {
match patch_workspace_dep_versions(path_str, &version, &workspace_member_names) {
Ok(true) => {
if !updated.contains(path_str) {
updated.push(path_str.clone());
any_cargo_toml_modified = true;
}
}
Ok(false) => {}
Err(e) => {
debug!("Could not patch dep versions in {path_str}: {e}");
}
}
}
}
if !workspace_member_names.is_empty() {
match patch_workspace_dep_versions("Cargo.toml", &version, &workspace_member_names) {
Ok(true) => {
if !updated.contains(&"Cargo.toml".to_string()) {
updated.push("Cargo.toml".to_string());
any_cargo_toml_modified = true;
}
}
Ok(false) => {}
Err(e) => {
debug!("Could not patch workspace dep versions in root Cargo.toml: {e}");
}
}
}
}
}
let python_version = to_pep440(&version);
{
let pkg_dir = config.package_dir(Language::Python);
let mut python_paths: Vec<String> = vec![
"packages/python/pyproject.toml".to_string(),
std::path::Path::new(&pkg_dir)
.join("pyproject.toml")
.to_string_lossy()
.into_owned(),
];
if let Some(output_dir) = config.output_for("python") {
python_paths.push(output_dir.join("pyproject.toml").to_string_lossy().into_owned());
}
let mut seen_canonical: std::collections::HashSet<std::path::PathBuf> = std::collections::HashSet::new();
for python_path in python_paths {
let canonical = match std::fs::canonicalize(&python_path) {
Ok(p) => p,
Err(_) => continue,
};
if !seen_canonical.insert(canonical) {
continue;
}
if let Ok(content) = std::fs::read_to_string(&python_path) {
if let Some(new_content) = replace_version_pattern(&content, r#"version = "[^"]*""#, &python_version) {
std::fs::write(&python_path, &new_content)
.with_context(|| format!("failed to write {python_path}"))?;
updated.push(python_path);
}
}
}
}
let node_pkg_dir = config.package_dir(Language::Node);
let mut node_paths: Vec<String> = vec![format!("{node_pkg_dir}/package.json")];
if node_pkg_dir != "packages/typescript" {
node_paths.push("packages/typescript/package.json".to_string());
}
for node_path in node_paths {
if let Ok(content) = std::fs::read_to_string(&node_path) {
if let Some(new_content) = replace_version_pattern(&content, r#""version": "[^"]*""#, &version) {
std::fs::write(&node_path, &new_content).with_context(|| format!("failed to write {node_path}"))?;
updated.push(node_path);
any_node_pkg_modified = true;
}
}
}
let ruby_version = to_rubygems_prerelease(&version);
if let Ok(entries) = std::fs::read_dir("packages/ruby") {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "gemspec") {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Some(new_content) =
replace_version_pattern(&content, r#"spec\.version\s*=\s*['"][^'"]*['"]"#, &ruby_version)
{
std::fs::write(&path, &new_content)?;
updated.push(path.to_string_lossy().to_string());
}
}
}
}
}
for pattern in &[
"packages/ruby/lib/*/version.rb",
"packages/ruby/ext/*/src/*/version.rb",
"packages/ruby/ext/*/native/src/*/version.rb",
] {
for entry in glob::glob(pattern).into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&entry) {
if let Some(new_content) =
replace_version_pattern(&content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, &ruby_version)
{
std::fs::write(&entry, &new_content)?;
updated.push(entry.to_string_lossy().to_string());
}
}
}
}
let gemfile_lock_path = std::path::Path::new("packages/ruby/Gemfile.lock");
if gemfile_lock_path.exists() {
if let Ok(content) = std::fs::read_to_string(gemfile_lock_path) {
if let Some(new_content) = sync_gemfile_lock(&content, &ruby_version) {
std::fs::write(gemfile_lock_path, &new_content)
.context("failed to write packages/ruby/Gemfile.lock")?;
updated.push("packages/ruby/Gemfile.lock".to_string());
}
}
}
if let Ok(content) = std::fs::read_to_string("packages/php/composer.json") {
if let Some(new_content) = replace_version_pattern(&content, r#""version": "[^"]*""#, &version) {
std::fs::write("packages/php/composer.json", &new_content)?;
updated.push("packages/php/composer.json".to_string());
any_composer_json_modified = true;
}
}
if let Ok(content) = std::fs::read_to_string("packages/elixir/mix.exs") {
if let Some(new_content) = replace_version_pattern(&content, r#"version: "[^"]*""#, &version) {
std::fs::write("packages/elixir/mix.exs", &new_content)?;
updated.push("packages/elixir/mix.exs".to_string());
any_mix_exs_modified = true;
} else if let Some(new_content) = replace_version_pattern(&content, r#"@version "[^"]*""#, &version) {
std::fs::write("packages/elixir/mix.exs", &new_content)?;
updated.push("packages/elixir/mix.exs".to_string());
any_mix_exs_modified = true;
}
}
{
let elixir_pkg = config.package_dir(Language::Elixir);
let nif_lock_glob = format!("{elixir_pkg}/native/*/Cargo.lock");
for entry in glob::glob(&nif_lock_glob).into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&entry) {
if let Some(new_content) = sync_cargo_lock_path_versions(&content, &version) {
std::fs::write(&entry, &new_content)
.with_context(|| format!("failed to write {}", entry.display()))?;
updated.push(entry.to_string_lossy().to_string());
}
}
}
}
if let Ok(content) = std::fs::read_to_string("packages/java/pom.xml") {
if let Some(new_content) = replace_version_pattern(&content, r#"<version>[^<]*</version>"#, &version) {
std::fs::write("packages/java/pom.xml", &new_content)?;
updated.push("packages/java/pom.xml".to_string());
}
}
for entry in glob::glob("packages/csharp/**/*.csproj")
.into_iter()
.flatten()
.flatten()
{
if let Ok(content) = std::fs::read_to_string(&entry) {
if let Some(new_content) = replace_version_pattern(&content, r#"<Version>[^<]*</Version>"#, &version) {
std::fs::write(&entry, &new_content)?;
updated.push(entry.to_string_lossy().to_string());
}
}
}
let kotlin_gradle = std::path::Path::new(&config.package_dir(Language::Kotlin)).join("build.gradle.kts");
if let Ok(content) = std::fs::read_to_string(&kotlin_gradle) {
if let Some(new_content) = replace_gradle_project_version(&content, &version) {
std::fs::write(&kotlin_gradle, &new_content)
.with_context(|| format!("failed to write {}", kotlin_gradle.display()))?;
updated.push(kotlin_gradle.to_string_lossy().to_string());
}
}
let kotlin_android_gradle =
std::path::Path::new(&config.package_dir(Language::KotlinAndroid)).join("build.gradle.kts");
if let Ok(content) = std::fs::read_to_string(&kotlin_android_gradle) {
if let Some(new_content) = replace_gradle_project_version(&content, &version) {
std::fs::write(&kotlin_android_gradle, &new_content)
.with_context(|| format!("failed to write {}", kotlin_android_gradle.display()))?;
updated.push(kotlin_android_gradle.to_string_lossy().to_string());
}
}
for wasm_pkg in glob::glob("crates/*-wasm/package.json").into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&wasm_pkg) {
if let Some(new_content) = replace_version_pattern(&content, r#""version":\s*"[^"]*""#, &version) {
std::fs::write(&wasm_pkg, &new_content)?;
updated.push(wasm_pkg.to_string_lossy().to_string());
}
}
}
for node_pkg in glob::glob("crates/*-node/package.json").into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&node_pkg) {
let mut working = content.clone();
if let Some(rewritten) = replace_version_pattern(&working, r#""version":\s*"[^"]*""#, &version) {
working = rewritten;
}
if let Ok(pkg_json) = serde_json::from_str::<serde_json::Value>(&working) {
if let Some(parent_name) = pkg_json.get("name").and_then(|v| v.as_str()) {
let pattern = format!(r#""({}-[^"]+)":\s*"[^"]*""#, regex::escape(parent_name));
if let Ok(re) = regex::Regex::new(&pattern) {
let replacement = format!(r#""$1": "{version}""#);
working = re.replace_all(&working, replacement.as_str()).to_string();
}
}
}
if working != content {
std::fs::write(&node_pkg, &working)?;
updated.push(node_pkg.to_string_lossy().to_string());
any_node_pkg_modified = true;
}
}
}
for platform_pkg in glob::glob("crates/*-node/npm/*/package.json")
.into_iter()
.flatten()
.flatten()
{
if let Ok(content) = std::fs::read_to_string(&platform_pkg) {
if let Some(new_content) = replace_version_pattern(&content, r#""version":\s*"[^"]*""#, &version) {
std::fs::write(&platform_pkg, &new_content)?;
updated.push(platform_pkg.to_string_lossy().to_string());
any_node_pkg_modified = true;
}
}
}
if let Ok(content) = std::fs::read_to_string("package.json") {
if let Some(new_content) = replace_version_pattern(&content, r#""version":\s*"[^"]*""#, &version) {
std::fs::write("package.json", &new_content)?;
updated.push("package.json".to_string());
any_node_pkg_modified = true;
}
}
if let Ok(content) = std::fs::read_to_string("composer.json") {
if let Some(new_content) = replace_version_pattern(&content, r#""version":\s*"[^"]*""#, &version) {
std::fs::write("composer.json", &new_content)?;
updated.push("composer.json".to_string());
any_composer_json_modified = true;
}
}
if let Ok(content) = std::fs::read_to_string("packages/r/DESCRIPTION") {
let r_version = to_r_version(&version);
if let Some(new_content) = replace_version_pattern(&content, r"Version:\s*[^\n]*", &r_version) {
std::fs::write("packages/r/DESCRIPTION", &new_content)?;
updated.push("packages/r/DESCRIPTION".to_string());
}
}
if let Ok(content) = std::fs::read_to_string("packages/dart/pubspec.yaml") {
static PUBSPEC_VERSION_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^version:\s*[^\s#\n]+").expect("valid regex"));
let new_content = PUBSPEC_VERSION_RE
.replace(&content, format!("version: {version}").as_str())
.into_owned();
if new_content != content {
std::fs::write("packages/dart/pubspec.yaml", &new_content)?;
updated.push("packages/dart/pubspec.yaml".to_string());
}
}
if let Ok(content) = std::fs::read_to_string("packages/zig/build.zig.zon") {
static ZON_VERSION_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"(?m)^(\s*)\.version\s*=\s*"[^"]*""#).expect("valid regex"));
let new_content = ZON_VERSION_RE
.replace(&content, format!(r#"$1.version = "{version}""#).as_str())
.into_owned();
if new_content != content {
std::fs::write("packages/zig/build.zig.zon", &new_content)?;
updated.push("packages/zig/build.zig.zon".to_string());
}
}
for py_init in glob::glob("packages/python/**/__init__.py")
.into_iter()
.flatten()
.flatten()
{
if let Ok(content) = std::fs::read_to_string(&py_init) {
if let Some(new_content) = replace_version_pattern(&content, r#"__version__\s*=\s*"[^"]*""#, &version) {
std::fs::write(&py_init, &new_content)
.with_context(|| format!("failed to write {}", py_init.display()))?;
updated.push(py_init.to_string_lossy().to_string());
}
}
}
if let Ok(content) = std::fs::read_to_string("packages/go/ffi_loader.go") {
if let Some(new_content) = replace_version_pattern(&content, r#"defaultFFIVersion\s*=\s*"[^"]*""#, &version) {
std::fs::write("packages/go/ffi_loader.go", &new_content)?;
updated.push("packages/go/ffi_loader.go".to_string());
}
}
for entry in glob::glob("packages/go/cmd/download_ffi/main.go")
.into_iter()
.flatten()
.flatten()
{
if let Ok(content) = std::fs::read_to_string(&entry) {
if let Some(new_content) = replace_version_pattern(&content, r#"moduleVersion\s*=\s*"[^"]*""#, &version) {
std::fs::write(&entry, &new_content).with_context(|| format!("failed to write {}", entry.display()))?;
updated.push(entry.to_string_lossy().to_string());
}
}
}
if let Ok(content) = std::fs::read_to_string("Package.swift") {
let new_content = content.replace("v__ALEF_SWIFT_VERSION__", &format!("v{version}"));
if new_content != content {
std::fs::write("Package.swift", &new_content)?;
updated.push("Package.swift".to_string());
}
}
for swift_pkg_pattern in &["test_apps/*/Package.swift", "e2e/*/Package.swift"] {
for swift_pkg in glob::glob(swift_pkg_pattern).into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&swift_pkg) {
if let Some(new_content) = replace_version_pattern(&content, r#"from:\s*"[^"]*""#, &version) {
std::fs::write(&swift_pkg, &new_content)
.with_context(|| format!("failed to write {}", swift_pkg.display()))?;
updated.push(swift_pkg.to_string_lossy().to_string());
}
}
}
}
for sh_pattern in &["e2e/c/download_ffi.sh", "test_apps/c/download_ffi.sh"] {
for sh_script in glob::glob(sh_pattern).into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&sh_script) {
if let Some(new_content) = replace_version_pattern(&content, r#"VERSION="[^"]*""#, &version) {
std::fs::write(&sh_script, &new_content)
.with_context(|| format!("failed to write {}", sh_script.display()))?;
updated.push(sh_script.to_string_lossy().to_string());
}
}
}
}
let e2e_java_pom = std::path::Path::new("e2e/java/pom.xml");
if let Ok(content) = std::fs::read_to_string(e2e_java_pom) {
if let Some(new_content) = sync_e2e_java_pom(&content, &version) {
std::fs::write(e2e_java_pom, &new_content).context("failed to write e2e/java/pom.xml")?;
updated.push("e2e/java/pom.xml".to_string());
}
}
let e2e_ruby_lock = std::path::Path::new("e2e/ruby/Gemfile.lock");
if e2e_ruby_lock.exists() {
if let Ok(content) = std::fs::read_to_string(e2e_ruby_lock) {
if let Some(new_content) = sync_gemfile_lock(&content, &ruby_version) {
std::fs::write(e2e_ruby_lock, &new_content).context("failed to write e2e/ruby/Gemfile.lock")?;
updated.push("e2e/ruby/Gemfile.lock".to_string());
}
}
}
for entry in glob::glob("e2e/go/go.mod").into_iter().flatten().flatten() {
if let Ok(content) = std::fs::read_to_string(&entry) {
static GO_MOD_REQUIRE_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(?m)^\s+([\w./\-]+/packages/go)\s+v[\w.\-]+").expect("valid regex")
});
if let Some(caps) = GO_MOD_REQUIRE_RE.captures(&content) {
let fragment = caps[1].to_string();
if let Some(new_content) = sync_e2e_go_mod(&content, &fragment, &version) {
std::fs::write(&entry, &new_content)
.with_context(|| format!("failed to write {}", entry.display()))?;
updated.push(entry.to_string_lossy().to_string());
}
}
}
}
let e2e_dart_lock = std::path::Path::new("e2e/dart/pubspec.lock");
if e2e_dart_lock.exists() {
if let Ok(content) = std::fs::read_to_string(e2e_dart_lock) {
if let Some(new_content) = sync_e2e_dart_pubspec_lock(&content, &version) {
std::fs::write(e2e_dart_lock, &new_content).context("failed to write e2e/dart/pubspec.lock")?;
updated.push("e2e/dart/pubspec.lock".to_string());
}
}
}
if let Some(citation_config) = config.citation.as_ref() {
let fallback_license = read_workspace_license(&config.version_from);
let rendered = render_citation_cff(citation_config, &version, fallback_license.as_deref());
let needs_write = match std::fs::read_to_string("CITATION.cff") {
Ok(current) => current != rendered,
Err(_) => true,
};
if needs_write {
std::fs::write("CITATION.cff", &rendered)?;
updated.push("CITATION.cff".to_string());
}
} else if let Ok(content) = std::fs::read_to_string("CITATION.cff") {
if let Some(new_content) = replace_citation_version(&content, &version) {
std::fs::write("CITATION.cff", &new_content)?;
updated.push("CITATION.cff".to_string());
}
}
if let Some(sync_config) = &config.sync {
for pattern in &sync_config.extra_paths {
match glob::glob(pattern) {
Ok(paths) => {
for entry in paths {
match entry {
Ok(path) => {
if let Ok(content) = std::fs::read_to_string(&path) {
let file_name = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if file_name == "package.json" {
if let Some(new_content) =
replace_version_pattern(&content, r#""version":\s*"[^"]*""#, &version)
{
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
} else if file_name == "Cargo.toml" {
let path_str = path.to_string_lossy().to_string();
if write_version_to_cargo_toml(&path_str, &version).is_ok() {
updated.push(path_str);
}
} else if file_name == "pyproject.toml" {
let py_ver = to_pep440(&version);
if let Some(new_content) =
replace_version_pattern(&content, r#"version = "[^"]*""#, &py_ver)
{
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
} else if file_name == "version.rb" {
let rb_ver = to_rubygems_prerelease(&version);
if let Some(new_content) = replace_version_pattern(
&content,
r#"VERSION\s*=\s*['"][^'"]*['"]"#,
&rb_ver,
) {
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
} else if extension == "gemspec" {
let rb_ver = to_rubygems_prerelease(&version);
if let Some(new_content) = replace_version_pattern(
&content,
r#"spec\.version\s*=\s*['"][^'"]*['"]"#,
&rb_ver,
) {
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
} else if file_name == "gleam.toml" {
let mut new_content = content.clone();
if let Some(updated_version) =
replace_version_pattern(&new_content, r#"version = "[^"]*""#, &version)
{
new_content = updated_version;
}
new_content = restore_gleam_dep_ranges(&new_content);
if new_content != content {
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
} else {
let new_content = SEMVER_RE.replace_all(&content, version.as_str()).to_string();
if new_content != content {
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
}
}
}
Err(e) => {
debug!("Glob entry error for pattern '{pattern}': {e}");
}
}
}
}
Err(e) => {
debug!("Invalid glob pattern '{pattern}': {e}");
}
}
}
for replacement in &sync_config.text_replacements {
match glob::glob(&replacement.path) {
Ok(paths) => {
for entry in paths {
match entry {
Ok(path) => {
text_replacement_paths.insert(path.clone());
if let Ok(content) = std::fs::read_to_string(&path) {
let pep440 = to_pep440(&version);
let rubygems = to_rubygems_prerelease(&version);
let r_ver = to_r_version(&version);
let search = replacement
.search
.replace("{python_version}", &pep440)
.replace("{ruby_version}", &rubygems)
.replace("{r_version}", &r_ver)
.replace("{version}", &version);
let replace = replacement
.replace
.replace("{python_version}", &pep440)
.replace("{ruby_version}", &rubygems)
.replace("{r_version}", &r_ver)
.replace("{version}", &version);
if let Ok(re) = regex::Regex::new(&search) {
let new_content = re.replace_all(&content, replace.as_str()).to_string();
if new_content != content {
if let Err(e) = std::fs::write(&path, &new_content) {
debug!("Could not write {}: {e}", path.display());
} else {
updated.push(path.to_string_lossy().to_string());
}
}
}
}
}
Err(e) => {
debug!("Glob entry error for pattern '{}': {e}", replacement.path);
}
}
}
}
Err(e) => {
debug!("Invalid glob pattern '{}': {e}", replacement.path);
}
}
}
}
for badge_file in sync_docs_version_badges(std::path::Path::new("docs/reference"), &version) {
updated.push(badge_file);
}
if any_node_pkg_modified {
run_optional("pnpm", &["install", "--no-frozen-lockfile", "--ignore-scripts", "-w"]);
}
if any_cargo_toml_modified {
run_optional("cargo", &["update", "--workspace", "--offline"]);
}
if any_composer_json_modified {
run_optional("composer", &["update", "--lock", "--no-interaction"]);
}
if any_mix_exs_modified {
run_optional("mix", &["deps.get"]);
}
let mut finalize_paths: std::collections::HashSet<std::path::PathBuf> =
updated.iter().map(std::path::PathBuf::from).collect();
finalize_paths.extend(text_replacement_paths);
if !finalize_paths.is_empty() {
let alef_toml_bytes = super::super::cache::read_alef_toml_bytes(config_path);
match super::super::cache::sources_hash(&config.sources) {
Ok(sources_hash) => {
match super::generate::finalize_hashes(&finalize_paths, &sources_hash, &alef_toml_bytes) {
Ok(n) if n > 0 => {
debug!(" Finalized alef:hash in {n} file(s)");
}
Ok(_) => {}
Err(e) => {
warn!("Could not finalize hashes after version sync: {e}");
}
}
}
Err(e) => {
warn!("Could not compute sources hash for finalize_hashes: {e}");
}
}
}
for file in &updated {
info!(" Updated: {file}");
}
if !updated.is_empty() && config.languages.contains(&Language::Ffi) {
let ffi_crate = config
.explicit_output
.ffi
.as_ref()
.and_then(|p| {
let p = p.to_string_lossy();
let trimmed = p.trim_end_matches('/');
let trimmed = trimmed.strip_suffix("/src").unwrap_or(trimmed);
trimmed.rsplit('/').next().map(|s| s.to_string())
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| format!("{}-ffi", config.core_crate_dir()));
info!("Rebuilding FFI ({ffi_crate}) to refresh C headers...");
let _ = run_command(&format!("cargo build -p {ffi_crate}"));
}
let _ = std::fs::create_dir_all(".alef");
let _ = std::fs::write(&last_path, &version);
match sync_registry_package_versions(config_path, &version) {
Ok(true) => {
info!("Updated registry package versions in {}", config_path.display());
}
Ok(false) => {}
Err(e) => {
warn!(
"Could not sync registry package versions in {}: {e}",
config_path.display()
);
}
}
if !no_regen {
if let Some(e2e_config) = config.e2e.as_ref() {
match regenerate_test_apps_after_sync(config, e2e_config, config_path) {
Ok(count) if count > 0 => {
info!(" Regenerated {count} test_apps file(s) with updated version pins");
}
Ok(_) => {}
Err(e) => {
warn!("Could not regenerate test_apps after version sync: {e}");
}
}
}
match regenerate_scaffold_after_sync(config, config_path) {
Ok(count) if count > 0 => {
info!(" Regenerated {count} scaffold file(s) with updated version pins");
}
Ok(_) => {}
Err(e) => {
warn!("Could not regenerate scaffold after version sync: {e}");
}
}
if let Ok(content) = std::fs::read_to_string("Package.swift") {
let new_content = content.replace("v__ALEF_SWIFT_VERSION__", &format!("v{version}"));
if new_content != content {
std::fs::write("Package.swift", &new_content)?;
if !updated.iter().any(|p| p == "Package.swift") {
updated.push("Package.swift".to_string());
}
}
}
if !skip_swift_checksum {
match precompute_swift_checksum(config) {
Ok(Some(checksum)) => {
info!("Swift artifactbundle checksum precomputed: {checksum}");
if !updated.iter().any(|p| p == "Package.swift") {
updated.push("Package.swift".to_string());
}
}
Ok(None) => {}
Err(e) => {
warn!("Swift checksum precompute failed: {e} — Package.swift retains placeholder");
}
}
}
}
if updated.is_empty() {
debug!("Versions already in sync — skipping README regeneration");
return Ok(());
}
let hashes_dir = std::path::Path::new(".alef").join("hashes");
for stem in ["readme", "docs", "scaffold"] {
for ext in [".hash", ".manifest", ".output_hashes"] {
let p = hashes_dir.join(format!("{stem}{ext}"));
if p.exists() {
let _ = std::fs::remove_file(&p);
}
}
}
info!("Regenerating READMEs with updated version");
match regenerate_readmes(config, config_path) {
Ok(count) => {
if count > 0 {
info!(" Regenerated {count} README(s)");
} else {
debug!(" No READMEs updated");
}
}
Err(e) => {
warn!("Could not regenerate READMEs: {e}");
}
}
Ok(())
}
pub(crate) fn render_registry_version(lang: &str, workspace_version: &str, existing_version: &str) -> Option<String> {
let prefix_len = existing_version.find(|c: char| c.is_ascii_digit()).unwrap_or(0);
let prefix = &existing_version[..prefix_len];
let rendered_core: String = match lang {
"python" => to_pep440(workspace_version),
"ruby" => to_rubygems_prerelease(workspace_version),
"r" => to_r_version(workspace_version),
_ => workspace_version.to_string(),
};
let new_version = format!("{prefix}{rendered_core}");
if new_version == existing_version {
None
} else {
Some(new_version)
}
}
fn update_zig_package_hash(existing_hash: &str, old_version: &str, new_version: &str) -> Option<String> {
let parts: Vec<&str> = existing_hash.split('-').collect();
if parts.len() < 3 {
return None; }
let base64_part = parts[parts.len() - 1];
let is_base64 = base64_part.contains('_') || base64_part.chars().next().is_some_and(|c| c.is_ascii_uppercase());
if !is_base64 {
return None; }
let middle_parts = &parts[1..parts.len() - 1]; let joined_middle = middle_parts.join("-");
if joined_middle.contains(old_version) {
let new_middle = joined_middle.replace(old_version, new_version);
let new_hash = format!("{}-{}-{}", parts[0], new_middle, base64_part);
if new_hash != existing_hash {
return Some(new_hash);
}
}
None
}
pub(crate) fn sync_registry_package_versions(
config_path: &std::path::Path,
workspace_version: &str,
) -> anyhow::Result<bool> {
use toml_edit::{DocumentMut, Item};
let content =
std::fs::read_to_string(config_path).with_context(|| format!("failed to read {}", config_path.display()))?;
let mut doc: DocumentMut = content
.parse()
.with_context(|| format!("failed to parse {} as TOML", config_path.display()))?;
let mut changed = false;
let crate_keys: Vec<String> = doc.iter().map(|(k, _)| k.to_string()).collect();
for key in &crate_keys {
if key != "crates" {
continue;
}
let crates_item = match doc.get_mut(key.as_str()) {
Some(item) => item,
None => continue,
};
fn patch_crate_table(crate_table: &mut dyn toml_edit::TableLike, workspace_version: &str) -> bool {
let e2e = match crate_table.get_mut("e2e").and_then(|i| i.as_table_like_mut()) {
Some(t) => t,
None => return false,
};
let registry = match e2e.get_mut("registry").and_then(|i| i.as_table_like_mut()) {
Some(t) => t,
None => return false,
};
let packages = match registry.get_mut("packages").and_then(|i| i.as_table_like_mut()) {
Some(t) => t,
None => return false,
};
let lang_keys: Vec<String> = packages.iter().map(|(k, _)| k.to_string()).collect();
let mut any = false;
for lang in &lang_keys {
let pkg = match packages.get_mut(lang.as_str()).and_then(|i| i.as_table_like_mut()) {
Some(t) => t,
None => continue,
};
let existing_version = match pkg.get("version").and_then(|i| i.as_str()) {
Some(v) => v.to_string(),
None => continue, };
if let Some(new_ver) = render_registry_version(lang, workspace_version, &existing_version) {
if let Some(ver_item) = pkg.get_mut("version") {
*ver_item = toml_edit::value(new_ver.clone());
any = true;
}
if lang == "zig" {
if let Some(hash_item) = pkg.get_mut("hash") {
if let Some(existing_hash) = hash_item.as_str() {
if let Some(new_hash) =
update_zig_package_hash(existing_hash, &existing_version, &new_ver)
{
*hash_item = toml_edit::value(new_hash);
}
}
}
}
}
}
any
}
if let Some(arr) = crates_item.as_array_of_tables_mut() {
for crate_table in arr.iter_mut() {
if patch_crate_table(crate_table, workspace_version) {
changed = true;
}
}
}
else if let Item::Table(tbl) = crates_item {
if patch_crate_table(tbl as &mut dyn toml_edit::TableLike, workspace_version) {
changed = true;
}
}
}
if changed {
let new_content = doc.to_string();
std::fs::write(config_path, &new_content)
.with_context(|| format!("failed to write {}", config_path.display()))?;
}
Ok(changed)
}
fn precompute_swift_checksum(config: &ResolvedCrateConfig) -> anyhow::Result<Option<String>> {
use super::helpers::run_command_captured;
let pkg_swift_path = std::path::Path::new("Package.swift");
let pkg_content = match std::fs::read_to_string(pkg_swift_path) {
Ok(c) => c,
Err(_) => {
debug!("Package.swift not found — skipping swift checksum precompute");
return Ok(None);
}
};
if !pkg_content.contains("__ALEF_SWIFT_CHECKSUM__") {
debug!("Package.swift already has a real checksum — skipping precompute");
return Ok(None);
}
if !config.languages.contains(&Language::Swift) {
debug!("Swift not configured — skipping swift checksum precompute");
return Ok(None);
}
let swift_crate = format!("{}-swift", config.name);
let candidate_manifests = [
format!("crates/{swift_crate}/Cargo.toml"),
"packages/swift/rust/Cargo.toml".to_string(),
];
let swift_manifest = candidate_manifests
.iter()
.find(|p| std::path::Path::new(p).exists())
.cloned();
let Some(swift_manifest) = swift_manifest else {
warn!(
"Swift binding crate `{swift_crate}` not found under any of {:?} — \
skipping checksum precompute. Run with --skip-swift-checksum to suppress.",
candidate_manifests
);
return Ok(None);
};
debug!("Using swift manifest: {swift_manifest}");
let bundle_dir = std::path::Path::new("dist/swift-artifactbundle");
let existing_zip = if bundle_dir.exists() {
std::fs::read_dir(bundle_dir).ok().and_then(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().and_then(|e| e.to_str()) == Some("zip"))
})
} else {
None
};
let zip_path = match existing_zip {
Some(p) => {
info!("Using pre-built artifactbundle: {}", p.display());
p
}
None => {
info!("Building swift artifactbundle for `{swift_crate}`…");
let build_cmd = format!("cargo build -p {swift_crate} --release --target aarch64-apple-darwin");
match run_command_captured(&build_cmd) {
Ok(_) => {}
Err(e) => {
warn!(
"Swift artifactbundle build failed (missing Xcode / Apple targets?): {e}\n\
Re-run with --skip-swift-checksum to skip this step."
);
return Ok(None);
}
}
std::fs::create_dir_all(bundle_dir).ok();
match std::fs::read_dir(bundle_dir).ok().and_then(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().and_then(|e| e.to_str()) == Some("zip"))
}) {
Some(p) => p,
None => {
warn!(
"No .zip found in `dist/swift-artifactbundle/` after build — \
skipping checksum substitution."
);
return Ok(None);
}
}
}
};
let checksum_cmd = format!("swift package compute-checksum {}", zip_path.display());
let checksum = match run_command_captured(&checksum_cmd) {
Ok((stdout, _)) => stdout.trim().to_string(),
Err(_) => {
info!("`swift` not found — computing SHA-256 in-process");
let bytes = std::fs::read(&zip_path).with_context(|| format!("failed to read {}", zip_path.display()))?;
compute_sha256_hex(&bytes)
}
};
if checksum.is_empty() {
warn!("Computed empty checksum — skipping substitution");
return Ok(None);
}
let new_content = pkg_content.replace("__ALEF_SWIFT_CHECKSUM__", &checksum);
std::fs::write(pkg_swift_path, &new_content).context("writing Package.swift with checksum")?;
info!("Substituted __ALEF_SWIFT_CHECKSUM__ → {checksum} in Package.swift");
std::fs::create_dir_all("target").ok();
std::fs::write("target/alef-swift-checksum.txt", &checksum).context("writing target/alef-swift-checksum.txt")?;
Ok(Some(checksum))
}
fn compute_sha256_hex(bytes: &[u8]) -> String {
use std::num::Wrapping;
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
let mut h: [Wrapping<u32>; 8] = [
Wrapping(0x6a09e667),
Wrapping(0xbb67ae85),
Wrapping(0x3c6ef372),
Wrapping(0xa54ff53a),
Wrapping(0x510e527f),
Wrapping(0x9b05688c),
Wrapping(0x1f83d9ab),
Wrapping(0x5be0cd19),
];
let bit_len = (bytes.len() as u64).wrapping_mul(8);
let mut msg = bytes.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0x00);
}
msg.extend_from_slice(&bit_len.to_be_bytes());
for chunk in msg.chunks_exact(64) {
let mut w = [Wrapping(0u32); 64];
for i in 0..16 {
w[i] = Wrapping(u32::from_be_bytes([
chunk[i * 4],
chunk[i * 4 + 1],
chunk[i * 4 + 2],
chunk[i * 4 + 3],
]));
}
for i in 16..64 {
let s0 = w[i - 15].0.rotate_right(7) ^ w[i - 15].0.rotate_right(18) ^ (w[i - 15].0 >> 3);
let s1 = w[i - 2].0.rotate_right(17) ^ w[i - 2].0.rotate_right(19) ^ (w[i - 2].0 >> 10);
w[i] = w[i - 16] + Wrapping(s0) + w[i - 7] + Wrapping(s1);
}
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
for i in 0..64 {
let s1 = e.0.rotate_right(6) ^ e.0.rotate_right(11) ^ e.0.rotate_right(25);
let ch = (e.0 & f.0) ^ ((!e.0) & g.0);
let temp1 = hh + Wrapping(s1) + Wrapping(ch) + Wrapping(K[i]) + w[i];
let s0 = a.0.rotate_right(2) ^ a.0.rotate_right(13) ^ a.0.rotate_right(22);
let maj = (a.0 & b.0) ^ (a.0 & c.0) ^ (b.0 & c.0);
let temp2 = Wrapping(s0) + Wrapping(maj);
hh = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
}
h[0] += a;
h[1] += b;
h[2] += c;
h[3] += d;
h[4] += e;
h[5] += f;
h[6] += g;
h[7] += hh;
}
format!(
"{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}{:08x}",
h[0].0, h[1].0, h[2].0, h[3].0, h[4].0, h[5].0, h[6].0, h[7].0
)
}
fn regenerate_test_apps_after_sync(
config: &ResolvedCrateConfig,
_e2e_config: &crate::core::config::e2e::E2eConfig,
config_path: &std::path::Path,
) -> anyhow::Result<usize> {
use crate::core::config::NewAlefConfig;
use crate::core::config::e2e::DependencyMode;
let raw = std::fs::read_to_string(config_path)
.with_context(|| format!("failed to read {} for test_apps regen", config_path.display()))?;
let new_alef_cfg: NewAlefConfig = toml::from_str(&raw)
.with_context(|| format!("failed to parse {} for test_apps regen", config_path.display()))?;
let mut resolved_crates = new_alef_cfg
.resolve()
.with_context(|| format!("failed to resolve {} for test_apps regen", config_path.display()))?;
let fresh_config = resolved_crates
.iter()
.position(|c| c.name == config.name && c.e2e.is_some())
.or_else(|| resolved_crates.iter().position(|c| c.e2e.is_some()))
.map(|idx| resolved_crates.swap_remove(idx))
.ok_or_else(|| anyhow::anyhow!("no crate with [e2e] block found in reloaded config"))?;
let e2e_config = fresh_config
.e2e
.as_ref()
.ok_or_else(|| anyhow::anyhow!("reloaded crate has no [e2e] block"))?;
let mut registry_config = e2e_config.clone();
registry_config.dep_mode = DependencyMode::Registry;
let e2e_ref = ®istry_config;
let api = extract(&fresh_config, config_path, false)?;
let files = crate::e2e::generate_e2e(&fresh_config, e2e_ref, None, &api.types, &api.enums)?;
if files.is_empty() {
return Ok(0);
}
let base_dir = std::path::PathBuf::from(".");
let count = super::generate::write_scaffold_files_with_overwrite(&files, &base_dir, true)?;
let sources_hash = super::super::cache::sources_hash(&fresh_config.sources)?;
let alef_toml_bytes = super::super::cache::read_alef_toml_bytes(config_path);
let path_set: std::collections::HashSet<std::path::PathBuf> =
files.iter().map(|f| base_dir.join(&f.path)).collect();
super::generate::finalize_hashes(&path_set, &sources_hash, &alef_toml_bytes)?;
Ok(count)
}
fn regenerate_scaffold_after_sync(
config: &ResolvedCrateConfig,
config_path: &std::path::Path,
) -> anyhow::Result<usize> {
use crate::core::config::NewAlefConfig;
let raw = std::fs::read_to_string(config_path)
.with_context(|| format!("failed to read {} for scaffold regen", config_path.display()))?;
let new_alef_cfg: NewAlefConfig = toml::from_str(&raw)
.with_context(|| format!("failed to parse {} for scaffold regen", config_path.display()))?;
let mut resolved_crates = new_alef_cfg
.resolve()
.with_context(|| format!("failed to resolve {} for scaffold regen", config_path.display()))?;
let fresh_config = resolved_crates
.iter()
.position(|c| c.name == config.name)
.or(Some(0))
.and_then(|idx| {
if idx < resolved_crates.len() {
Some(resolved_crates.swap_remove(idx))
} else {
None
}
})
.ok_or_else(|| anyhow::anyhow!("no crate found in reloaded config for scaffold regen"))?;
let api = extract(&fresh_config, config_path, false)?;
let languages = fresh_config.languages.clone();
let scaffold_files = super::scaffold(&api, &fresh_config, &languages)?;
if scaffold_files.is_empty() {
return Ok(0);
}
let base_dir = std::path::PathBuf::from(".");
let count = super::generate::write_scaffold_files_with_overwrite(&scaffold_files, &base_dir, true)?;
let sources_hash = super::super::cache::sources_hash(&fresh_config.sources)?;
let alef_toml_bytes = super::super::cache::read_alef_toml_bytes(config_path);
let path_set: std::collections::HashSet<std::path::PathBuf> =
scaffold_files.iter().map(|f| base_dir.join(&f.path)).collect();
super::generate::finalize_hashes(&path_set, &sources_hash, &alef_toml_bytes)?;
Ok(count)
}
fn regenerate_readmes(config: &ResolvedCrateConfig, config_path: &std::path::Path) -> anyhow::Result<usize> {
let api = extract(config, config_path, false)?;
let languages = config.languages.clone();
let readme_files = readme(&api, config, &languages)?;
let base_dir = std::path::PathBuf::from(".");
let sources_hash = super::super::cache::sources_hash(&config.sources)?;
let alef_toml_bytes = super::super::cache::read_alef_toml_bytes(config_path);
let count = super::generate::write_scaffold_files_with_overwrite(&readme_files, &base_dir, true)?;
let paths: std::collections::HashSet<std::path::PathBuf> =
readme_files.iter().map(|f| base_dir.join(&f.path)).collect();
super::generate::finalize_hashes(&paths, &sources_hash, &alef_toml_bytes)?;
Ok(count)
}
fn sync_gemfile_lock(content: &str, new_ruby_version: &str) -> Option<String> {
use std::sync::LazyLock;
static GEM_VERSION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(?m)^([ \t]*)([A-Za-z0-9_-]+) \(([^)]+)\)$").expect("valid regex")
});
let path_gem_names: std::collections::HashSet<String> = {
let mut names = std::collections::HashSet::new();
let mut in_specs = false;
for line in content.lines() {
if line.trim_start().starts_with("specs:") {
}
if line == "PATH" {
in_specs = true;
continue;
}
if in_specs && line.starts_with(" specs:") {
continue;
}
if in_specs && line.starts_with(" ") {
if let Some(caps) = GEM_VERSION_RE.captures(line) {
let indent = &caps[1];
let name = &caps[2];
if indent.len() == 4 {
names.insert(name.to_string());
}
}
continue;
}
if in_specs
&& !line.starts_with(" ")
&& !line.trim().is_empty()
&& line != "PATH"
&& !line.starts_with(" ")
{
in_specs = false;
}
}
names
};
if path_gem_names.is_empty() {
return None;
}
let mut changed = false;
let new_content = content
.lines()
.map(|line| {
if let Some(caps) = GEM_VERSION_RE.captures(line) {
let gem_name = &caps[2];
let current_version = &caps[3];
if path_gem_names.contains(gem_name) && current_version != new_ruby_version {
changed = true;
let indent = &caps[1];
return format!("{indent}{gem_name} ({new_ruby_version})");
}
}
line.to_string()
})
.collect::<Vec<_>>()
.join("\n");
let new_content = if content.ends_with('\n') {
format!("{new_content}\n")
} else {
new_content
};
if changed { Some(new_content) } else { None }
}
fn sync_e2e_java_pom(content: &str, new_version: &str) -> Option<String> {
use std::sync::LazyLock;
static DEP_BLOCK_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?s)<dependency>(.*?)</dependency>").expect("valid regex"));
static VERSION_TAG_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"<version>([^<]*)</version>").expect("valid regex"));
static SYSTEM_PATH_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(<systemPath>[^<]*?-)(\d+\.\d+\.\d+(?:-[A-Za-z0-9._]+)*)(\.[a-zA-Z]+</systemPath>)")
.expect("valid regex")
});
let mut result = content.to_string();
let mut changed = false;
let dep_matches: Vec<(usize, usize, String)> = DEP_BLOCK_RE
.find_iter(content)
.filter_map(|m| {
let block = m.as_str();
if !block.contains("<systemPath>") {
return None;
}
let new_block = VERSION_TAG_RE
.replace(block, |caps: ®ex::Captures<'_>| {
let ver = &caps[1];
if ver != new_version && !ver.contains('$') && !ver.contains('.') && ver.parse::<u64>().is_err() {
format!("<version>{ver}</version>")
} else if ver != new_version && ver.contains('.') && !ver.contains('$') {
format!("<version>{new_version}</version>")
} else {
format!("<version>{ver}</version>")
}
})
.into_owned();
let new_block = SYSTEM_PATH_RE
.replace(&new_block, |caps: ®ex::Captures<'_>| {
format!("{}{}{}", &caps[1], new_version, &caps[3])
})
.into_owned();
if new_block != block {
Some((m.start(), m.end(), new_block))
} else {
None
}
})
.collect();
for (start, end, new_block) in dep_matches.into_iter().rev() {
result.replace_range(start..end, &new_block);
changed = true;
}
if changed { Some(result) } else { None }
}
fn sync_e2e_go_mod(content: &str, module_path_fragment: &str, new_version: &str) -> Option<String> {
let mut changed = false;
let lines: Vec<String> = content
.lines()
.map(|line| {
let trimmed = line.trim();
if trimmed.starts_with(module_path_fragment) || line.trim_start().starts_with(module_path_fragment) {
if let Some(pos) = trimmed.rfind(" v") {
let current_ver = &trimmed[pos + 2..]; if current_ver != new_version {
changed = true;
let indent = &line[..line.len() - line.trim_start().len()];
let module_path = &trimmed[..pos];
return format!("{indent}{module_path} v{new_version}");
}
}
}
line.to_string()
})
.collect();
if !changed {
return None;
}
let new_content = lines.join("\n");
let new_content = if content.ends_with('\n') {
format!("{new_content}\n")
} else {
new_content
};
Some(new_content)
}
fn sync_e2e_dart_pubspec_lock(content: &str, new_version: &str) -> Option<String> {
let lines: Vec<&str> = content.lines().collect();
let n = lines.len();
let mut result: Vec<String> = Vec::with_capacity(n);
let mut changed = false;
let mut i = 0;
while i < n {
let line = lines[i];
if line.starts_with(" ") && !line.starts_with(" ") && line.trim_end().ends_with(':') {
let block_start = i;
i += 1;
while i < n && (lines[i].starts_with(" ") || lines[i].trim().is_empty()) {
i += 1;
}
let block = &lines[block_start..i];
let is_path_source = block.iter().any(|l| l.trim() == "source: path");
if is_path_source {
for &bline in block {
let trimmed = bline.trim();
if trimmed.starts_with("version:") {
let val = trimmed.trim_start_matches("version:").trim().trim_matches('"');
if val != new_version {
changed = true;
let indent = &bline[..bline.len() - bline.trim_start().len()];
result.push(format!("{indent}version: \"{new_version}\""));
} else {
result.push(bline.to_string());
}
} else {
result.push(bline.to_string());
}
}
} else {
for &bline in block {
result.push(bline.to_string());
}
}
} else {
result.push(line.to_string());
i += 1;
}
}
if !changed {
return None;
}
let new_content = result.join("\n");
let new_content = if content.ends_with('\n') {
format!("{new_content}\n")
} else {
new_content
};
Some(new_content)
}
fn read_workspace_license(version_from: &str) -> Option<String> {
let content = std::fs::read_to_string(version_from).ok()?;
let value: toml::Value = toml::from_str(&content).ok()?;
value
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("license"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
value
.get("package")
.and_then(|p| p.get("license"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
}
fn render_citation_cff(citation: &CitationConfig, version: &str, fallback_license: Option<&str>) -> String {
let mut out = String::new();
out.push_str("# This file is generated by alef sync-versions; do not edit by hand.\n");
out.push_str("# Source: [workspace.citation] in alef.toml + workspace version in Cargo.toml.\n");
out.push_str("cff-version: 1.2.0\n");
out.push_str(&format!("message: {}\n", yaml_scalar(&citation.message)));
out.push_str(&format!("title: {}\n", yaml_scalar(&citation.title)));
out.push_str(&format!("abstract: {}\n", yaml_scalar(&citation.abstract_)));
out.push_str("authors:\n");
for author in &citation.authors {
out.push_str(&render_citation_author(author));
}
out.push_str(&format!(
"repository-code: {}\n",
yaml_scalar(&citation.repository_code)
));
if let Some(url) = &citation.url {
out.push_str(&format!("url: {}\n", yaml_scalar(url)));
}
let license = citation.license.as_deref().or(fallback_license);
if let Some(license) = license {
out.push_str(&format!("license: {}\n", yaml_scalar(license)));
}
out.push_str(&format!("version: {version}\n"));
if let Some(date) = &citation.date_released {
out.push_str(&format!("date-released: {}\n", yaml_scalar(date)));
}
if let Some(doi) = &citation.doi {
out.push_str(&format!("doi: {}\n", yaml_scalar(doi)));
}
out
}
fn render_citation_author(author: &CitationAuthor) -> String {
let mut entry = String::new();
let person_form = author.family_names.is_some() || author.given_names.is_some();
if person_form {
if let Some(family) = &author.family_names {
entry.push_str(&format!(" - family-names: {}\n", yaml_scalar(family)));
if let Some(given) = &author.given_names {
entry.push_str(&format!(" given-names: {}\n", yaml_scalar(given)));
}
} else if let Some(given) = &author.given_names {
entry.push_str(&format!(" - given-names: {}\n", yaml_scalar(given)));
}
if let Some(email) = &author.email {
entry.push_str(&format!(" email: {}\n", yaml_scalar(email)));
}
if let Some(orcid) = &author.orcid {
entry.push_str(&format!(" orcid: {}\n", yaml_scalar(orcid)));
}
} else if let Some(name) = &author.name {
entry.push_str(&format!(" - name: {}\n", yaml_scalar(name)));
if let Some(email) = &author.email {
entry.push_str(&format!(" email: {}\n", yaml_scalar(email)));
}
if let Some(orcid) = &author.orcid {
entry.push_str(&format!(" orcid: {}\n", yaml_scalar(orcid)));
}
}
entry
}
fn yaml_scalar(value: &str) -> String {
let needs_quoting = value.is_empty()
|| value.contains(':')
|| value.contains('#')
|| value.contains('"')
|| value.contains('\\')
|| value.contains('\n')
|| value.contains('\t')
|| value.contains(' ')
|| value.contains('\'')
|| value.contains('@')
|| value.starts_with(['!', '&', '*', '?', '|', '>', '"', '%', '`', '[', ']', '{', '}', ',']);
if needs_quoting {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
} else {
value.to_string()
}
}
static CITATION_VERSION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"(?m)^(version:[ \t]+)(?:"([^"\n]*)"|'([^'\n]*)'|([^\s#'"]+))[ \t]*(?:#[^\n]*)?$"#)
.expect("valid regex")
});
fn replace_citation_version(content: &str, new_version: &str) -> Option<String> {
let captures = CITATION_VERSION_RE.captures(content)?;
let (current, replacement) = if let Some(value) = captures.get(2) {
(value.as_str(), format!("{}\"{new_version}\"", &captures[1]))
} else if let Some(value) = captures.get(3) {
(value.as_str(), format!("{}'{new_version}'", &captures[1]))
} else if let Some(value) = captures.get(4) {
(value.as_str(), format!("{}{new_version}", &captures[1]))
} else {
return None;
};
if current == new_version {
return None;
}
let new_content = CITATION_VERSION_RE.replace(content, replacement.as_str()).into_owned();
if new_content == content {
return None;
}
Some(new_content)
}
fn replace_version_pattern(content: &str, pattern: &str, version: &str) -> Option<String> {
let regex = regex::Regex::new(pattern).ok()?;
let captures = regex.captures(content)?;
let matched = captures.get(0)?.as_str();
if matched_version_equals(matched, version) {
return None;
}
let replacement = match pattern {
p if p.contains("version =") && !p.contains("spec") && !p.contains("VERSION") => {
format!(r#"version = "{version}""#)
}
p if p.contains("\"version\"") && p.contains("\"") => format!(r#""version": "{version}""#),
p if p.contains("spec") => format!("spec.version = \"{version}\""),
p if p.contains("<version>") => format!("<version>{version}</version>"),
p if p.contains("<Version>") => format!("<Version>{version}</Version>"),
p if p.contains("@version") => format!(r#"@version "{version}""#),
p if p.contains("version:") && p.contains(":") => format!(r#"version: "{version}""#),
p if p.contains("__version__") => format!(r#"__version__ = "{version}""#),
p if p.contains("defaultFFIVersion") => format!(r#"defaultFFIVersion = "{version}""#),
p if p.contains("moduleVersion") => format!(r#"moduleVersion = "{version}""#),
p if p.contains("Version:") => format!("Version: {version}"),
p if p.contains("from:") => format!(r#"from: "{version}""#),
p if p.contains("VERSION=\"") => format!(r#"VERSION="{version}""#),
p if p.contains("VERSION") => format!("VERSION = \"{version}\""),
_ => return None,
};
let new_content = regex.replace(content, replacement.as_str()).to_string();
if new_content == content {
return None;
}
Some(new_content)
}
fn matched_version_equals(matched: &str, target: &str) -> bool {
extract_version_literal(matched).is_some_and(|v| v == target)
}
fn restore_gleam_dep_ranges(content: &str) -> String {
use crate::core::template_versions::hex;
static GLEAM_DEP_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"(?m)^(gleam_stdlib|gleeunit)\s*=\s*"([^"]*)""#).expect("valid regex")
});
GLEAM_DEP_RE
.replace_all(content, |caps: ®ex::Captures<'_>| {
let name = &caps[1];
let canonical = match name {
"gleam_stdlib" => hex::GLEAM_STDLIB_VERSION_RANGE,
"gleeunit" => hex::GLEEUNIT_VERSION_RANGE,
_ => return caps[0].to_string(),
};
format!("{name} = \"{canonical}\"")
})
.into_owned()
}
fn extract_version_literal(matched: &str) -> Option<&str> {
if let Some(start) = matched.find(['"', '\'']) {
let quote = matched.as_bytes()[start];
let rest = &matched[start + 1..];
if let Some(end) = rest.find(quote as char) {
return Some(&rest[..end]);
}
}
if let Some(close) = matched.find('>') {
let rest = &matched[close + 1..];
if let Some(end) = rest.find('<') {
return Some(&rest[..end]);
}
}
if let Some(colon) = matched.find(':') {
return Some(matched[colon + 1..].trim());
}
if let Some(eq) = matched.find('=') {
return Some(matched[eq + 1..].trim());
}
None
}
fn replace_gradle_project_version(content: &str, new_version: &str) -> Option<String> {
static GRADLE_VERSION_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"(?m)^(\s*)version\s*=\s*"[^"]*""#).expect("valid regex"));
let captures = GRADLE_VERSION_RE.captures(content)?;
let matched = captures.get(0)?.as_str();
if matched_version_equals(matched, new_version) {
return None;
}
let indent = captures.get(1).map(|m| m.as_str()).unwrap_or("");
let replacement = format!(r#"{indent}version = "{new_version}""#);
let new_content = GRADLE_VERSION_RE.replace(content, replacement.as_str()).into_owned();
if new_content == content {
return None;
}
Some(new_content)
}
fn sync_cargo_lock_path_versions(content: &str, new_version: &str) -> Option<String> {
let mut out = String::with_capacity(content.len());
let mut changed = false;
let mut block: Vec<&str> = Vec::new();
let mut in_package_block = false;
let flush = |block: &mut Vec<&str>, out: &mut String, changed: &mut bool| {
if block.is_empty() {
return;
}
let is_package = block.first().is_some_and(|l| l.trim() == "[[package]]");
let has_source = block.iter().any(|l| l.trim_start().starts_with("source = "));
for line in block.iter() {
if is_package && !has_source && line.trim_start().starts_with("version = ") {
let indent_len = line.len() - line.trim_start().len();
let indent = &line[..indent_len];
let rewritten = format!(r#"{indent}version = "{new_version}""#);
if rewritten != *line {
*changed = true;
}
out.push_str(&rewritten);
} else {
out.push_str(line);
}
out.push('\n');
}
block.clear();
};
for line in content.lines() {
if line.trim() == "[[package]]" {
flush(&mut block, &mut out, &mut changed);
in_package_block = true;
block.push(line);
} else if in_package_block {
block.push(line);
} else {
out.push_str(line);
out.push('\n');
}
}
flush(&mut block, &mut out, &mut changed);
if !changed {
return None;
}
if !content.ends_with('\n') {
out.pop();
}
Some(out)
}
fn sync_docs_version_badges(docs_reference_dir: &std::path::Path, new_version: &str) -> Vec<String> {
static BADGE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"(<span class="version-badge">v)[^<]*(</span>)"#).expect("valid regex"));
let mut updated = Vec::new();
let pattern = docs_reference_dir.join("api-*.md");
let Some(pattern_str) = pattern.to_str() else {
return updated;
};
for entry in glob::glob(pattern_str).into_iter().flatten().flatten() {
let Ok(content) = std::fs::read_to_string(&entry) else {
continue;
};
let replacement = format!("${{1}}{new_version}${{2}}");
let new_content = BADGE_RE.replace_all(&content, replacement.as_str()).into_owned();
if new_content != content {
if let Err(e) = std::fs::write(&entry, &new_content) {
debug!("Could not write {}: {e}", entry.display());
} else {
updated.push(entry.to_string_lossy().to_string());
}
}
}
updated
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::pipeline::generate;
#[test]
fn to_pep440_no_prerelease_passthrough() {
assert_eq!(to_pep440("1.2.3"), "1.2.3");
assert_eq!(to_pep440("0.1.0"), "0.1.0");
}
#[test]
fn to_pep440_rc_prerelease() {
assert_eq!(to_pep440("0.1.0-rc.1"), "0.1.0rc1");
assert_eq!(to_pep440("4.10.0-rc.9"), "4.10.0rc9");
}
#[test]
fn to_pep440_alpha_beta_prerelease() {
assert_eq!(to_pep440("1.0.0-alpha.2"), "1.0.0a2");
assert_eq!(to_pep440("1.0.0-beta.3"), "1.0.0b3");
}
#[test]
fn to_pep440_strips_internal_dots() {
assert_eq!(to_pep440("0.1.0-rc.1.2"), "0.1.0rc12");
}
#[test]
fn zon_version_regex_anchors_to_dot_version_only() {
let re = regex::Regex::new(r#"(?m)^\s*\.version\s*=\s*"([^"]*)""#).expect("valid regex");
let zon = r#".{
.name = .my_pkg,
.version = "1.9.0-rc.1",
.fingerprint = 0x6f52c41163f42c8c,
.minimum_zig_version = "0.16.0",
}
"#;
let captures: Vec<_> = re.captures_iter(zon).collect();
assert_eq!(
captures.len(),
1,
"regex must match exactly one line, not .minimum_zig_version"
);
assert_eq!(&captures[0][1], "1.9.0-rc.1");
}
#[test]
fn matched_version_equals_treats_quote_style_uniformly() {
assert!(matched_version_equals("VERSION = '1.0.0'", "1.0.0"));
assert!(matched_version_equals("VERSION = \"1.0.0\"", "1.0.0"));
assert!(!matched_version_equals("VERSION = '1.0.0'", "2.0.0"));
assert!(matched_version_equals("<version>1.0.0</version>", "1.0.0"));
assert!(matched_version_equals("Version: 1.0.0", "1.0.0"));
}
fn citation_author_person() -> CitationAuthor {
CitationAuthor {
family_names: Some("Hirschfeld".to_string()),
given_names: Some("Na'aman".to_string()),
name: None,
email: Some("naaman@sample_crate.dev".to_string()),
orcid: Some("https://orcid.org/0009-0000-2247-5072".to_string()),
}
}
fn citation_author_entity() -> CitationAuthor {
CitationAuthor {
family_names: None,
given_names: None,
name: Some("SampleCrate, Inc.".to_string()),
email: None,
orcid: None,
}
}
fn citation_config_mit() -> CitationConfig {
CitationConfig {
title: "sample-markdown".to_string(),
abstract_: "Fast markup conversion converter.".to_string(),
authors: vec![citation_author_person()],
message: "If you use this software, please cite it using the metadata below.".to_string(),
repository_code: "https://github.com/sample_crate-dev/sample-markdown".to_string(),
url: Some("https://sample_crate.dev".to_string()),
license: Some("MIT".to_string()),
date_released: Some("2026-05-17".to_string()),
doi: None,
}
}
#[test]
fn render_citation_cff_mit_full_round_trip() {
let rendered = render_citation_cff(&citation_config_mit(), "3.5.0", None);
let expected = r#"# This file is generated by alef sync-versions; do not edit by hand.
# Source: [workspace.citation] in alef.toml + workspace version in Cargo.toml.
cff-version: 1.2.0
message: "If you use this software, please cite it using the metadata below."
title: sample-markdown
abstract: "Fast markup conversion converter."
authors:
- family-names: Hirschfeld
given-names: "Na'aman"
email: "naaman@sample_crate.dev"
orcid: "https://orcid.org/0009-0000-2247-5072"
repository-code: "https://github.com/sample_crate-dev/sample-markdown"
url: "https://sample_crate.dev"
license: MIT
version: 3.5.0
date-released: 2026-05-17
"#;
assert_eq!(rendered, expected);
}
#[test]
fn render_citation_cff_elv2_with_entity_author() {
let mut config = citation_config_mit();
config.title = "sample_crate".to_string();
config.repository_code = "https://github.com/sample_crate-dev/sample_crate".to_string();
config.license = Some("Elastic-2.0".to_string());
config.authors = vec![citation_author_person(), citation_author_entity()];
let rendered = render_citation_cff(&config, "5.0.0-rc.1", None);
assert!(rendered.contains(" - family-names: Hirschfeld\n given-names: \"Na'aman\""));
assert!(rendered.contains(" - name: \"SampleCrate, Inc.\"\n"));
assert!(rendered.contains("license: Elastic-2.0\n"));
assert!(rendered.contains("version: 5.0.0-rc.1\n"));
}
#[test]
fn render_citation_cff_falls_back_to_cargo_license() {
let mut config = citation_config_mit();
config.license = None;
let rendered = render_citation_cff(&config, "1.0.0", Some("Apache-2.0"));
assert!(rendered.contains("license: Apache-2.0\n"));
}
#[test]
fn render_citation_cff_omits_optional_fields_when_unset() {
let config = CitationConfig {
title: "tiny".to_string(),
abstract_: "Tiny library.".to_string(),
authors: vec![citation_author_person()],
message: "Cite me.".to_string(),
repository_code: "https://example.com/tiny".to_string(),
url: None,
license: None,
date_released: None,
doi: None,
};
let rendered = render_citation_cff(&config, "0.1.0", None);
assert!(!rendered.contains("url:"));
assert!(!rendered.contains("license:"));
assert!(!rendered.contains("date-released:"));
assert!(!rendered.contains("doi:"));
}
#[test]
fn render_citation_cff_idempotent_for_unchanged_version() {
let config = citation_config_mit();
let first = render_citation_cff(&config, "3.5.0", None);
let second = render_citation_cff(&config, "3.5.0", None);
assert_eq!(first, second);
}
#[test]
fn replace_citation_version_unquoted_scalar() {
let content = "cff-version: 1.2.0\ntitle: example\nversion: 1.0.0\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert!(new.contains("version: 2.0.0\n"));
assert!(new.contains("title: example\n"));
assert!(!new.contains("1.0.0"));
}
#[test]
fn replace_citation_version_double_quoted_preserves_quotes() {
let content = "version: \"1.0.0\"\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert_eq!(new, "version: \"2.0.0\"\n");
}
#[test]
fn replace_citation_version_single_quoted_preserves_quotes() {
let content = "version: '1.0.0'\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert_eq!(new, "version: '2.0.0'\n");
}
#[test]
fn replace_citation_version_rc_suffix_passes_through() {
let content = "version: 5.0.0-rc.1\n";
let new = replace_citation_version(content, "5.0.0-rc.2").expect("regex matched");
assert_eq!(new, "version: 5.0.0-rc.2\n");
}
#[test]
fn replace_citation_version_no_op_when_already_current() {
let content = "version: 1.0.0\n";
assert!(replace_citation_version(content, "1.0.0").is_none());
}
#[test]
fn replace_citation_version_ignores_nested_version_keys() {
let content = "version: 1.0.0\nreferences:\n - type: software\n version: 9.9.9\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert!(new.starts_with("version: 2.0.0\n"));
assert!(new.contains(" version: 9.9.9\n"));
}
#[test]
fn test_replace_version_pattern_ruby_version() {
let content = r#"# This file is auto-generated by alef
module SampleCrate
VERSION = "1.0.0"
end
"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(
new_content,
r#"# This file is auto-generated by alef
module SampleCrate
VERSION = "2.0.0"
end
"#
);
}
#[test]
fn test_replace_version_pattern_ruby_version_single_quotes() {
let content = "VERSION = '1.5.2'";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(new_content, "VERSION = \"2.0.0\"");
}
#[test]
fn test_replace_version_pattern_ruby_version_double_quotes() {
let content = "VERSION = \"1.5.2\"";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "3.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(new_content, "VERSION = \"3.0.0\"");
}
#[test]
fn test_replace_version_pattern_ruby_in_module() {
let content = r#"module MyGem
VERSION = "0.5.0"
end"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "1.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert!(new_content.contains("VERSION = \"1.0.0\""));
assert!(!new_content.contains("0.5.0"));
}
#[test]
fn test_replace_version_pattern_no_match() {
let content = "NOTHING = \"1.0.0\"";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_none());
}
#[test]
fn test_replace_version_pattern_preserves_other_content() {
let content = r#"# frozen_string_literal: true
module SampleCrate
VERSION = "1.0.0"
# Other stuff
CONST = "something"
end"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert!(new_content.contains("# frozen_string_literal: true"));
assert!(new_content.contains("CONST = \"something\""));
assert!(new_content.contains("VERSION = \"2.0.0\""));
}
#[test]
fn test_finalize_hashes_updates_alef_hash_line() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("version.rb");
let content = "# This file is auto-generated by alef — do not edit manually.\n# frozen_string_literal: true\n\nmodule MyGem\n VERSION = '2.0.0'\nend\n";
std::fs::write(&path, content).expect("write");
let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";
let n = generate::finalize_hashes(&paths, "test-sources-hash", alef_toml_bytes).expect("finalize ok");
assert_eq!(n, 1, "finalize_hashes must update the file with the alef:hash line");
let updated = std::fs::read_to_string(&path).expect("read");
assert!(
updated.contains("alef:hash:"),
"file must contain alef:hash: after finalize_hashes, got:\n{updated}"
);
}
#[test]
fn test_finalize_hashes_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("version.rb");
let content =
"# This file is auto-generated by alef — do not edit manually.\n\nmodule MyGem\n VERSION = '2.0.0'\nend\n";
std::fs::write(&path, content).expect("write");
let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";
let _ = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("first finalize");
let after_first = std::fs::read_to_string(&path).expect("read after first");
let n2 = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("second finalize");
assert_eq!(n2, 0, "second finalize_hashes must be a no-op (same inputs hash)");
let after_second = std::fs::read_to_string(&path).expect("read after second");
assert_eq!(after_first, after_second, "content must not change on second finalize");
}
const GEMFILE_LOCK_SAMPLE: &str = "\
PATH
remote: .
specs:
sample_crate (4.10.0.pre.rc.13)
rb_sys (~> 0.9)
GEM
remote: https://rubygems.org/
specs:
rake (13.4.2)
PLATFORMS
ruby
DEPENDENCIES
sample_crate!
CHECKSUMS
sample_crate (4.10.0.pre.rc.13)
rake (13.4.2) sha256=abcdef
BUNDLED WITH
4.0.7
";
#[test]
fn sync_gemfile_lock_updates_both_occurrences() {
let result = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains(" sample_crate (4.10.0.pre.rc.14)"),
"PATH specs entry not updated:\n{new}"
);
assert!(
new.contains(" sample_crate (4.10.0.pre.rc.14)"),
"CHECKSUMS entry not updated:\n{new}"
);
assert!(
new.contains("rake (13.4.2)"),
"non-path gem version must not change:\n{new}"
);
assert!(!new.contains("4.10.0.pre.rc.13"), "old version must be removed:\n{new}");
}
#[test]
fn sync_gemfile_lock_is_idempotent() {
let first = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14").unwrap();
let second = sync_gemfile_lock(&first, "4.10.0.pre.rc.14");
assert!(
second.is_none(),
"second call with same version must return None (already in sync)"
);
}
#[test]
fn sync_gemfile_lock_preserves_trailing_newline() {
let with_newline = format!("{GEMFILE_LOCK_SAMPLE}\n");
let result = sync_gemfile_lock(&with_newline, "4.10.0.pre.rc.99").unwrap();
assert!(result.ends_with('\n'), "trailing newline must be preserved");
}
#[test]
fn sync_gemfile_lock_no_path_gem_returns_none() {
let content = "GEM\n remote: https://rubygems.org/\n specs:\n rake (13.4.2)\n";
let result = sync_gemfile_lock(content, "1.0.0");
assert!(result.is_none(), "no PATH gem means nothing to update");
}
#[test]
fn restore_gleam_dep_ranges_repairs_corrupted_workspace_version_ranges() {
let corrupted = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\ntarget = \"erlang\"\n\n[dependencies]\ngleam_stdlib = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n\n[dev-dependencies]\ngleeunit = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n";
let healed = restore_gleam_dep_ranges(corrupted);
assert!(
healed.contains("gleam_stdlib = \">= 0.34.0 and < 2.0.0\""),
"gleam_stdlib should be restored to canonical range, got:\n{healed}"
);
assert!(
healed.contains("gleeunit = \">= 1.0.0 and < 2.0.0\""),
"gleeunit should be restored to canonical range, got:\n{healed}"
);
assert!(
healed.contains("version = \"5.0.0-rc.1\""),
"package version must not be rewritten, got:\n{healed}"
);
}
#[test]
fn restore_gleam_dep_ranges_is_idempotent_on_healthy_input() {
let healthy = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\ngleam_stdlib = \">= 0.34.0 and < 2.0.0\"\n\n[dev-dependencies]\ngleeunit = \">= 1.0.0 and < 2.0.0\"\n";
let healed = restore_gleam_dep_ranges(healthy);
assert_eq!(healed, healthy, "healthy gleam.toml must not be rewritten");
}
#[test]
fn test_replace_version_pattern_root_package_json_only_top_level() {
let content = r#"{
"name": "sample_crate-root",
"version": "4.9.5",
"private": true,
"devDependencies": {
"@vitest/coverage-v8": "^4.1.5",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
},
"pnpm": {
"overrides": {
"glob": "10.5.0"
}
}
}
"#;
let new_content = replace_version_pattern(content, r#""version":\s*"[^"]*""#, "5.0.0-rc.1")
.expect("root package.json version must update");
assert!(
new_content.contains(r#""version": "5.0.0-rc.1""#),
"top-level version must be rewritten, got:\n{new_content}"
);
assert!(
!new_content.contains(r#""version": "4.9.5""#),
"old version must be removed, got:\n{new_content}"
);
assert!(
new_content.contains("\"@vitest/coverage-v8\": \"^4.1.5\""),
"devDependency version specs must not be touched, got:\n{new_content}"
);
assert!(
new_content.contains("\"glob\": \"10.5.0\""),
"pnpm overrides must not be touched, got:\n{new_content}"
);
}
static CWD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn sync_versions_writes_root_and_node_crate_package_json() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::write(
root.join("package.json"),
"{\n \"name\": \"mylib-root\",\n \"version\": \"0.9.0\",\n \"private\": true\n}\n",
)
.expect("write root package.json");
std::fs::create_dir_all(root.join("crates/mylib-node")).expect("mkdir crates/mylib-node");
std::fs::write(
root.join("crates/mylib-node/package.json"),
"{\n \"name\": \"mylib\",\n \"version\": \"0.9.0\"\n}\n",
)
.expect("write crates/mylib-node/package.json");
let alef_toml = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_pkg = std::fs::read_to_string(root.join("package.json")).expect("read root package.json");
assert!(
root_pkg.contains(r#""version": "1.0.0""#),
"root package.json must be bumped to canonical version, got:\n{root_pkg}"
);
assert!(
!root_pkg.contains("0.9.0"),
"old version must be gone from root package.json, got:\n{root_pkg}"
);
let node_pkg = std::fs::read_to_string(root.join("crates/mylib-node/package.json"))
.expect("read crates/mylib-node/package.json");
assert!(
node_pkg.contains(r#""version": "1.0.0""#),
"crates/*-node/package.json must be bumped to canonical version, got:\n{node_pkg}"
);
}
#[test]
fn sync_versions_bumps_napi_platform_pins_and_manifests() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("crates/mylib-node")).expect("mkdir crates/mylib-node");
std::fs::write(
root.join("crates/mylib-node/package.json"),
"{\n \"name\": \"@scope/mylib\",\n \"version\": \"0.9.0\",\n \"optionalDependencies\": {\n \"@scope/mylib-linux-x64-gnu\": \"0.9.0\",\n \"@scope/mylib-darwin-arm64\": \"0.9.0\",\n \"@scope/mylib-win32-x64-msvc\": \"0.9.0\"\n }\n}\n",
)
.expect("write crates/mylib-node/package.json");
for platform in &["linux-x64-gnu", "darwin-arm64", "win32-x64-msvc"] {
let dir = root.join(format!("crates/mylib-node/npm/{platform}"));
std::fs::create_dir_all(&dir).expect("mkdir platform dir");
std::fs::write(
dir.join("package.json"),
format!("{{\n \"name\": \"@scope/mylib-{platform}\",\n \"version\": \"0.9.0\"\n}}\n"),
)
.expect("write platform package.json");
}
let alef_toml = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let crate_pkg = std::fs::read_to_string(root.join("crates/mylib-node/package.json"))
.expect("read crates/mylib-node/package.json");
assert!(
!crate_pkg.contains("0.9.0"),
"old version must be gone from crates/mylib-node/package.json (including optionalDependencies), got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-linux-x64-gnu": "1.0.0""#),
"optionalDependencies pin to linux-x64-gnu must be bumped, got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-darwin-arm64": "1.0.0""#),
"optionalDependencies pin to darwin-arm64 must be bumped, got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-win32-x64-msvc": "1.0.0""#),
"optionalDependencies pin to win32-x64-msvc must be bumped, got:\n{crate_pkg}"
);
for platform in &["linux-x64-gnu", "darwin-arm64", "win32-x64-msvc"] {
let manifest = std::fs::read_to_string(root.join(format!("crates/mylib-node/npm/{platform}/package.json")))
.expect("read platform package.json");
assert!(
manifest.contains(r#""version": "1.0.0""#),
"platform manifest {platform} must be bumped, got:\n{manifest}"
);
assert!(
!manifest.contains("0.9.0"),
"old version must be gone from platform manifest {platform}, got:\n{manifest}"
);
}
}
#[test]
fn sync_versions_bumps_both_python_pyprojects_to_pep440_prerelease() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"0.15.6-rc.2\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/python")).expect("mkdir packages/python");
std::fs::write(
root.join("packages/python/pyproject.toml"),
"[project]\nname = \"mylib\"\nversion = \"0.15.5\"\n",
)
.expect("write packages/python/pyproject.toml");
std::fs::create_dir_all(root.join("crates/mylib-py/src")).expect("mkdir crates/mylib-py/src");
std::fs::write(
root.join("crates/mylib-py/src/pyproject.toml"),
"[project]\nname = \"mylib\"\nversion = \"0.15.5\"\n",
)
.expect("write crates/mylib-py/src/pyproject.toml");
let alef_toml = format!(
"[workspace]\nlanguages = [\"python\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n[crates.output]\npython = \"crates/mylib-py/src/\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let consumer =
std::fs::read_to_string(root.join("packages/python/pyproject.toml")).expect("read consumer pyproject");
assert!(
consumer.contains(r#"version = "0.15.6rc2""#),
"consumer pyproject must be PEP 440 normalised, got:\n{consumer}"
);
let source = std::fs::read_to_string(root.join("crates/mylib-py/src/pyproject.toml"))
.expect("read source-template pyproject");
assert!(
source.contains(r#"version = "0.15.6rc2""#),
"source-template pyproject must be PEP 440 normalised, got:\n{source}"
);
assert!(
!source.contains("0.15.5") && !source.contains("0.15.6-rc.2"),
"source-template must hold only the normalised version, got:\n{source}"
);
}
#[test]
fn patch_workspace_dep_versions_all_dep_table_shapes() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml = r#"[package]
name = "crate-a"
version = "5.0.0-rc.1"
[dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1", optional = true }
serde = "1.0"
[dev-dependencies]
crate-c = { path = "../crate-c", version = "5.0.0-rc.1" }
tempfile = "3"
[build-dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1" }
[target.'cfg(unix)'.dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1", optional = true }
libc = "0.2"
[workspace.dependencies]
crate-c = { path = "../crate-c", version = "5.0.0-rc.1", default-features = false }
tokio = { version = "1.0", features = ["full"] }
"#;
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = ["crate-b", "crate-c"].iter().map(|s| s.to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "5.0.0-rc.2", &members).expect("patch ok");
assert!(changed, "at least one version pin must have been updated");
let result = std::fs::read_to_string(&path).expect("read");
let crate_b_lines: Vec<&str> = result
.lines()
.filter(|l| l.contains("crate-b") && l.contains("version"))
.collect();
assert!(
!crate_b_lines.is_empty(),
"expected crate-b dep lines with version=:\n{result}"
);
for line in &crate_b_lines {
assert!(
line.contains("5.0.0-rc.2"),
"crate-b pin not bumped:\n {line}\nfull:\n{result}"
);
}
let crate_c_lines: Vec<&str> = result
.lines()
.filter(|l| l.contains("crate-c") && l.contains("version"))
.collect();
assert!(
!crate_c_lines.is_empty(),
"expected crate-c dep lines with version=:\n{result}"
);
for line in &crate_c_lines {
assert!(
line.contains("5.0.0-rc.2"),
"crate-c pin not bumped:\n {line}\nfull:\n{result}"
);
}
assert!(
result.contains(r#"serde = "1.0""#),
"serde must not be touched:\n{result}"
);
assert!(
result.contains(r#"tempfile = "3""#),
"tempfile must not be touched:\n{result}"
);
assert!(
result.contains(r#"libc = "0.2""#),
"libc must not be touched:\n{result}"
);
assert!(
result.contains(r#"tokio = { version = "1.0", features = ["full"] }"#),
"tokio must not be touched:\n{result}"
);
}
#[test]
fn patch_workspace_dep_versions_is_idempotent() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml = "[package]\nname = \"crate-a\"\nversion = \"5.0.0-rc.2\"\n\n[dependencies]\ncrate-b = { path = \"../crate-b\", version = \"5.0.0-rc.2\" }\n";
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = std::iter::once("crate-b".to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "5.0.0-rc.2", &members).expect("patch ok");
assert!(!changed, "no change expected when already at target version");
}
#[test]
fn patch_workspace_dep_versions_skips_path_only_deps() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml = "[package]\nname = \"crate-a\"\nversion = \"1.0.0\"\n\n[dependencies]\ncrate-b = { path = \"../crate-b\" }\n";
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = std::iter::once("crate-b".to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "2.0.0", &members).expect("patch ok");
assert!(!changed, "path-only deps without version= must not be touched");
}
#[test]
fn sync_versions_patches_dep_tables_on_version_change() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fn write_file(dir: &std::path::Path, rel: &str, content: &str) {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir");
}
std::fs::write(path, content).expect("write");
}
write_file(
root,
"Cargo.toml",
"[workspace.package]\nversion = \"5.0.0-rc.2\"\n\n[workspace]\nresolver = \"2\"\nmembers = [\"crates/alpha\", \"crates/beta\"]\n\n[workspace.dependencies]\nalpha = { path = \"crates/alpha\", version = \"5.0.0-rc.1\", default-features = false }\nserde = \"1.0\"\n",
);
write_file(
root,
"crates/alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\nserde = \"1.0\"\n",
);
write_file(
root,
"crates/beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\", optional = true }\nserde = \"1.0\"\n\n[dev-dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\" }\ntempfile = \"3\"\n\n[build-dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\" }\n\n[target.'cfg(unix)'.dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\", features = [\"unix\"] }\nlibc = \"0.2\"\n",
);
let alef_toml_content = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"alpha\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
write_file(root, "alef.toml", &alef_toml_content);
let alef_toml_path = root.join("alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml_content).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_cargo = std::fs::read_to_string(root.join("Cargo.toml")).expect("read root");
assert!(
root_cargo.contains(r#"alpha = { path = "crates/alpha", version = "5.0.0-rc.2""#),
"root [workspace.dependencies] alpha must be bumped to rc.2:\n{root_cargo}"
);
assert!(
root_cargo.contains(r#"serde = "1.0""#),
"root serde must be untouched:\n{root_cargo}"
);
let alpha_cargo = std::fs::read_to_string(root.join("crates/alpha/Cargo.toml")).expect("read alpha");
assert!(
alpha_cargo.contains("version = \"5.0.0-rc.2\""),
"alpha [package] must be bumped:\n{alpha_cargo}"
);
let beta_cargo = std::fs::read_to_string(root.join("crates/beta/Cargo.toml")).expect("read beta");
let alpha_version_lines: Vec<&str> = beta_cargo
.lines()
.filter(|l| l.contains("alpha") && l.contains("version"))
.collect();
assert!(
!alpha_version_lines.is_empty(),
"expected alpha dep lines with version= in beta:\n{beta_cargo}"
);
for line in &alpha_version_lines {
assert!(
line.contains("5.0.0-rc.2"),
"alpha pin not bumped to rc.2 in beta:\n {line}\nfull:\n{beta_cargo}"
);
}
assert!(
!beta_cargo.contains("5.0.0-rc.1"),
"old rc.1 must be gone from beta:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"serde = "1.0""#),
"serde must not be touched:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"tempfile = "3""#),
"tempfile must not be touched:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"libc = "0.2""#),
"libc must not be touched:\n{beta_cargo}"
);
}
#[test]
fn run_optional_logs_but_does_not_fail_on_missing_binary() {
super::super::helpers::run_optional("nonexistent_binary_12345", &["arg1", "arg2"]);
}
#[test]
fn run_optional_succeeds_for_simple_command() {
super::super::helpers::run_optional("echo", &["test"]);
}
const GRADLE_BUILD_SAMPLE: &str = r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
`java-library`
kotlin("jvm") version "2.3.21"
`maven-publish`
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}
group = "dev.example"
version = "0.15.6-rc.2"
repositories {
mavenCentral()
}
dependencies {
api("net.java.dev.jna:jna:5.14.0")
}
ktlint {
version.set("1.0.1")
}
"#;
#[test]
fn replace_gradle_project_version_bumps_only_project_version() {
let out = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").expect("project version bumped");
assert!(
out.contains("version = \"0.15.6-rc.3\""),
"project version must be bumped:\n{out}"
);
assert!(
out.contains(r#"kotlin("jvm") version "2.3.21""#),
"kotlin plugin version must not change:\n{out}"
);
assert!(
out.contains(r#"id("org.jlleitschuh.gradle.ktlint") version "12.1.0""#),
"ktlint plugin version must not change:\n{out}"
);
assert!(
out.contains(r#"version.set("1.0.1")"#),
"ktlint extension version must not change:\n{out}"
);
assert!(
out.contains(r#"api("net.java.dev.jna:jna:5.14.0")"#),
"jna coordinate must not change:\n{out}"
);
assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
}
#[test]
fn replace_gradle_project_version_is_idempotent() {
let first = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").unwrap();
assert!(
replace_gradle_project_version(&first, "0.15.6-rc.3").is_none(),
"second call with same version must return None"
);
}
#[test]
fn replace_gradle_project_version_no_project_version_returns_none() {
let content = "plugins {\n kotlin(\"jvm\") version \"2.3.21\"\n}\n";
assert!(replace_gradle_project_version(content, "1.0.0").is_none());
}
const NIF_CARGO_LOCK_SAMPLE: &str = r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "example_core"
version = "0.15.6-rc.2"
dependencies = [
"serde",
]
[[package]]
name = "example_nif"
version = "0.15.6-rc.2"
dependencies = [
"example_core",
"rustler",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
"#;
#[test]
fn sync_cargo_lock_path_versions_bumps_only_sourceless_entries() {
let out = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").expect("lock updated");
assert!(
out.contains("version = 4"),
"lock format version line must be preserved:\n{out}"
);
assert_eq!(
out.matches("version = \"0.15.6-rc.3\"").count(),
2,
"both local crates must be bumped:\n{out}"
);
assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
assert!(
out.contains("version = \"1.0.219\""),
"registry dep version must not change:\n{out}"
);
assert!(
out.contains("source = \"registry+https://github.com/rust-lang/crates.io-index\""),
"registry source line must be preserved:\n{out}"
);
}
#[test]
fn sync_cargo_lock_path_versions_is_idempotent() {
let first = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").unwrap();
assert!(
sync_cargo_lock_path_versions(&first, "0.15.6-rc.3").is_none(),
"second call with same version must return None"
);
}
#[test]
fn sync_cargo_lock_path_versions_preserves_no_trailing_newline() {
let no_newline = NIF_CARGO_LOCK_SAMPLE.trim_end_matches('\n');
let out = sync_cargo_lock_path_versions(no_newline, "0.15.6-rc.3").unwrap();
assert!(
!out.ends_with('\n'),
"absence of trailing newline must be preserved:\n{out:?}"
);
}
#[test]
fn sync_cargo_lock_path_versions_all_registry_returns_none() {
let content = "version = 4\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.219\"\nsource = \"registry+x\"\n";
assert!(sync_cargo_lock_path_versions(content, "9.9.9").is_none());
}
#[test]
fn sync_docs_version_badges_updates_api_files_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
std::fs::write(
dir.join("api-rust.md"),
"## Rust API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n\nbody\n",
)
.expect("write api-rust.md");
std::fs::write(
dir.join("api-python.md"),
"## Python API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write api-python.md");
std::fs::write(
dir.join("configuration.md"),
"## Configuration <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write configuration.md");
let updated = sync_docs_version_badges(dir, "0.15.6-rc.3");
assert_eq!(
updated.len(),
2,
"only the two api-*.md files must be updated: {updated:?}"
);
let rust = std::fs::read_to_string(dir.join("api-rust.md")).unwrap();
assert!(
rust.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
"rust badge must be bumped:\n{rust}"
);
let config = std::fs::read_to_string(dir.join("configuration.md")).unwrap();
assert!(
config.contains("v0.15.6-rc.2"),
"non-api doc must not be touched:\n{config}"
);
}
#[test]
fn sync_docs_version_badges_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
std::fs::write(
dir.join("api-go.md"),
"## Go API Reference <span class=\"version-badge\">v1.0.0</span>\n",
)
.expect("write api-go.md");
let _ = sync_docs_version_badges(dir, "1.0.0");
let second = sync_docs_version_badges(dir, "1.0.0");
assert!(second.is_empty(), "second call with same version must be a no-op");
}
#[test]
fn sync_versions_bumps_kotlin_gradle_nif_lock_and_docs_badges() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"0.15.6-rc.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/kotlin")).expect("mkdir packages/kotlin");
std::fs::write(root.join("packages/kotlin/build.gradle.kts"), GRADLE_BUILD_SAMPLE)
.expect("write build.gradle.kts");
std::fs::create_dir_all(root.join("packages/elixir/native/example_nif")).expect("mkdir native");
std::fs::write(
root.join("packages/elixir/native/example_nif/Cargo.lock"),
NIF_CARGO_LOCK_SAMPLE,
)
.expect("write Cargo.lock");
std::fs::create_dir_all(root.join("docs/reference")).expect("mkdir docs/reference");
std::fs::write(
root.join("docs/reference/api-elixir.md"),
"## Elixir API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write api-elixir.md");
let alef_toml = format!(
"[workspace]\nlanguages = [\"kotlin\", \"elixir\"]\n[[crates]]\nname = \"example\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let gradle = std::fs::read_to_string(root.join("packages/kotlin/build.gradle.kts")).expect("read gradle");
assert!(
gradle.contains("version = \"0.15.6-rc.3\""),
"kotlin gradle project version must be bumped:\n{gradle}"
);
assert!(
gradle.contains(r#"kotlin("jvm") version "2.3.21""#),
"kotlin plugin version must not change:\n{gradle}"
);
let lock =
std::fs::read_to_string(root.join("packages/elixir/native/example_nif/Cargo.lock")).expect("read lock");
assert_eq!(
lock.matches("version = \"0.15.6-rc.3\"").count(),
2,
"both local NIF lock entries must be bumped:\n{lock}"
);
assert!(
lock.contains("version = \"1.0.219\""),
"registry dep in lock must not change:\n{lock}"
);
let badge = std::fs::read_to_string(root.join("docs/reference/api-elixir.md")).expect("read api-elixir.md");
assert!(
badge.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
"docs version badge must be bumped:\n{badge}"
);
}
#[test]
fn update_zig_package_hash_rc_prerelease() {
let existing = "sample_pkg-1.4.0-rc.50-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9";
let result = update_zig_package_hash(existing, "1.4.0-rc.50", "1.4.0-rc.53");
assert_eq!(
result,
Some("sample_pkg-1.4.0-rc.53-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9".to_string()),
"rc prerelease version must be substituted in hash"
);
}
#[test]
fn update_zig_package_hash_release_version() {
let existing = "sample_pkg-1.4.0-rc.53-AbCd_XyZ123456789";
let result = update_zig_package_hash(existing, "1.4.0-rc.53", "1.4.0");
assert_eq!(
result,
Some("sample_pkg-1.4.0-AbCd_XyZ123456789".to_string()),
"release version must substitute prerelease"
);
}
#[test]
fn update_zig_package_hash_same_version_is_none() {
let existing = "mylib-0.1.0-rc.1-SomeBase64Hash";
let result = update_zig_package_hash(existing, "0.1.0-rc.1", "0.1.0-rc.1");
assert_eq!(result, None, "same version must return None");
}
#[test]
fn update_zig_package_hash_malformed_hash_is_none() {
let existing = "notenoughparts";
let result = update_zig_package_hash(existing, "0.1.0", "0.2.0");
assert_eq!(result, None, "malformed hash must return None");
}
#[test]
fn render_registry_version_python_pep440_rc_prerelease() {
let result = render_registry_version("python", "0.3.0-rc.28", ">=0.1.0rc9");
assert_eq!(result, Some(">=0.3.0rc28".to_string()));
}
#[test]
fn render_registry_version_python_pep440_release() {
let result = render_registry_version("python", "1.0.0", ">=0.9.0");
assert_eq!(result, Some(">=1.0.0".to_string()));
}
#[test]
fn render_registry_version_python_already_current_is_none() {
let result = render_registry_version("python", "0.3.0-rc.28", ">=0.3.0rc28");
assert_eq!(result, None);
}
#[test]
fn render_registry_version_node_semver_rc() {
let result = render_registry_version("node", "0.3.0-rc.28", "^0.1.0-rc.9");
assert_eq!(result, Some("^0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_elixir_hex_constraint() {
let result = render_registry_version("elixir", "0.3.0-rc.28", "~> 0.1.0-rc.9");
assert_eq!(result, Some("~> 0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_ruby_rubygems_prerelease() {
let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.1.0.pre.rc.9");
assert_eq!(result, Some(">= 0.3.0.pre.rc.28".to_string()));
}
#[test]
fn render_registry_version_ruby_already_current_is_none() {
let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.3.0.pre.rc.28");
assert_eq!(result, None);
}
#[test]
fn render_registry_version_go_module_version() {
let result = render_registry_version("go", "0.3.0-rc.28", "v0.1.0-rc.9");
assert_eq!(result, Some("v0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_rust_bare_semver() {
let result = render_registry_version("rust", "0.3.0-rc.28", "0.1.0-rc.9");
assert_eq!(result, Some("0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_php_composer_range() {
let result = render_registry_version("php", "0.3.0-rc.28", ">=0.1.0-rc.9");
assert_eq!(result, Some(">=0.3.0-rc.28".to_string()));
}
#[test]
fn sync_registry_package_versions_rewrites_all_language_entries() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nalef_version = \"0.19.0\"\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry]\noutput = \"test_apps\"\n\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \">=0.1.0rc9\"\n\n",
"[crates.e2e.registry.packages.node]\n",
"name = \"@myorg/mylib\"\n",
"version = \"^0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.elixir]\n",
"name = \"mylib\"\n",
"version = \"~> 0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.ruby]\n",
"name = \"mylib\"\n",
"version = \">= 0.1.0.pre.rc.9\"\n",
),
)
.expect("write alef.toml");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(changed, "must report at least one change");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated.contains("version = \">=0.3.0rc28\""),
"python version must be PEP 440 formatted: {updated}"
);
assert!(
updated.contains("version = \"^0.3.0-rc.28\""),
"node version must preserve ^ prefix: {updated}"
);
assert!(
updated.contains("version = \"~> 0.3.0-rc.28\""),
"elixir version must preserve ~> prefix: {updated}"
);
assert!(
updated.contains("version = \">= 0.3.0.pre.rc.28\""),
"ruby version must be RubyGems formatted: {updated}"
);
assert!(updated.contains("name = \"mylib\""), "package names must be preserved");
assert!(
updated.contains("name = \"@myorg/mylib\""),
"node name must be preserved"
);
}
#[test]
fn sync_registry_package_versions_skips_entries_without_version_field() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
let original = concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.go]\n",
"module = \"github.com/myorg/mylib\"\n",
);
std::fs::write(&alef_toml_path, original).expect("write");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(!changed, "no version field → no change");
let content = std::fs::read_to_string(&alef_toml_path).expect("read");
assert!(!content.contains("version"), "version must not be inserted: {content}");
}
#[test]
fn sync_registry_package_versions_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.python]\n",
"version = \">=0.3.0rc28\"\n",
),
)
.expect("write");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(!changed, "already-current version must be a no-op");
}
#[test]
fn sync_registry_package_versions_preserves_toml_comments_and_order() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
let original = concat!(
"# Top-level comment\n",
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"# Registry section comment\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \">=0.1.0rc9\"\n",
);
std::fs::write(&alef_toml_path, original).expect("write");
sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read");
assert!(
updated.contains("# Top-level comment"),
"top-level comment must be preserved: {updated}"
);
assert!(
updated.contains("# Registry section comment"),
"registry section comment must be preserved: {updated}"
);
let name_pos = updated.find("name = ").expect("name field present");
let ver_pos = updated.find("version = ").expect("version field present");
assert!(name_pos < ver_pos, "name must appear before version in output");
}
#[test]
fn sync_registry_package_versions_handles_go_and_bare_semver_langs() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.go]\n",
"module = \"github.com/myorg/mylib\"\n",
"version = \"v0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.rust]\n",
"name = \"mylib\"\n",
"version = \"0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.php]\n",
"name = \"myorg/mylib\"\n",
"version = \">=0.1.0-rc.9\"\n",
),
)
.expect("write alef.toml");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(changed, "must report at least one change");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated.contains("version = \"v0.3.0-rc.28\""),
"go version must have v prefix: {updated}"
);
assert!(
updated.contains("version = \"0.3.0-rc.28\""),
"rust bare semver must be updated: {updated}"
);
assert!(
updated.contains("version = \">=0.3.0-rc.28\""),
"php composer constraint must be updated: {updated}"
);
}
const JAVA_E2E_POM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>dev.sample_crate.sample_crawler</groupId>
<artifactId>sample_crawler-e2e-java</artifactId>
<version>0.1.0</version>
<dependencies>
<dependency>
<groupId>dev.sample_crate.sample_crawler</groupId>
<artifactId>sample_crawler</artifactId>
<version>0.3.0-rc.27</version>
<scope>system</scope>
<systemPath>${project.basedir}/../../packages/java/target/sample_crawler-0.3.0-rc.27.jar</systemPath>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
"#;
#[test]
fn sync_e2e_java_pom_updates_dependency_version_and_system_path() {
let result = sync_e2e_java_pom(JAVA_E2E_POM, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("<version>0.3.0-rc.28</version>"),
"dependency version must be updated:\n{new}"
);
assert!(
new.contains("sample_crawler-0.3.0-rc.28.jar"),
"systemPath must be updated:\n{new}"
);
assert!(
new.contains("<version>0.1.0</version>"),
"project version must be unchanged:\n{new}"
);
assert!(
new.contains("<version>${junit.version}</version>"),
"junit version placeholder must be unchanged:\n{new}"
);
assert!(!new.contains("0.3.0-rc.27"), "old version must be removed:\n{new}");
}
#[test]
fn sync_e2e_java_pom_is_idempotent() {
let first = sync_e2e_java_pom(JAVA_E2E_POM, "0.3.0-rc.28").unwrap();
let second = sync_e2e_java_pom(&first, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
#[test]
fn sync_e2e_java_pom_no_system_scope_returns_none() {
let content = "<?xml version=\"1.0\"?>\n<project><version>0.1.0</version></project>\n";
assert!(
sync_e2e_java_pom(content, "1.0.0").is_none(),
"no system-scope dep means nothing to update"
);
}
const GO_MOD_E2E: &str = "\
module e2e_go
go 1.26
require (
\tgithub.com/sample_crate-dev/sample_crawler/packages/go v0.3.0-rc.27
\tgithub.com/stretchr/testify v1.11.1
)
replace github.com/sample_crate-dev/sample_crawler/packages/go => ../../packages/go
";
#[test]
fn sync_e2e_go_mod_updates_library_require_line() {
let fragment = "github.com/sample_crate-dev/sample_crawler/packages/go";
let result = sync_e2e_go_mod(GO_MOD_E2E, fragment, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("github.com/sample_crate-dev/sample_crawler/packages/go v0.3.0-rc.28"),
"library require line must be updated:\n{new}"
);
assert!(
new.contains("github.com/stretchr/testify v1.11.1"),
"testify version must be unchanged:\n{new}"
);
assert!(!new.contains("v0.3.0-rc.27"), "old version must be gone:\n{new}");
}
#[test]
fn sync_e2e_go_mod_is_idempotent() {
let fragment = "github.com/sample_crate-dev/sample_crawler/packages/go";
let first = sync_e2e_go_mod(GO_MOD_E2E, fragment, "0.3.0-rc.28").unwrap();
let second = sync_e2e_go_mod(&first, fragment, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
const DART_PUBSPEC_LOCK: &str = "\
# Generated by pub
packages:
async:
dependency: transitive
description:
name: async
sha256: abc123
url: \"https://pub.dev\"
source: hosted
version: \"1.19.1\"
sample_crawler:
dependency: \"direct main\"
description:
path: \"../../packages/dart\"
relative: true
source: path
version: \"0.3.0-rc.23\"
logging:
dependency: transitive
description:
name: logging
sha256: def456
url: \"https://pub.dev\"
source: hosted
version: \"1.2.0\"
";
#[test]
fn sync_e2e_dart_pubspec_lock_updates_path_source_version() {
let result = sync_e2e_dart_pubspec_lock(DART_PUBSPEC_LOCK, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("version: \"0.3.0-rc.28\""),
"path-source version must be updated:\n{new}"
);
assert!(
new.contains("version: \"1.19.1\""),
"hosted async version must be unchanged:\n{new}"
);
assert!(
new.contains("version: \"1.2.0\""),
"hosted logging version must be unchanged:\n{new}"
);
assert!(!new.contains("0.3.0-rc.23"), "old version must be gone:\n{new}");
}
#[test]
fn sync_e2e_dart_pubspec_lock_is_idempotent() {
let first = sync_e2e_dart_pubspec_lock(DART_PUBSPEC_LOCK, "0.3.0-rc.28").unwrap();
let second = sync_e2e_dart_pubspec_lock(&first, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
#[test]
fn sync_e2e_dart_pubspec_lock_no_path_source_returns_none() {
let content = "packages:\n async:\n dependency: transitive\n description:\n name: async\n url: \"https://pub.dev\"\n source: hosted\n version: \"1.19.1\"\n";
assert!(
sync_e2e_dart_pubspec_lock(content, "0.3.0-rc.28").is_none(),
"no path-source means nothing to update"
);
}
#[test]
fn sync_versions_regenerates_test_apps_pins() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.2.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("fixtures")).expect("mkdir fixtures");
let alef_toml = format!(
concat!(
"[workspace]\n",
"languages = [\"python\"]\n\n",
"[[crates]]\n",
"name = \"mylib\"\n",
"sources = []\n",
"version_from = \"{cargo_toml}\"\n\n",
"[crates.e2e]\n",
"fixtures = \"fixtures\"\n",
"languages = [\"python\"]\n\n",
"[crates.e2e.call]\n",
"module = \"mylib\"\n",
"function = \"parse\"\n\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \"0.0.0\"\n",
),
cargo_toml = root.join("Cargo.toml").display().to_string().replace('\\', "/"),
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let updated_toml = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated_toml.contains("version = \"1.2.3\""),
"alef.toml registry package version must be updated to 1.2.3:\n{updated_toml}"
);
assert!(
!updated_toml.contains("version = \"0.0.0\""),
"stale 0.0.0 must be gone from alef.toml:\n{updated_toml}"
);
let pyproject_path = root.join("test_apps/python/pyproject.toml");
assert!(
pyproject_path.exists(),
"test_apps/python/pyproject.toml must be generated by auto-regen"
);
let pyproject = std::fs::read_to_string(&pyproject_path).expect("read pyproject.toml");
assert!(
pyproject.contains("mylib==1.2.3"),
"test_apps/python/pyproject.toml must pin the new registry version 1.2.3:\n{pyproject}"
);
assert!(
!pyproject.contains("mylib==0.0.0"),
"stale registry pin mylib==0.0.0 must be gone from test_apps/python/pyproject.toml:\n{pyproject}"
);
}
#[test]
fn sync_versions_updates_go_module_version_in_download_ffi() {
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.14\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let download_ffi_dir = root.join("packages/go/cmd/download_ffi");
std::fs::create_dir_all(&download_ffi_dir).expect("mkdir download_ffi");
let stale_main_go = concat!(
"// Tool to download platform-specific FFI libraries from GitHub releases.\n",
"package main\n\nconst (\n",
"\tmoduleVersion = \"1.9.0-rc.13\"\n",
"\trepoURL = \"https://github.com/example/mylib\"\n",
")\n",
);
std::fs::write(download_ffi_dir.join("main.go"), stale_main_go).expect("write main.go");
let alef_toml = format!(
concat!(
"[workspace]\nlanguages = [\"go\"]\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n",
"version_from = \"{cargo_toml}\"\n",
),
cargo_toml = root.join("Cargo.toml").display().to_string().replace('\\', "/"),
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: crate::core::config::NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let updated_main = std::fs::read_to_string(download_ffi_dir.join("main.go")).expect("read main.go");
assert!(
updated_main.contains("moduleVersion = \"1.9.0-rc.14\""),
"moduleVersion must be updated to 1.9.0-rc.14:\n{updated_main}"
);
assert!(
!updated_main.contains("1.9.0-rc.13"),
"stale rc.13 moduleVersion must be gone from main.go:\n{updated_main}"
);
assert!(
updated_main.contains("repoURL"),
"other constants must be preserved:\n{updated_main}"
);
}
#[test]
fn sync_versions_regenerates_scaffold_version_fields() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.2.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/r")).expect("mkdir packages/r");
let stale_description = concat!(
"Package: mylib\nTitle: My Library\nVersion: 0.0.0\nDescription: A library.\n",
"License: MIT\nEncoding: UTF-8\nRoxygenNote: 7.3.1\n",
);
std::fs::write(root.join("packages/r/DESCRIPTION"), stale_description).expect("write DESCRIPTION");
let alef_toml = format!(
concat!(
"[workspace]\n",
"languages = [\"r\"]\n\n",
"[[crates]]\n",
"name = \"mylib\"\n",
"sources = []\n",
"version_from = \"{cargo_toml}\"\n",
),
cargo_toml = root.join("Cargo.toml").display().to_string().replace('\\', "/"),
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let description_path = root.join("packages/r/DESCRIPTION");
assert!(
description_path.exists(),
"packages/r/DESCRIPTION must exist after scaffold regen"
);
let description = std::fs::read_to_string(&description_path).expect("read DESCRIPTION");
assert!(
description.contains("Version: 1.2"),
"DESCRIPTION must contain the new version 1.2.x after scaffold regen:\n{description}"
);
assert!(
!description.contains("Version: 0.0.0"),
"stale Version: 0.0.0 must be gone from DESCRIPTION:\n{description}"
);
}
#[test]
fn sync_versions_bumps_kotlin_android_gradle_coordinates_version() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let gradle_content = concat!(
"plugins {\n",
" id(\"com.android.library\") version \"8.13.0\"\n",
" kotlin(\"android\") version \"2.3.21\"\n",
"}\n",
"\n",
"mavenPublishing {\n",
" coordinates(\n",
" groupId = \"dev.example\",\n",
" artifactId = \"mylib-android\",\n",
" version = \"1.9.0-rc.16\",\n",
" )\n",
"}\n",
);
std::fs::create_dir_all(root.join("packages/kotlin-android")).expect("mkdir");
std::fs::write(root.join("packages/kotlin-android/build.gradle.kts"), gradle_content)
.expect("write build.gradle.kts");
let alef_toml = format!(
"[workspace]\nlanguages = [\"kotlin_android\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let gradle = std::fs::read_to_string(root.join("packages/kotlin-android/build.gradle.kts"))
.expect("read build.gradle.kts");
assert!(
gradle.contains("version = \"1.9.0-rc.17\""),
"kotlin-android coordinates version must be bumped:\n{gradle}"
);
assert!(
gradle.contains(r#"kotlin("android") version "2.3.21""#),
"kotlin plugin version must not change:\n{gradle}"
);
assert!(
gradle.contains(r#"id("com.android.library") version "8.13.0""#),
"android plugin version must not change:\n{gradle}"
);
assert!(
!gradle.contains("1.9.0-rc.16"),
"stale rc.16 version must be gone:\n{gradle}"
);
}
#[test]
fn sync_versions_bumps_nested_python_init_version() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let py_module_dir = root.join("packages/python/mylib");
std::fs::create_dir_all(&py_module_dir).expect("mkdir");
std::fs::write(
py_module_dir.join("__init__.py"),
"\"\"\"mylib public API.\"\"\"\n\n__version__ = \"1.9.0-rc.16\"\n",
)
.expect("write __init__.py");
let alef_toml = format!(
"[workspace]\nlanguages = [\"python\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let content = std::fs::read_to_string(py_module_dir.join("__init__.py")).expect("read __init__.py");
assert!(
content.contains("__version__ = \"1.9.0-rc.17\""),
"nested __version__ must be bumped:\n{content}"
);
assert!(
!content.contains("1.9.0-rc.16"),
"stale rc.16 __version__ must be gone:\n{content}"
);
}
#[test]
fn sync_versions_bumps_swift_package_from_version() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let swift_pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"\n",
"let package = Package(\n",
" name: \"TestApp\",\n",
" dependencies: [\n",
" .package(url: \"https://example.com/alef-sample/mylib.git\", from: \"1.9.0-rc.16\"),\n",
" ],\n",
" targets: []\n",
")\n",
);
let swift_dir = root.join("test_apps/swift");
std::fs::create_dir_all(&swift_dir).expect("mkdir");
std::fs::write(swift_dir.join("Package.swift"), swift_pkg_content).expect("write Package.swift");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let swift_pkg = std::fs::read_to_string(swift_dir.join("Package.swift")).expect("read Package.swift");
assert!(
swift_pkg.contains("from: \"1.9.0-rc.17\""),
"swift from: version must be bumped:\n{swift_pkg}"
);
assert!(
!swift_pkg.contains("from: \"1.9.0-rc.16\""),
"stale rc.16 from: version must be gone:\n{swift_pkg}"
);
assert!(
swift_pkg.contains("https://example.com/alef-sample/mylib.git"),
"repo URL must be preserved:\n{swift_pkg}"
);
}
#[test]
fn sync_versions_root_package_swift_placeholder_survives_scaffold_regen() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let root_pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"let package = Package(name: \"MyLib\", targets: [\n",
" .binaryTarget(\n",
" name: \"RustBridge\",\n",
" url: \"https://example.com/alef-sample/mylib/releases/download/v__ALEF_SWIFT_VERSION__/MyLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), root_pkg_content).expect("write root Package.swift");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_pkg = std::fs::read_to_string(root.join("Package.swift")).expect("read root Package.swift");
assert!(
!root_pkg.contains("v__ALEF_SWIFT_VERSION__"),
"root Package.swift must not retain the version placeholder after sync_versions, got:\n{root_pkg}"
);
assert!(
root_pkg.contains("/releases/download/v1.9.0-rc.17/"),
"root Package.swift URL must point at substituted version v1.9.0-rc.17, got:\n{root_pkg}"
);
assert!(
root_pkg.contains("__ALEF_SWIFT_CHECKSUM__"),
"root Package.swift must retain the checksum placeholder when skip_swift_checksum=true, got:\n{root_pkg}"
);
}
#[test]
fn sync_versions_bumps_c_download_ffi_sh_version() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"1.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let sh_content = concat!(
"#!/usr/bin/env bash\n",
"set -euo pipefail\n",
"\n",
"REPO_URL=\"https://example.com/alef-sample/mylib\"\n",
"VERSION=\"1.9.0-rc.16\"\n",
"FFI_PKG_NAME=\"mylib-ffi\"\n",
);
let e2e_c_dir = root.join("e2e/c");
std::fs::create_dir_all(&e2e_c_dir).expect("mkdir e2e/c");
std::fs::write(e2e_c_dir.join("download_ffi.sh"), sh_content).expect("write e2e download_ffi.sh");
let test_apps_c_dir = root.join("test_apps/c");
std::fs::create_dir_all(&test_apps_c_dir).expect("mkdir test_apps/c");
std::fs::write(test_apps_c_dir.join("download_ffi.sh"), sh_content).expect("write test_apps download_ffi.sh");
let alef_toml = format!(
"[workspace]\nlanguages = [\"c\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve config");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("set_current_dir");
let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
for (label, dir) in [("e2e", &e2e_c_dir), ("test_apps", &test_apps_c_dir)] {
let content = std::fs::read_to_string(dir.join("download_ffi.sh"))
.unwrap_or_else(|_| panic!("read {label}/c/download_ffi.sh"));
assert!(
content.contains("VERSION=\"1.9.0-rc.17\""),
"{label}/c/download_ffi.sh VERSION must be bumped:\n{content}"
);
assert!(
!content.contains("VERSION=\"1.9.0-rc.16\""),
"{label}/c/download_ffi.sh stale rc.16 must be gone:\n{content}"
);
assert!(
content.contains("REPO_URL="),
"{label}/c/download_ffi.sh REPO_URL must be preserved:\n{content}"
);
}
}
#[test]
fn compute_sha256_hex_empty_input() {
let hex = compute_sha256_hex(b"");
assert_eq!(
hex, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"SHA-256 of empty input must match reference"
);
}
#[test]
fn compute_sha256_hex_abc() {
let hex = compute_sha256_hex(b"abc");
assert_eq!(
hex, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"SHA-256 of 'abc' must match reference"
);
}
#[test]
fn precompute_swift_checksum_substitutes_when_zip_present() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"2.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"let package = Package(name: \"TestLib\", targets: [\n",
" .binaryTarget(\n",
" name: \"RustBridge\",\n",
" url: \"https://example.com/testlib/releases/download/v2.0.0/TestLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), pkg_content).expect("write Package.swift");
let swift_crate_dir = root.join("crates/testlib-swift");
std::fs::create_dir_all(&swift_crate_dir).expect("mkdir swift crate");
std::fs::write(
swift_crate_dir.join("Cargo.toml"),
"[package]\nname = \"testlib-swift\"\nversion = \"2.0.0\"\n",
)
.expect("write swift Cargo.toml");
let bundle_dir = root.join("dist/swift-artifactbundle");
std::fs::create_dir_all(&bundle_dir).expect("mkdir bundle dir");
let zip_content = b"fake-artifactbundle-zip-content-for-testing";
std::fs::write(bundle_dir.join("TestLib-rs.artifactbundle.zip"), zip_content).expect("write fake zip");
let expected_checksum = compute_sha256_hex(zip_content);
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"testlib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("chdir");
let result = precompute_swift_checksum(&resolved_cfg);
let _ = std::env::set_current_dir(&original_cwd);
let checksum = result
.expect("precompute_swift_checksum must succeed")
.expect("must return Some(checksum) when zip is present");
assert_eq!(
checksum, expected_checksum,
"returned checksum must equal in-process SHA-256 of the fake zip"
);
let pkg_result = std::fs::read_to_string(root.join("Package.swift")).expect("read");
assert!(
!pkg_result.contains("__ALEF_SWIFT_CHECKSUM__"),
"Package.swift must not retain the placeholder after precompute, got:\n{pkg_result}"
);
assert!(
pkg_result.contains(&expected_checksum),
"Package.swift must contain the computed checksum, got:\n{pkg_result}"
);
let sidecar =
std::fs::read_to_string(root.join("target/alef-swift-checksum.txt")).expect("sidecar file must exist");
assert_eq!(
sidecar.trim(),
expected_checksum,
"sidecar must contain the computed checksum"
);
}
#[test]
fn precompute_swift_checksum_skips_when_no_zip_and_build_fails() {
use crate::core::config::NewAlefConfig;
let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().expect("cwd");
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace.package]\nversion = \"2.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"let package = Package(name: \"TestLib\", targets: [\n",
" .binaryTarget(name: \"RustBridge\",\n",
" url: \"https://example.com/v2.0.0/TestLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), pkg_content).expect("write Package.swift");
let swift_crate_dir = root.join("crates/testlib-swift");
std::fs::create_dir_all(&swift_crate_dir).expect("mkdir swift crate");
std::fs::write(
swift_crate_dir.join("Cargo.toml"),
"[package]\nname = \"testlib-swift\"\nversion = \"2.0.0\"\n[lib]\nname = \"nonexistent_guaranteed_fail\"\n",
)
.expect("write swift Cargo.toml");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"testlib\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
let alef_toml_path = root.join("alef.toml");
std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("chdir");
let result = precompute_swift_checksum(&resolved_cfg);
let _ = std::env::set_current_dir(&original_cwd);
assert!(
result.is_ok(),
"precompute_swift_checksum must not propagate build errors, got: {:?}",
result
);
assert!(result.unwrap().is_none(), "must return None when build fails");
let pkg_result = std::fs::read_to_string(root.join("Package.swift")).expect("read");
assert!(
pkg_result.contains("__ALEF_SWIFT_CHECKSUM__"),
"Package.swift must retain placeholder when build fails, got:\n{pkg_result}"
);
}
}