blob: a00253bcc4f5170287fc0e4d7a78fb680b99efcd [file] [log] [blame]
#!/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()