eng/scripts/update-pathversions.rs (119 lines of code) (raw):

#!/usr/bin/env -S cargo +nightly -Zscript --- [package] edition = "2021" description = "In all Cargo.toml files in the repo, for all dependencies that have both path and version properties, update the version property to the version in the package." [dependencies] regex = "1.5" toml_edit = "0.22" --- use regex::Regex; use std::io::Write; use std::{env, error::Error, fs, path::PathBuf}; use toml_edit::{value, DocumentMut, Item, Table}; fn main() -> Result<(), Box<dyn std::error::Error>> { let add_mode = env::args().nth(1) .map(|arg| match arg.as_str() { "add" => true, "update" => false, _ => panic!("Invalid mode. Use 'add' or 'update'.") }) .expect("requires 'add' or 'update' mode argument"); let script_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); let repo_root = script_root.join("../../..").canonicalize()?; // find all Cargo.toml files in the repo_root directory let exclude_dirs = vec![ repo_root.join("eng"), repo_root.join("target") ]; let toml_files = load_cargo_toml_files(&repo_root, &exclude_dirs)?; let package_versions = get_package_versions(&toml_files); for mut toml_file in toml_files { let should_add = add_mode && !toml_file.is_publish_disabled; update_package_versions(toml_file.document.as_table_mut(), &package_versions, should_add); // if the toml file has a workspace table, update the workspace table if let Some(workspace) = toml_file.document.get_mut("workspace") { if let Some(table) = workspace.as_table_mut() { update_package_versions(table, &package_versions, should_add); } } // write the updated document back to the file let mut file = fs::File::create(toml_file.path)?; fs::File::write_all(&mut file, toml_file.document.to_string().as_bytes())?; } Ok(()) } fn load_cargo_toml_files(repo_root: &PathBuf, exclude_dirs: &Vec<PathBuf>) -> Result<Vec<TomlInfo>, Box<dyn Error>> { let mut toml_paths = Vec::new(); find_cargo_toml_files(repo_root, exclude_dirs, &mut toml_paths)?; let mut toml_files = Vec::new(); for path in toml_paths { let content = fs::read_to_string(&path)?; let doc = content.parse::<DocumentMut>()?; let package_table = doc.get("package").and_then(Item::as_table); let publish_property = package_table.and_then(|table| table.get("publish")).and_then(Item::as_bool); let package_name = package_table.and_then(|table| table.get("name")).and_then(Item::as_str); let package_version = package_table.and_then(|table| table.get("version")).and_then(Item::as_str); toml_files.push(TomlInfo { path, package_name: package_name.map(|s| s.to_string()), package_version: package_version.map(|s| s.to_string()), is_publish_disabled: publish_property == Some(false), document: doc }); } Ok(toml_files) } fn find_cargo_toml_files(dir: &PathBuf, exclude_dirs: &Vec<PathBuf>, toml_paths: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() && !exclude_dirs.contains(&path) { find_cargo_toml_files(&path, exclude_dirs, toml_paths)?; } else if path.is_file() && path.file_name() == Some("Cargo.toml".as_ref()) { toml_paths.push(path); } } Ok(()) } fn get_package_versions(toml_files: &Vec<TomlInfo>) -> Vec<(String, String, bool)> { let mut package_versions = Vec::new(); for toml_file in toml_files { if toml_file.package_name.is_none() || toml_file.package_version.is_none() { continue; } package_versions.push((toml_file.package_name.clone().unwrap(), toml_file.package_version.clone().unwrap(), toml_file.is_publish_disabled)); } package_versions } fn update_package_versions(toml: &mut Table, package_versions: &Vec<(String, String, bool)>, add: bool) { // for each dependency table, for each package in package_versions // if the package is in the dependency table // if the dependency has both path and version properties, update the version property // if the dependency has has path, but not version, add the version property only if // 1. the table name is not "dev-dependencies" // 2. the package is not publish disabled // 3. the add flag is true let dependency_tables = get_dependency_tables(toml); for (table_name, table) in dependency_tables { for (package, version, is_publish_disabled) in package_versions { if let Some(dependency) = table.get_mut(package) { // azure_idenentity will only be a transitive dev-dependency let should_add = add && table_name != "dev-dependencies" && !is_publish_disabled && package != "azure_identity"; let has_path_property = dependency.get("path").is_some(); let has_version_property = dependency.get("version").is_some(); if has_path_property && (has_version_property || should_add) { dependency["version"] = value(version); } } } } } fn get_dependency_tables(toml: &mut Table) -> Vec<(String, &mut Table)> { let re = Regex::new(r"[.-]?dependencies$").unwrap(); let mut tables = Vec::new(); for (key, value) in toml.iter_mut() { if let Some(table) = value.as_table_mut() { if re.is_match(&key) { tables.push((key.to_string(), table)); } } } tables } struct TomlInfo { path: PathBuf, package_name: Option<String>, package_version: Option<String>, is_publish_disabled: bool, document: DocumentMut, }