Add Chromium autoroller rough draft
Adds the initial rough draft of the Chromium -> Dawn DEPS autorolling
script. This is missing a number of features required for actually
autorolling such as CL description generation, but is capable of
locally generating valid rolls.
Bug: 452840620
Change-Id: Ic1368a99e6f2a654625e64121cd3849ea24016ea
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/271234
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Reviewed-by: Yuly Novikov <ynovikov@chromium.org>
Commit-Queue: Brian Sheedy <bsheedy@google.com>
diff --git a/DEPS b/DEPS
index 77c7e1a..ea0d3b4 100644
--- a/DEPS
+++ b/DEPS
@@ -63,6 +63,10 @@
# Set to True by Chromium if syncing from a Chromium checkout.
'build_with_chromium': False,
+
+ # NOTE: This is not currently accurate since no Chromium -> Dawn roll has
+ # been performed yet.
+ 'chromium_revision': 'c2d941cd12644d6b893c305b1904e358727d597d',
}
deps = {
diff --git a/scripts/roll_chromium_deps.py b/scripts/roll_chromium_deps.py
new file mode 100755
index 0000000..a00253b
--- /dev/null
+++ b/scripts/roll_chromium_deps.py
@@ -0,0 +1,697 @@
+#!/usr/bin/env vpython3
+# Copyright 2025 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Rolls DEPS entries shared with Chromium."""
+
+import abc
+import argparse
+import base64
+import dataclasses
+import logging
+import pathlib
+import posixpath
+import subprocess
+import sys
+from typing import Any, Self
+
+import requests
+
+DAWN_ROOT = pathlib.Path(__file__).resolve().parents[1]
+DEPS_FILE = DAWN_ROOT / 'DEPS'
+
+CHROMIUM_SRC_URL = 'https://chromium.googlesource.com/chromium/src'
+CHROMIUM_REVISION_VAR = 'chromium_revision'
+
+# GN variables that need to be synced. A map from Dawn variable name to
+# Chromium variable name.
+SYNCED_VARIABLES = {}
+
+# DEPS entries which have dep_type = cipd. In the Chromium DEPS file, these
+# will be prefixed with src/.
+SYNCED_CIPD_DEPS = {
+ 'buildtools/linux64',
+ 'buildtools/mac',
+ 'buildtools/reclient',
+ 'buildtools/win',
+ 'third_party/ninja',
+ 'third_party/siso/cipd',
+}
+
+# DEPS entries which have dep_type = gcs. In the Chromium DEPS file, these will
+# be prefixed with src/.
+SYNCED_GCS_DEPS = {
+ 'build/linux/debian_bullseye_arm64-sysroot',
+ 'build/linux/debian_bullseye_armhf-sysroot',
+ 'build/linux/debian_bullseye_i386-sysroot',
+ 'build/linux/debian_bullseye_mipsel-sysroot',
+ 'build/linux/debian_bullseye_mips64el-sysroot',
+ 'build/linux/debian_bullseye_amd64-sysroot',
+}
+
+# Repos that are independently synced by Chromium and Dawn. A map from Dawn
+# names to Chromium names. None means that the names are identical. In the
+# Chromium DEPS file, these will be prefixed with src/.
+# The following repos are already synced by dedicated autorollers:
+# * https://autoroll.skia.org/r/angle-dawn-autoroll
+# * third_party/angle
+# * https://autoroll.skia.org/r/swiftshader-dawn-autoroll
+# * third_party/swiftshader
+# * https://autoroll.skia.org/r/vulkan-deps-dawn-autoroll
+# * third_party/glslang/src
+# * third_party/spirv-headers/src
+# * third_party/spirv-tools/src
+# * third_party/vulkan-deps
+# * third_party/vulkan-headers/src
+# * third_party/vulkan-loader/src
+# * third_party/vulkan-tools/src
+# * third_party/vulkan-utility-libraries/src
+# * third_party/vulkan-validation-layers/src
+SYNCED_REPOS = {
+ 'third_party/catapult': None,
+ 'third_party/clang-format/script': None,
+ 'third_party/depot_tools': None,
+ # third_party/dxheaders is technically used by both Chromium and Dawn, but
+ # for different purposes and on non-overlapping platforms. Thus, there is
+ # no need to sync their revisions.
+ 'third_party/google_benchmark/src': None,
+ 'third_party/googletest': 'third_party/googletest/src',
+ 'third_party/jsoncpp': 'third_party/jsoncpp/source',
+ 'third_party/libc++/src': None,
+ 'third_party/libc++abi/src': None,
+ 'third_party/libprotobuf-mutator/src': None,
+ 'third_party/llvm-libc/src': None,
+ 'third_party/libdrm/src': None,
+ 'third_party/libFuzzer/src': None,
+ # third_party/vulkan_memory_allocator is shared with Chromium, but is
+ # manually rolled since it typically requires additional code changes in
+ # the repo.
+ # third_party/webgpu-cts is technically used by both Chromium and Dawn, but
+ # they are used for different purposes and the CTS roller needs to roll
+ # Dawn's copy in order to update expectations.
+}
+
+# Chromium directories that are exported as pseudo-repos in
+# chromium.googlesource.com under chromium/src/. Mapping of Dawn path to
+# Chromium src-relative path. None means that the names are identical.
+EXPORTED_CHROMIUM_REPOS = {
+ 'build': None,
+ 'buildtools': None,
+ 'testing': None,
+ 'third_party/abseil-cpp': None,
+ 'third_party/jinja2': None,
+ 'third_party/markupsafe': None,
+ 'third_party/partition_alloc': 'base/allocator/partition_allocator',
+ 'third_party/protobuf': None,
+ 'third_party/zlib': None,
+ 'tools/clang': None,
+ 'tools/mb': None,
+ 'tools/memory': None,
+ 'tools/protoc_wrapper': None,
+ 'tools/valgrind': None,
+ 'tools/win': None,
+}
+
+
+class ChangedDepsEntry(abc.ABC):
+ """Base class for all changed DEPS entries."""
+
+ @abc.abstractmethod
+ def setdep_args(self) -> list[str]:
+ """Returns 'gclient setdep'-compatible arguments.
+
+ The returned arguments will cause 'gclient setdep' to update the DEPS
+ file content to the new version.
+ """
+
+
+@dataclasses.dataclass
+class ChangedVariable(ChangedDepsEntry):
+ """Represents a single changed DEPS variable."""
+ # The name of the variable in Dawn's DEPS file.
+ name: str
+ # The old version in Dawn's DEPS file.
+ old_version: str
+ # The new version that Dawn's DEPS file will contain.
+ new_version: str
+
+ def setdep_args(self) -> list[str]:
+ return [
+ '--var',
+ f'{self.name}={self.new_version}',
+ ]
+
+
+@dataclasses.dataclass
+class ChangedRepo(ChangedDepsEntry):
+ """Represents a single changed DEPS repo entry."""
+ # The name of the dependency in Dawn's DEPS file.
+ name: str
+ # The old revision in Dawn's DEPS file.
+ old_revision: str
+ # The new revision that Dawn's DEPS file will contain.
+ new_revision: str
+
+ def setdep_args(self) -> list[str]:
+ return [
+ '--revision',
+ f'{self.name}@{self.new_revision}',
+ ]
+
+
+@dataclasses.dataclass
+class CipdPackage:
+ """Represents a single element of a CIPD DEPS entry's 'packages' list."""
+ package: str
+ version: str
+
+ @classmethod
+ def from_dict(cls, dict_repr: dict[str, str]) -> Self:
+ """Creates an instance from a DEPS entry dict.
+
+ Args:
+ dict_repr: A dictionary from a GCS DEPS entry's 'packages' list.
+
+ Returns:
+ A CipdPackage instance filled with the information contained within
+ |dict_repr|.
+ """
+ return cls(
+ package=dict_repr['package'],
+ version=dict_repr['version'],
+ )
+
+ def setdep_str(self) -> str:
+ """Gets a 'gclient setdep'-compatible string representation."""
+ # Remove any double curly braces, e.g. ${{arch}}
+ package = self.package.format()
+ return f'{package}@{self.version}'
+
+
+@dataclasses.dataclass
+class ChangedCipd(ChangedDepsEntry):
+ """Represents a single changed DEPS CIPD entry."""
+ # The name of the dependency in Dawn's DEPS file.
+ name: str
+ # The old packages in Dawn's DEPS file
+ old_packages: list[CipdPackage]
+ # The new packages that Dawn's DEPS file will contain.
+ new_packages: list[CipdPackage]
+
+ def setdep_args(self) -> list[str]:
+ revisions = [
+ f'{self.name}:{p.setdep_str()}' for p in self.new_packages
+ ]
+ return ['--revision'] + revisions
+
+
+@dataclasses.dataclass
+class GcsObject:
+ """Represents a single element of a GCS DEPS entry's 'objects' list."""
+ object_name: str
+ sha256sum: str
+ size_bytes: int
+ generation: int
+
+ @classmethod
+ def from_dict(cls, dict_repr: dict[str, str | int]) -> Self:
+ """Creates an instance from a DEPS entry dict.
+
+ Args:
+ dict_repr: A dictionary from a GCS DEPS entry's 'objects' list.
+
+ Returns:
+ A GcsObject instance filled with the information contained within
+ |dict_repr|.
+ """
+ return cls(
+ object_name=dict_repr['object_name'],
+ sha256sum=dict_repr['sha256sum'],
+ size_bytes=dict_repr['size_bytes'],
+ generation=dict_repr['generation'],
+ )
+
+ def as_comma_separated_str(self) -> str:
+ return (f'{self.object_name},{self.sha256sum},{self.size_bytes},'
+ f'{self.generation}')
+
+
+@dataclasses.dataclass
+class ChangedGcs(ChangedDepsEntry):
+ """Represents a single changed DEPS GCS entry."""
+ # The name of the dependency in Dawn's DEPS file.
+ name: str
+ # The old objects in Dawn's DEPS file.
+ old_objects: list[GcsObject]
+ # The new objects that Dawn's DEPS file will contain.
+ new_objects: list[GcsObject]
+
+ def setdep_args(self) -> list[str]:
+ comma_separated_objects = [
+ o.as_comma_separated_str() for o in self.new_objects
+ ]
+ object_string = '?'.join(comma_separated_objects)
+ return ['--revision', f'{self.name}@{object_string}']
+
+
+def _parse_deps_file(deps_content: str) -> dict[str, Any]:
+ """Parses DEPS file content into a Python dict.
+
+ Args:
+ deps_content: The content of a DEPS file.
+
+ Returns:
+ A dict containing all content of the DEPS file. For example, the top
+ level `vars` mapping in a DEPS file can be accessed via the 'vars' item
+ in the returned dictionary.
+ """
+ local_scope = {}
+ global_scope = {
+ 'Str': lambda str_value: str_value,
+ 'Var': lambda var_name: local_scope['vars'][var_name],
+ }
+ exec(deps_content, global_scope, local_scope)
+ return local_scope
+
+
+def _add_depot_tools_to_path() -> None:
+ sys.path.append(str(DAWN_ROOT / 'build'))
+ import find_depot_tools
+ find_depot_tools.add_depot_tools_to_path()
+
+
+def _get_remote_head_revision(remote_url: str) -> str:
+ """Retrieves the HEAD revision for a remote git URL.
+
+ Args:
+ remote_url: The remote git URL to get HEAD from.
+
+ Returns:
+ The revision currently corresponding to HEAD.
+ """
+ cmd = [
+ 'git',
+ 'ls-remote',
+ remote_url,
+ 'HEAD',
+ ]
+ proc = subprocess.run(cmd,
+ check=True,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ head_revision = proc.stdout.strip().split()[0]
+ return head_revision
+
+
+def _get_roll_revision_range(target_revision: str | None,
+ dawn_deps: dict) -> ChangedRepo:
+ """Determines the range being rolled.
+
+ Args:
+ target_revision: The revision being targeted for the roll. If not
+ specified, the HEAD revision will be determined and used.
+ dawn_deps: The parsed contents of the Dawn DEPS file.
+
+ Returns:
+ A ChangedRepo with the determined revision range.
+ """
+ old_revision = dawn_deps['vars'][CHROMIUM_REVISION_VAR]
+ new_revision = target_revision
+ if not new_revision:
+ new_revision = _get_remote_head_revision(CHROMIUM_SRC_URL)
+ logging.info('Using %s as the HEAD revision.', new_revision)
+ return ChangedRepo(name='chromium/src',
+ old_revision=old_revision,
+ new_revision=new_revision)
+
+
+def _read_gitiles_content(file_url: str) -> str:
+ """Reads the contents of a file from Gitiles.
+
+ Args:
+ file_url: A URL pointing to a file to read from Gitiles.
+
+ Returns:
+ The string content of the specified file.
+ """
+ file_url = file_url + '?format=TEXT'
+ r = requests.get(file_url)
+ r.raise_for_status()
+ return base64.b64decode(r.text).decode('utf-8')
+
+
+def _read_remote_chromium_file(src_relative_path: str, revision: str) -> str:
+ """Reads the contents of a Chromium file from Gitiles.
+
+ Args:
+ src_relative_path: A POSIX path to the file to read relative to
+ chromium/src.
+ revision: The Chromium revision to read the file contents at.
+ """
+ file_url = posixpath.join(CHROMIUM_SRC_URL, '+', revision,
+ src_relative_path)
+ return _read_gitiles_content(file_url)
+
+
+def _get_changed_deps_entries(dawn_deps: dict,
+ chromium_deps: dict) -> list[ChangedDepsEntry]:
+ """Gets all entries that have changed between the two provided DEPS.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+ chromium_deps: The parsed content of the Chromium DEPS file.
+
+ Returns:
+ A list ChangedDepsEntry objects, each one corresponding to a change
+ between the two DEPS files.
+ """
+ changed_entries = []
+ changed_entries.extend(_get_changed_variables(dawn_deps, chromium_deps))
+ changed_entries.extend(_get_changed_cipd(dawn_deps, chromium_deps))
+ changed_entries.extend(_get_changed_gcs(dawn_deps, chromium_deps))
+ changed_entries.extend(
+ _get_changed_non_exported_repos(dawn_deps, chromium_deps))
+ changed_entries.extend(_get_changed_exported_repos(dawn_deps))
+ return changed_entries
+
+
+def _get_changed_variables(dawn_deps: dict,
+ chromium_deps: dict) -> list[ChangedVariable]:
+ """Gets all GN variables that have changed between the two provided DEPS.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+ chromium_deps: The parsed content of the Chromium DEPS file.
+
+ Returns:
+ A list of all variable entries that have changed between the two DEPS
+ files.
+ """
+ changed_variables = []
+ for dawn_var, chromium_var in SYNCED_VARIABLES.items():
+ dawn_value = dawn_deps['vars'].get(dawn_var)
+ chromium_value = chromium_deps['vars'].get(chromium_var)
+ if not dawn_value:
+ raise RuntimeError(
+ f'Could not find Dawn GN variable {dawn_var}. Was it removed?')
+ if not chromium_value:
+ raise RuntimeError(
+ f'Could not find Chromium GN variable {chromium_var}. Was it '
+ f'removed?')
+ changed_variables.append(
+ ChangedVariable(
+ name=dawn_var,
+ old_version=dawn_value,
+ new_version=chromium_value,
+ ))
+ return changed_variables
+
+
+def _get_changed_cipd(dawn_deps: dict,
+ chromium_deps: dict) -> list[ChangedCipd]:
+ """Gets all CIPD entries that have changed between the two provided DEPS.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+ chromium_deps: The parsed content of the Chromium DEPS file.
+
+ Returns:
+ A list of all CIPD DEPS entries that have changed between the two
+ DEPS files.
+ """
+ changed_cipd = []
+ for dawn_name in SYNCED_CIPD_DEPS:
+ chromium_name = 'src/' + dawn_name
+ if dawn_name not in dawn_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Dawn CIPD entry {dawn_name}. Was it removed?')
+ if chromium_name not in chromium_deps['deps']:
+ raise RuntimeErrror(
+ f'Unable to find Chromium CIPD entry {chromium_name}. Was it '
+ f'removed?')
+
+ dawn_packages = [
+ CipdPackage.from_dict(p)
+ for p in dawn_deps['deps'][dawn_name]['packages']
+ ]
+ chromium_packages = [
+ CipdPackage.from_dict(p)
+ for p in chromium_deps['deps'][chromium_name]['packages']
+ ]
+ # Unlike GCS entries which provide all object content with a single
+ # --revision, CIPD entries provide one package to update per
+ # --revision. The behavior when a package within an entry disappears is
+ # not clearly defined, so just fail if we ever see that. This should
+ # rarely happen, though.
+ dawn_package_names = set(p.package for p in dawn_packages)
+ chromium_package_names = set(p.package for p in chromium_packages)
+ if not dawn_package_names.issubset(chromium_package_names):
+ raise RuntimeError(
+ f'Packages for CIPD entry {dawn_name} appear to have changed. '
+ f'Please manually sync the package list.')
+ if dawn_packages != chromium_packages:
+ changed_cipd.append(
+ ChangedCipd(
+ name=dawn_name,
+ old_packages=dawn_packages,
+ new_packages=chromium_packages,
+ ))
+ return changed_cipd
+
+
+def _get_changed_gcs(dawn_deps: dict, chromium_deps: dict) -> list[ChangedGcs]:
+ """Gets all GCS entries that have changed between the two provided DEPS.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+ chromium_deps: The parsed content of the Chromium DEPS file.
+
+ Returns:
+ A list of all GCS DEPS entries that have changed between the two DEPS
+ files.
+ """
+ changed_gcs = []
+ for dawn_name in SYNCED_GCS_DEPS:
+ chromium_name = 'src/' + dawn_name
+ if dawn_name not in dawn_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Dawn GCS entry {dawn_name}. Was it removed?')
+ if chromium_name not in chromium_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Chromium GCS entry {chromium_name}. Was it '
+ f'removed?')
+
+ dawn_objects = [
+ GcsObject.from_dict(o)
+ for o in dawn_deps['deps'][dawn_name]['objects']
+ ]
+ chromium_objects = [
+ GcsObject.from_dict(o)
+ for o in chromium_deps['deps'][chromium_name]['objects']
+ ]
+ if dawn_objects != chromium_objects:
+ changed_gcs.append(
+ ChangedGcs(
+ name=dawn_name,
+ old_objects=dawn_objects,
+ new_objects=chromium_objects,
+ ))
+ return changed_gcs
+
+
+def _get_changed_non_exported_repos(dawn_deps: dict,
+ chromium_deps: dict) -> list[ChangedRepo]:
+ """Gets all non-exported repos that have changed between the DEPS files.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+ chromium_deps: The parsed content of the Chromium DEPS file.
+
+ Returns:
+ A list of all repo entries that have changed between the two DEPS files
+ that are not exported pseudo-repos from Chromium.
+ """
+ changed_repos = []
+ for dawn_name, chromium_name in SYNCED_REPOS.items():
+ chromium_name = chromium_name or dawn_name
+ chromium_name = 'src/' + chromium_name
+ if dawn_name not in dawn_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Dawn repo {dawn_name}. Was it removed?')
+ if chromium_name not in chromium_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Chromium repo {chromium_name}. Was it '
+ f'removed?')
+
+ _, dawn_revision = _get_url_and_revision(
+ _get_raw_url_for_dep_entry(dawn_name, dawn_deps),
+ dawn_deps['vars'])
+ _, chromium_revision = _get_url_and_revision(
+ _get_raw_url_for_dep_entry(chromium_name, chromium_deps),
+ chromium_deps['vars'])
+ if dawn_revision != chromium_revision:
+ changed_repos.append(
+ ChangedRepo(
+ name=dawn_name,
+ old_revision=dawn_revision,
+ new_revision=chromium_revision,
+ ))
+ return changed_repos
+
+
+def _get_changed_exported_repos(dawn_deps: dict) -> list[ChangedRepo]:
+ """Gets all exported repos that have changed since the last roll.
+
+ Args:
+ dawn_deps: The parsed content of the Dawn DEPS file.
+
+ Returns:
+ A list of all repo entries for exported pseudo-repos from Chromium
+ whose HEAD revision is different from the revision currently used by
+ Dawn.
+ """
+ changed_repos = []
+ for dawn_name, chromium_path in EXPORTED_CHROMIUM_REPOS.items():
+ chromium_path = chromium_path or dawn_name
+ if dawn_name not in dawn_deps['deps']:
+ raise RuntimeError(
+ f'Unable to find Dawn repo {dawn_name}. Was it removed?')
+ url, dawn_revision = _get_url_and_revision(
+ _get_raw_url_for_dep_entry(dawn_name, dawn_deps),
+ dawn_deps['vars'])
+ head_revision = _get_remote_head_revision(url)
+ if dawn_revision != head_revision:
+ changed_repos.append(
+ ChangedRepo(
+ name=dawn_name,
+ old_revision=dawn_revision,
+ new_revision=head_revision,
+ ))
+ return changed_repos
+
+
+def _get_raw_url_for_dep_entry(dep_name: str, deps: dict) -> str:
+ """Gets the URL associated with the specified DEPS entry.
+
+ Args:
+ dep_name: The name of the DEPS entry to retrieve the URL for.
+ deps: The parsed DEPS content to extract information from.
+
+ Returns:
+ A string containing the URL associated with |dep_name|.
+ """
+ dep_entry = deps['deps'][dep_name]
+ # Most entries are dicts, but it's also valid for an entry to just be a
+ # git URL.
+ if isinstance(dep_entry, str):
+ return dep_entry
+ return dep_entry['url']
+
+
+def _get_url_and_revision(deps_url: str, vars: dict) -> tuple[str, str]:
+ """Extracts the repo URL and revision from a DEPS url value.
+
+ Known substitutions are also performed on the URL, e.g. replacing
+ {chromium_git} with the actual Chromium git URL.
+
+ Args:
+ deps_url: The URL to operate on.
+ vars: The 'vars' mapping from the parsed DEPS content that |deps_url|
+ came from.
+
+ Returns:
+ A tuple (url, revision).
+ """
+ url, revision = deps_url.rsplit('@', 1)
+ for var, value in vars.items():
+ if not isinstance(value, str):
+ continue
+ search_str = f'{{{var}}}'
+ url = url.replace(search_str, value)
+ return url, revision
+
+
+def _apply_changed_deps(changed_entries: list[ChangedDepsEntry]) -> None:
+ """Applies all changed DEPS entries to the Dawn DEPS file.
+
+ Args:
+ changed_entries: All calculated ChangedDepsEntry objects.
+ """
+ cmd = [
+ 'gclient',
+ 'setdep',
+ ]
+ for ce in changed_entries:
+ cmd.extend(ce.setdep_args())
+ subprocess.run(cmd, check=True)
+
+
+def _parse_args() -> argparse.Namespace:
+ """Parses and returns command line arguments."""
+ parser = argparse.ArgumentParser('Roll DEPS entries shared with Chromium.')
+ parser.add_argument('--verbose',
+ '-v',
+ action='store_true',
+ help='Increase logging verbosity')
+ parser.add_argument('--autoroll',
+ action='store_true',
+ help='Run the script in autoroll mode')
+ parser.add_argument('--revision',
+ help=('A Chromium revision to roll to. If '
+ 'unspecified, HEAD is used.'))
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = _parse_args()
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.INFO)
+
+ # The autoroller does not have locally synced dependencies, so we cannot use
+ # the copy under //third_party.
+ if not args.autoroll:
+ _add_depot_tools_to_path()
+
+ with open(DEPS_FILE, encoding='utf-8') as infile:
+ dawn_deps = _parse_deps_file(infile.read())
+ revision_range = _get_roll_revision_range(args.revision, dawn_deps)
+ chromium_deps = _parse_deps_file(
+ _read_remote_chromium_file('DEPS', revision_range.new_revision))
+ changed_entries = _get_changed_deps_entries(dawn_deps, chromium_deps)
+ changed_entries.append(
+ ChangedVariable(
+ name=CHROMIUM_REVISION_VAR,
+ old_version=revision_range.old_revision,
+ new_version=revision_range.new_revision,
+ ))
+ _apply_changed_deps(changed_entries)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/roll_chromium_deps.py.vpython3 b/scripts/roll_chromium_deps.py.vpython3
new file mode 100644
index 0000000..e2c3812
--- /dev/null
+++ b/scripts/roll_chromium_deps.py.vpython3
@@ -0,0 +1,60 @@
+# This is a vpython "spec" file.
+#
+# It describes patterns for python wheel dependencies of the python scripts in
+# the chromium repo, particularly for dependencies that have compiled components
+# (since pure-python dependencies can be easily vendored into third_party).
+#
+# When vpython is invoked, it finds this file and builds a python VirtualEnv,
+# containing all of the dependencies described in this file, fetching them from
+# CIPD (the "Chrome Infrastructure Package Deployer" service). Unlike `pip`,
+# this never requires the end-user machine to have a working python extension
+# compilation environment. All of these packages are built using:
+# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/
+#
+# All python scripts in the repo share this same spec, to avoid dependency
+# fragmentation.
+#
+# If you have depot_tools installed in your $PATH, you can invoke python scripts
+# in this repo by running them as you normally would run them, except
+# substituting `vpython` instead of `python` on the command line, e.g.:
+# vpython path/to/script.py some --arguments
+#
+# Read more about `vpython` and how to modify this file here:
+# https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
+
+python_version: "3.11"
+
+# The default set of platforms vpython checks does not yet include mac-arm64.
+# Setting `verify_pep425_tag` to the list of platforms we explicitly must support
+# allows us to ensure that vpython specs stay mac-arm64-friendly
+verify_pep425_tag: [
+ {python: "cp311", abi: "cp311", platform: "manylinux1_x86_64"},
+ {python: "cp311", abi: "cp311", platform: "linux_arm64"},
+
+ {python: "cp311", abi: "cp311", platform: "macosx_10_10_intel"},
+ {python: "cp311", abi: "cp311", platform: "macosx_11_0_arm64"},
+
+ {python: "cp311", abi: "cp311", platform: "win32"},
+ {python: "cp311", abi: "cp311", platform: "win_amd64"}
+]
+
+wheel: <
+ name: "infra/python/wheels/requests-py3"
+ version: "version:2.32.5"
+>
+wheel: <
+ name: "infra/python/wheels/charset_normalizer-py3"
+ version: "version:2.0.12"
+>
+wheel: <
+ name: "infra/python/wheels/idna-py3"
+ version: "version:3.10"
+>
+wheel: <
+ name: "infra/python/wheels/urllib3-py3"
+ version: "version:2.5.0"
+>
+wheel: <
+ name: "infra/python/wheels/certifi-py3"
+ version: "version:2025.8.3"
+>
\ No newline at end of file