#!/usr/bin/env vpython3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
This script runs `gnrt update`, `gnrt vendor`, and `gnrt gen`, and then uploads
zero, one, or more resulting CLs to Gerrit. For more details please see
`tools/crates/create_update_cl.md`."""
import argparse
import datetime
import os
import re
import shlex
import subprocess
import sys
import textwrap
import toml
from dataclasses import dataclass
from typing import List, Set, Dict
# Throughout the script, the following naming conventions are used (illustrated
# with a crate named `syn` and published as version `2.0.50`):
#
# * `crate_name` : "syn" string
# * `crate_version`: "2.0.50" string
# * `crate_id` : "[email protected]" string (syntax used by `cargo`)
# * `crate_epoch` : "v2" string (syntax used in dir names under
# //third_party/rust/<crate name>/<crate epoch>)
#
# Note that `crate_name` may not be unique (e.g. if there is both `[email protected]`
# and `[email protected]`). Also note that f`{crate_name}@{crate_epoch}` doesn't
# change during a minor version update (such as the one that this script
# produces in `auto` and `single` modes).
THIS_DIR = os.path.dirname(__file__)
CHROMIUM_DIR = os.path.normpath(os.path.join(THIS_DIR, '..', '..'))
THIRD_PARTY_RUST = os.path.join(CHROMIUM_DIR, "third_party", "rust")
CRATES_DIR = os.path.join(THIRD_PARTY_RUST, "chromium_crates_io")
VENDOR_DIR = os.path.join(CRATES_DIR, "vendor")
CARGO_LOCK = os.path.join(CRATES_DIR, "Cargo.lock")
VET_CONFIG = os.path.join(CRATES_DIR, "supply-chain", "config.toml")
INCLUSIVE_LANG_SCRIPT = os.path.join(
CHROMIUM_DIR, "infra", "update_inclusive_language_presubmit_exempt_dirs.sh")
INCLUSIVE_LANG_CONFIG = os.path.join(
CHROMIUM_DIR, "infra", "inclusive_language_presubmit_exempt_dirs.txt")
RUN_GNRT = os.path.join(THIS_DIR, "run_gnrt.py")
UPDATE_RUST_SCRIPT = os.path.join(CHROMIUM_DIR, "tools", "rust",
"update_rust.py")
g_is_verbose = False
timestamp = datetime.datetime.now()
BRANCH_BASENAME = "rust-crates-update"
BRANCH_BASENAME += f"--{timestamp.year}{timestamp.month:02}{timestamp.day:02}"
BRANCH_BASENAME += f"-{timestamp.hour:02}{timestamp.minute:02}"
def RunCommandAndCheckForErrors(args, check_stdout: bool):
"""Runs a command and returns its output."""
args = list(args)
assert args
if g_is_verbose:
escaped = [shlex.quote(arg) for arg in args]
msg = " ".join(escaped)
print(f" Running: {msg}")
# Needs shell=True on Windows due to git.bat in depot_tools.
is_win = sys.platform.startswith('win32')
result = subprocess.run(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
shell=is_win)
success = result.returncode == 0
if check_stdout:
success &= re.search(r'\bwarning\b', result.stdout.lower()) is None
success &= re.search(r'\berror\b', result.stdout.lower()) is None
if not success:
print(f"ERROR: Failure when running: {' '.join(args)}")
print(result.stdout)
raise RuntimeError(f"Failure when running {args[0]}")
return result.stdout
def Git(*args) -> str:
"""Runs a git command."""
return RunCommandAndCheckForErrors(['git'] + list(args), False)
def GitAddRustFiles():
Git("add", "-f", f"{VENDOR_DIR}")
Git("add", f"{THIRD_PARTY_RUST}")
def Gnrt(*args) -> str:
"""Runs a gnrt command."""
return RunCommandAndCheckForErrors([RUN_GNRT] + list(args), True)
def GetCurrentCrateIds() -> Set[str]:
"""Parses Cargo.lock and returns a set of crate ids
(e.g. "[email protected]", "[email protected]", ...)."""
t = toml.load(open(CARGO_LOCK))
result = set()
for p in t["package"]:
name = p["name"]
version = p["version"]
crate_id = f"{name}@{version}"
assert crate_id not in result
result.add(crate_id)
return result
def GetVetExemptedCrateIds() -> Set[str]:
"""Parses supply-chain/config.toml and returns a set of crate ids
that have `exemptions` entries."""
vet_config = toml.load(open(VET_CONFIG))
result = set()
if "exemptions" not in vet_config:
return result
for crate_name, exemptions in vet_config["exemptions"].items():
for exemption in exemptions:
# Ignoring which criteria are covered by the exemption
# (it is not needed if we just want to emit a warning).
crate_version = exemption["version"]
crate_id = f"{crate_name}@{crate_version}"
result.add(crate_id)
return result
@dataclass(eq=True, order=True)
class UpdatedCrate:
old_crate_id: str
new_crate_id: str
def __str__(self):
name = ConvertCrateIdToCrateName(self.old_crate_id)
assert name == ConvertCrateIdToCrateName(self.new_crate_id)
old_version = ConvertCrateIdToCrateVersion(self.old_crate_id)
new_version = ConvertCrateIdToCrateVersion(self.new_crate_id)
return f"{name}: {old_version} => {new_version}"
@dataclass
class CratesDiff:
updates: List[UpdatedCrate]
removed_crate_ids: List[str]
added_crate_ids: List[str]
def size(self):
return len(self.updates) + len(self.added_crate_ids) + len(
self.removed_crate_ids)
def DiffCrateIds(old_crate_ids: Set[str],
new_crate_ids: Set[str],
only_minor_updates=True) -> CratesDiff:
"""Compares two results of `GetCurrentCrateIds` and returns what changed.
When `only_minor_updates` is True, then `[email protected]` => `[email protected]` will be
treated as a removal of `[email protected]` and an addition of `[email protected]`. Otherwise,
it will be treated as an update.
"""
def CrateIdsToDict(crate_ids: Set[str],
only_minor_updates) -> Dict[str, str]:
"""Transforms `crate_ids` into a dictionary that maps either 1) a crate
name (when `only_minor_updates=False`) or 2) a crate epoch (when
`only_minor_updates=True`) into 3) a crate version. The caller should
treat the key format as an opaque implementation detail and just assume
that it will be stable for tracking a crate version across updates."""
result = dict()
for crate_id in crate_ids:
name = ConvertCrateIdToCrateName(crate_id)
version = ConvertCrateIdToCrateVersion(crate_id)
if only_minor_updates:
epoch = GetEpoch(version)
key = f'{name}@{epoch}'
else:
key = name
if key in result:
# No conflicts expected in `auto` or `single` mode.
assert not only_minor_updates
old_crate_id = result[key]
new_crate_id = crate_id
raise RuntimeError(f"Error calculating a `Cargo.lock` diff:" + \
f" conflict between {old_crate_id} and " + \
f"{new_crate_id}")
result[key] = crate_id
return result
# Ignoring `unchanged_ids` limits the situations when the key of `[email protected]`
# may conflict with the key of `[email protected]`.
unchanged_ids = new_crate_ids & old_crate_ids
old_dict = CrateIdsToDict(old_crate_ids - unchanged_ids, only_minor_updates)
new_dict = CrateIdsToDict(new_crate_ids - unchanged_ids, only_minor_updates)
updates = list()
removed_crate_ids = list()
for key, old_crate_id in old_dict.items():
new_crate_id = new_dict.get(key)
if new_crate_id:
assert old_crate_id != new_crate_id
updates.append(UpdatedCrate(old_crate_id, new_crate_id))
else:
removed_crate_ids.append(old_crate_id)
added_crate_ids = list()
for key, new_crate_id in new_dict.items():
if key not in old_dict:
added_crate_ids.append(new_crate_id)
return CratesDiff(sorted(updates), sorted(removed_crate_ids),
sorted(added_crate_ids))
def GetEpoch(crate_version: str) -> str:
v = crate_version.split('.')
if v[0] == '0':
return f'v0_{v[1]}'
return f'v{v[0]}'
def ConvertCrateIdToCrateName(crate_id: str) -> str:
""" Converts a `crate_id` into a `crate_name`."""
return crate_id[:crate_id.find("@")]
def ConvertCrateIdToCrateVersion(crate_id: str) -> str:
""" Converts a `crate_id` into a `crate_version`."""
crate_version = crate_id[crate_id.find("@") + 1:]
return crate_version
def _ConvertCrateIdToEpochDirRelativeToChromiumRoot(crate_id: str) -> str:
""" Converts a `crate_id` (e.g. "[email protected]") into an epoch dir
(e.g. "third_party/rust/foo/v_1"). The returned dir is relative
to Chromium root. """
crate_name = ConvertCrateIdToCrateName(crate_id)
crate_name = crate_name.replace("-", "_")
epoch = GetEpoch(ConvertCrateIdToCrateVersion(crate_id))
target = os.path.join("third_party", "rust", crate_name, epoch)
return target
def ConvertCrateIdToEpochDir(crate_id: str) -> str:
""" Converts a `crate_id` (e.g. "[email protected]") into a path to an epoch dir
(e.g. on Windows: "<path to chromium root>\\third_party\\rust\\foo\\v_1").
"""
return os.path.join(
CHROMIUM_DIR, _ConvertCrateIdToEpochDirRelativeToChromiumRoot(crate_id))
def ConvertCrateIdToVendorDir(crate_id: str) -> str:
""" Converts a `crate_id` (e.g. "[email protected]") into a path to a target dir
(e.g. on Windows: "<path to chromium root>\\third_party\\rust\\foo\\v_1").
"""
crate_name = ConvertCrateIdToCrateName(crate_id)
crate_version = ConvertCrateIdToCrateVersion(crate_id)
crate_vendor_dir = os.path.join(VENDOR_DIR, f"{crate_name}-{crate_version}")
return crate_vendor_dir
def ConvertCrateIdToGnLabel(crate_id: str) -> str:
""" Converts a `crate_id` (e.g. "[email protected]") into
[a GN label](https://gn.googlesource.com/gn/+/main/docs/reference.md#labels)
(e.g. "//third_party/rust/foo/v_1:lib"). """
dir_name = _ConvertCrateIdToEpochDirRelativeToChromiumRoot(crate_id)
dir_name = dir_name.replace(os.sep, "/") # GN uses `/` as a path separator.
return f"//{dir_name}:lib"
def FindUpdateableCrates() -> List[str]:
"""Runs `gnrt update` and returns a `list` of old crate ids (e.g.
"[email protected]") that can be updated to a new version. (Idempotent -
afterwards it runs `git reset --hard` to undo any changes.)"""
print("Checking which crates can be updated...")
assert not Git("status", "--porcelain") # No local changes expected here.
old_crate_ids = GetCurrentCrateIds()
Gnrt("update")
new_crate_ids = GetCurrentCrateIds()
Git("reset", "--hard")
diff = DiffCrateIds(old_crate_ids, new_crate_ids)
crate_ids = [update.old_crate_id for update in diff.updates]
if crate_ids:
names = sorted([ConvertCrateIdToCrateName(id) for id in crate_ids])
text = f"Found updates for {len(crate_ids)} crates: {', '.join(names)}"
print(textwrap.shorten(text, 80))
return sorted(crate_ids)
def FindSizeOfCrateUpdate(crate_id: str) -> int:
"""Runs `gnrt update <crate_id>` and returns how many crates this would
update. (`crate_id` typically looks like "[email protected]". This function is
idempotent - at the end it runs `git reset --hard` to undo any changes.)"""
print(f"Measuring the delta of updating {crate_id} to a newer version...")
assert not Git("status", "--porcelain") # No local changes expected here.
old_crate_ids = GetCurrentCrateIds()
Gnrt("update", crate_id)
new_crate_ids = GetCurrentCrateIds()
Git("reset", "--hard")
diff = DiffCrateIds(old_crate_ids, new_crate_ids)
return diff.size()
def FormatMarkdownItem(item: str) -> str:
return textwrap.fill(f"* {item}",
width=72,
subsequent_indent=' ',
break_long_words=False,
break_on_hyphens=False).strip()
def SortedMarkdownList(input_list: List[str]) -> str:
input_list = [FormatMarkdownItem(item) for item in sorted(input_list)]
return "\n".join(input_list)
def CreateVetPolicyDescription(crate_ids: List[str]) -> str:
"""Returns a textual description of the required `cargo vet`'s
certifications.
Args:
crate_ids: List of crate ids - e.g. `["[email protected]","[email protected]"]`
Returns:
String suitable for including in the CL description.
"""
vet_config = toml.load(open(VET_CONFIG))
crate_id_to_criteria = dict()
for crate_id in crate_ids:
crate_name = ConvertCrateIdToCrateName(crate_id)
crate_version = ConvertCrateIdToCrateVersion(crate_id)
policy = vet_config["policy"][f"{crate_name}:{crate_version}"][
"criteria"]
policy.sort()
crate_id_to_criteria[crate_id] = policy
criteria_to_crate_ids = dict()
for crate_id, criteria in crate_id_to_criteria.items():
criteria = ', '.join(criteria)
if criteria not in criteria_to_crate_ids:
criteria_to_crate_ids[criteria] = []
criteria_to_crate_ids[criteria].append(crate_id)
items = []
for criteria, crate_ids in criteria_to_crate_ids.items():
crate_ids.sort()
crate_ids = ', '.join(crate_ids)
if not criteria:
criteria = "No audit criteria found. Crates with no audit " \
"criteria can be submitted without an update to " \
"audits.toml."
items.append(f"{crate_ids}: {criteria}")
description = \
"""Chromium `supply-chain/config.toml` policy requires that the following
audit criteria are met (note that these are the *minimum* required
criteria and `supply-chain/audits.toml` can and should record a stricter
certification if possible; see also //docs/rust-unsafe.md):
"""
description += SortedMarkdownList(items)
return description
def CreateCommitTitle(old_crate_id: str, diff: CratesDiff) -> str:
crate_name = ConvertCrateIdToCrateName(old_crate_id)
old_version = ConvertCrateIdToCrateVersion(old_crate_id)
update = [u for u in diff.updates if u.old_crate_id == old_crate_id][0]
update = next(filter(lambda u: u.old_crate_id == old_crate_id,
diff.updates))
new_version = ConvertCrateIdToCrateVersion(update.new_crate_id)
roll_summary = f"{crate_name}: " + \
f"{old_version} => {new_version}"
title = f"Roll {roll_summary} in //third_party/rust."
return title
def CreateCommitDescription(title: str, diff: CratesDiff,
include_vet_criteria: bool) -> str:
description = f"""{title}
This CL has been created semi-automatically. The expected review
process and other details can be found at
//tools/crates/create_update_cl.md
"""
update_descriptions = SortedMarkdownList(
[str(update) for update in diff.updates])
new_crate_descriptions = SortedMarkdownList(diff.added_crate_ids)
removed_crate_descriptions = SortedMarkdownList(diff.removed_crate_ids)
assert (update_descriptions)
description += f"\nUpdated crates:\n\n{update_descriptions}\n"
if new_crate_descriptions:
description += f"\nNew crates:\n\n{new_crate_descriptions}\n"
if removed_crate_descriptions:
description += f"\nRemoved crates:\n\n{removed_crate_descriptions}\n"
new_or_updated_crate_ids = diff.added_crate_ids + \
[update.new_crate_id for update in diff.updates]
if include_vet_criteria:
vet_policies = CreateVetPolicyDescription(new_or_updated_crate_ids)
description += f"\n{vet_policies}"
description += """
Bug: None
Cq-Include-Trybots: chromium/try:android-rust-arm32-rel
Cq-Include-Trybots: chromium/try:android-rust-arm64-dbg
Cq-Include-Trybots: chromium/try:android-rust-arm64-rel
Cq-Include-Trybots: chromium/try:linux-rust-x64-dbg
Cq-Include-Trybots: chromium/try:linux-rust-x64-rel
Cq-Include-Trybots: chromium/try:win-rust-x64-dbg
Cq-Include-Trybots: chromium/try:win-rust-x64-rel
Disable-Rts: True
"""
return description
def UpdateCrate(args, crate_id: str, upstream_branch: str):
"""Runs `gnrt update <crate_id>` and other follow-up commands to actually
update the crate."""
print(f"Updating {crate_id} to a newer version...")
assert not Git("status", "--porcelain") # No local changes expected here.
Git("checkout", upstream_branch)
assert not Git("status", "--porcelain") # No local changes expected here.
# gnrt update
old_crate_ids = GetCurrentCrateIds()
print(f" Running `gnrt update {crate_id}` ...")
Gnrt("update", crate_id)
new_crate_ids = GetCurrentCrateIds()
if old_crate_ids == new_crate_ids:
print(" `gnrt update` resulted in no changes - "\
"maybe other steps will handle this crate...")
return upstream_branch
diff = DiffCrateIds(old_crate_ids, new_crate_ids)
title = CreateCommitTitle(crate_id, diff)
description = CreateCommitDescription(title, diff, False)
# Checkout a new git branch + `git cl upload`
new_branch = f"{BRANCH_BASENAME}--{crate_id.replace('@', '-')}"
Git("checkout", upstream_branch, "-b", new_branch)
Git("branch", "--set-upstream-to", upstream_branch)
GitAddRustFiles()
Git("commit", "-m", description)
if args.upload:
print(f" Running `git cl upload ...` ...")
GitClUpload("--hashtag=cratesio-autoupdate",
"[email protected]")
FinishUpdatingCrate(args, title, diff)
return new_branch
def FinishUpdatingCrate(args, title: str, diff: CratesDiff):
vet_exempted_crate_ids = GetVetExemptedCrateIds()
updated_old_crate_ids = set()
# git mv <vendor/old version> <vendor/new version>
print(f" Running `git mv <old dir> <new dir>` (for better diff)...")
for update in diff.updates:
updated_old_crate_ids.add(update.old_crate_id)
old_dir = ConvertCrateIdToVendorDir(update.old_crate_id)
new_dir = ConvertCrateIdToVendorDir(update.new_crate_id)
Git("mv", "--force", f"{old_dir}", f"{new_dir}")
old_target_dir = ConvertCrateIdToEpochDir(update.old_crate_id)
new_target_dir = ConvertCrateIdToEpochDir(update.new_crate_id)
if old_target_dir != new_target_dir:
Git("mv", "--force", old_target_dir, new_target_dir)
GitAddRustFiles()
GitCommit(args, "git mv <old dir> <new dir> (for better diff)")
Git("reset", "--hard", "HEAD^") # Undoing `git mv ...`
# gnrt vendor
print(f" Running `gnrt vendor`...")
Gnrt("vendor")
GitAddRustFiles()
# `INCLUSIVE_LANG_SCRIPT` below uses `git grep` and therefore depends on the
# earlier `Git("add"...)` above. Please don't reorder/coalesce the `add`.
new_content = RunCommandAndCheckForErrors([INCLUSIVE_LANG_SCRIPT], False)
with open(INCLUSIVE_LANG_CONFIG, "w") as f:
f.write(new_content)
Git("add", INCLUSIVE_LANG_CONFIG)
GitCommit(args, "gnrt vendor")
if args.upload:
print(f" Running `git cl upload --commit-description=...` ...")
description = CreateCommitDescription(title, diff, True)
GitClUpload(f"--commit-description={description}", "-t",
"Edit CL description to include vet policy")
# gnrt gen
print(f" Running `gnrt gen`...")
Gnrt("gen")
# Some crates (e.g. ones in the `remove_crates` list of `gnrt_config.toml`)
# may result in no changes - this is why we have an `if` below...
if Git("status", "--porcelain"):
GitAddRustFiles()
GitCommit(args, "gnrt gen")
if args.upload:
issue = Git("cl", "issue")
print(f" {issue}")
for exempted_crate_id in (
updated_old_crate_ids.intersection(vet_exempted_crate_ids)):
exempted_crate_name = ConvertCrateIdToCrateName(exempted_crate_id)
print(f" WARNING: The `{exempted_crate_name}` crate "\
"is covered by an exemption rather than an audit. "\
"Please bump the exemption in `vet_config.toml.hbs` "\
"and run `tools/crates/run_gnrt.py vendor` again.")
def RaiseErrorIfGitIsDirty():
if Git("status", "--porcelain"):
raise RuntimeError("Dirty `git status` - save you local changes "\
"before rerunning the script")
def CheckoutInitialBranch(branch):
print(f"Checking out the `{branch}` branch...")
RaiseErrorIfGitIsDirty()
Git("checkout", branch)
RaiseErrorIfGitIsDirty()
# Ensure the //third_party/rust-toolchain version matches the branch.
print("Running //tools/rust/update_rust.py (hopefully a no-op)...")
RunCommandAndCheckForErrors([UPDATE_RUST_SCRIPT], False)
def GitClUpload(*args):
# `--bypass-hooks` because the uploaded CL will initially fail
# `tools/crates/run_cargo_vet.py check`.
#
# `-o banned-words-skip` is used, because the CL is auto-generated and only
# modifies third-party libraries (where any banned words would be purely
# accidental; see also https://crbug.com/346174899).
#
# I am not 100% sure exactly why `--force` is needed, but without it
# `git cl upload` hangs sometimes. I am guessing that `--force` is needed
# to suppress a prompt, although I am not sure what prompt + why that prompt
# appears.
Git("cl", "upload", "--bypass-hooks", "--force", "-o", "banned-words~skip",
*args)
def GitCommit(args, title):
Git("commit", "-m", title)
if args.upload:
print(f" Running `git cl upload ...` ...")
GitClUpload("-m", title)
def ResolveCrateNameToCrateId(crate_name):
"""Parses `Cargo.toml` to resolve `crate_name` into "[email protected]".
Throws if `crate_name` can't be resolved.
Parameters:
crate_name: Either "crate-name" or already "[email protected]"
"""
t = toml.load(open(CARGO_LOCK))
if '@' in crate_name:
resolved_crate_version = ConvertCrateIdToCrateVersion(crate_name)
resolved_crate_name = ConvertCrateIdToCrateName(crate_name)
else:
same_name = [p for p in t["package"] if p["name"] == crate_name]
if len(same_name) == 0:
raise RuntimeError(
f"`Cargo.toml` has no crates matching `{crate_name}`")
elif len(same_name) > 1:
ver1 = same_name[0]["version"]
ver2 = same_name[1]["version"]
raise RuntimeError(
f"Ambiguous argument - specify which old version to update, "\
f"e.g. `{crate_name}@{ver1}` or `{crate_name}@{ver2}")
resolved_crate_name = crate_name
resolved_crate_version = same_name[0]["version"]
crate_id = f"{resolved_crate_name}@{resolved_crate_version}"
return crate_id
def AutoUpdate(args):
upstream_branch = args.upstream_branch
CheckoutInitialBranch(upstream_branch)
todo_crate_ids = FindUpdateableCrates()
if args.skip:
todo_crate_ids = list([
crate_id for crate_id in todo_crate_ids
if not crate_id.split("@")[0] in args.skip
])
if not todo_crate_ids:
print("There were no updates - exiting early...")
return 0
update_sizes = dict()
for crate_id in todo_crate_ids:
update_sizes[crate_id] = FindSizeOfCrateUpdate(crate_id)
todo_crate_ids = sorted(todo_crate_ids,
key=lambda crate_id: update_sizes[crate_id])
print(f"** Updating {len(todo_crate_ids)} crates! "
f"Expect this to take about {len(todo_crate_ids) * 2} minutes.")
while todo_crate_ids:
old_crate_ids = GetCurrentCrateIds()
for crate_id in todo_crate_ids:
upstream_branch = UpdateCrate(args, crate_id, upstream_branch)
new_crate_ids = GetCurrentCrateIds()
diff = DiffCrateIds(old_crate_ids, new_crate_ids)
actually_updated_crate_ids = set([u.old_crate_id for u in diff.updates])
missed_crate_ids = [
crate_id for crate_id in todo_crate_ids
if crate_id not in actually_updated_crate_ids
]
if missed_crate_ids:
if len(missed_crate_ids) == len(todo_crate_ids):
print("ERROR: Failed to make progress with these crates:")
print(f"{', '.join(todo_crate_ids)}")
raise RuntimeError("Failed to make progress")
else:
print(f"** Retrying {len(missed_crate_ids)} crates.")
todo_crate_ids = missed_crate_ids
def SingleCrate(args):
upstream_branch = args.upstream_branch
CheckoutInitialBranch(upstream_branch)
crate_id = ResolveCrateNameToCrateId(args.crate[0])
UpdateCrate(args, crate_id, upstream_branch)
def ManualUpdate(args):
title = args.title
RaiseErrorIfGitIsDirty()
print(f"Post-processing a manual edit of `Cargo.toml`...")
print(f" Running `gnrt vendor` to detect `Cargo.lock` changes...")
old_crate_ids = GetCurrentCrateIds()
Gnrt("vendor")
new_crate_ids = GetCurrentCrateIds()
Git("reset", "--hard")
Git("clean", "-d", "--force", "--", f"{THIRD_PARTY_RUST}")
diff = DiffCrateIds(old_crate_ids, new_crate_ids, only_minor_updates=False)
if diff.size() == 0:
raise RuntimeError(
"No changes in `Cargo.lock` after running `gnrt vendor`")
# This covers most update steps: git mv, gnrt vendor, gnrt gen
FinishUpdatingCrate(args, title, diff)
# Remove old `//third_party/rust/foo/v<old>` directories
print(f" Removing //third_party/rust/.../<old_epoch> ...")
for update in diff.updates:
old_target_dir = ConvertCrateIdToEpochDir(update.old_crate_id)
new_target_dir = ConvertCrateIdToEpochDir(update.new_crate_id)
if old_target_dir == new_target_dir:
continue # Skip minor crate updates
old_files_count = len(
Git("ls-files", "--", old_target_dir).splitlines())
new_files_count = len(
Git("ls-files", "--", new_target_dir).splitlines())
if old_files_count == new_files_count:
Git("rm", "-r", "--force", "--", old_target_dir)
elif old_files_count > new_files_count:
print(f"WARNING: not deleting {old_target_dir} "\
"because it contains extra files")
else:
print(
f"WARNING: {old_target_dir} unexpectedly has less files "\
f"than {new_target_dir}")
GitCommit(args, "Removing //third_party/rust/.../<old_epoch>")
# Fix up the target names
print(f" Updating the target name in BUILD.gn files...")
for update in diff.updates:
old_target = ConvertCrateIdToGnLabel(update.old_crate_id)
new_target = ConvertCrateIdToGnLabel(update.new_crate_id)
if old_target == new_target: continue
grep = Git("grep", "-l", old_target, "--", "*/BUILD.gn")
for path in grep.splitlines():
if not path: continue
if "third_party/rust" in path: continue
with open(path, 'r') as file:
file_contents = file.read()
file_contents = file_contents.replace(old_target, new_target)
with open(path, 'w') as file:
file.write(file_contents)
Git("add", "--", path)
GitCommit(args, "Updating the target name in BUILD.gn files")
def main():
parser = argparse.ArgumentParser(description="Update Rust crates")
parser.add_argument("--no-upload",
dest='upload',
default=True,
action='store_false',
help="Avoids uploading CLs to Gerrit")
parser.add_argument("--verbose", action='store_true')
subparsers = parser.add_subparsers(required=False)
parser_auto = subparsers.add_parser(
"auto", description="Automatically update minor version of all crates")
parser_auto.set_defaults(func=AutoUpdate)
parser_auto.add_argument(
"--upstream-branch",
default="origin/main",
help="The upstream branch on which to base the series of CLs.")
parser_auto.add_argument("--skip",
nargs="+",
help="Skip updating this crate name.")
parser_single = subparsers.add_parser(
"single",
description="Automatically update minor version of a single crate")
parser_single.set_defaults(func=SingleCrate)
parser_single.add_argument("crate",
nargs=1,
help="The name of the crate to update.")
parser_single.add_argument(
"--upstream-branch",
default="origin/main",
help="The upstream branch on which to base the update CL.")
parser_manual = subparsers.add_parser(
"manual",
description="Generate update CL after manual edit of `Cargo.toml`")
parser_manual.set_defaults(func=ManualUpdate)
parser_manual.add_argument("--title",
required=True,
help="The first line of CL description.")
args = parser.parse_args()
if "func" not in args:
msg = "ERROR: No auto/single/manual mode specified"
print(msg)
parser.print_help()
raise RuntimeError(msg)
global g_is_verbose
g_is_verbose = args.verbose
args.func(args)
return 0
if __name__ == '__main__':
sys.exit(main())