Files
uv/scripts/bump-workspace-crate-versions.py
T
2026-02-05 20:28:44 +00:00

115 lines
4.4 KiB
Python

# Naively increment the patch version of each crate in the workspace.
#
# This excludes crates which are versioned as binaries.
#
# After incrementing the version in each member `Cargo.toml`, it updates the version pins in the
# root `Cargo.toml` to match.
# /// script
# requires-python = ">=3.13"
# dependencies = []
# ///
import json
import pathlib
import subprocess
import tomllib
NO_BUMP_CRATES = {"uv", "uv-build", "uv-version"}
def main() -> None:
# Pre-sync NO_BUMP_CRATES versions before running cargo metadata.
#
# Rooster updates crate versions but isn't workspace-aware, so it doesn't update
# the workspace dependency pins in the root Cargo.toml. This can cause cargo metadata
# to fail with version mismatches (e.g., when going from 0.9.x to 0.10.0).
# We fix the pins first by reading the crate versions directly.
script_dir = pathlib.Path(__file__).parent
workspace_manifest = script_dir.parent / "Cargo.toml"
workspace_manifest_contents = workspace_manifest.read_text()
parsed_workspace_manifest = tomllib.loads(workspace_manifest_contents)
for crate_name in NO_BUMP_CRATES:
crate_manifest = script_dir.parent / "crates" / crate_name / "Cargo.toml"
if not crate_manifest.exists():
continue
crate_version = tomllib.loads(crate_manifest.read_text())["package"]["version"]
manifest_dependency = parsed_workspace_manifest["workspace"][
"dependencies"
].get(crate_name)
if manifest_dependency is None:
continue
manifest_version = manifest_dependency["version"]
if manifest_version != crate_version:
workspace_manifest_contents = workspace_manifest_contents.replace(
f'{crate_name} = {{ version = "{manifest_version}"',
f'{crate_name} = {{ version = "{crate_version}"',
)
workspace_manifest.write_text(workspace_manifest_contents)
# Now cargo metadata will succeed
result = subprocess.run(
["cargo", "metadata", "--format-version", "1"],
capture_output=True,
text=True,
check=True,
)
content = json.loads(result.stdout)
packages = {package["id"]: package for package in content["packages"]}
workspace_manifest_contents = workspace_manifest.read_text()
parsed_workspace_manifest = tomllib.loads(workspace_manifest_contents)
version_changes = {}
for workspace_member in content["workspace_members"]:
manifest = pathlib.Path(packages[workspace_member]["manifest_path"])
name = packages[workspace_member]["name"]
# For the members we're not bumping, we'll still make sure that the version pinned in the
# workspace manifest matches the version of the crate. This is done because Rooster isn't
# Cargo workspace aware and won't otherwise bump these when updating the member `Cargo.toml`
# files. We could make Rooster smarter instead of this.
if name in NO_BUMP_CRATES:
manifest_dependency = parsed_workspace_manifest["workspace"][
"dependencies"
].get(name)
if manifest_dependency is None:
continue
manifest_version = manifest_dependency["version"]
metadata_version = packages[workspace_member]["version"]
if manifest_version != metadata_version:
version_changes[name] = (manifest_version, metadata_version)
continue
# For other members, bump the patch version
version = packages[workspace_member]["version"]
version_parts = [int(part) for part in version.split(".")]
new_version = f"{version_parts[0]}.{version_parts[1]}.{version_parts[2] + 1}"
contents = manifest.read_text()
contents = contents.replace(
'version = "' + version + '"',
'version = "' + new_version + '"',
1,
)
version_changes[name] = (version, new_version)
manifest.write_text(contents)
# Update all the pins in the workspace root
for name, (old_version, new_version) in version_changes.items():
workspace_manifest_contents = workspace_manifest_contents.replace(
f'{name} = {{ version = "{old_version}"',
f'{name} = {{ version = "{new_version}"',
)
workspace_manifest.write_text(workspace_manifest_contents)
if __name__ == "__main__":
main()