blob: 3c3a807056dd55dda9e1e73957a36c45cf550632 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This script is used to roll cobalt's submodules to match those used in Fuchsia.
It also copies some files from fuchsia and syncs the versions of prebuilts.
With a clean checkout, updating the submodules should be as simple as:
./cobaltb.py sync_with_fuchsia --make_commit
This will update all of the submodules to match those used in Fuchsia, and then
if there are any changes, will add them all to a commit that you can then upload
to Gerrit.
If you omit the '--make_commit' flag, then the commit will not be generated, but
a suggested commit message will be printed out at the end of the command.
"""
import sys
import os
# Ensure that this file can import from cobalt root even if run as a script.
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import tempfile
import xml.etree.ElementTree as ElementTree
import subprocess
import re
import shutil
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
ROOT_DIR = os.path.abspath(os.path.join(THIS_DIR, '..'))
def short_hash(long_hash):
"""Returns a shortened, still unique commit hash."""
return subprocess.check_output(['git', 'rev-parse', '--short',
long_hash]).decode('utf-8').strip()
def hash_distance(start, end):
"""Returns the number of commits between the two provided hashes."""
return len(
subprocess.check_output(['git', 'rev-list',
'%s..%s' % (start, end)
]).decode('utf-8').strip().split('\n'))
def update_from_project(project, update_report, commit_message):
"""Attempts to update the submodule for the supplied project.
The project is of the following format:
<project name="third_party/abseil-cpp"
path="third_party/abseil-cpp"
revision="17b90e77683fd71c72478c2af966fe0a4c1c07be"
remote="https://fuchsia.googlesource.com/third_party/abseil-cpp"/>
"""
full_path = os.path.join(ROOT_DIR, project.attrib['path'])
if os.path.isdir(full_path):
savedDir = os.getcwd()
try:
os.chdir(full_path)
root_dir = subprocess.check_output(
['git', 'rev-parse', '--show-toplevel']).decode('utf-8').strip()
if root_dir == ROOT_DIR:
# If the root_dir at this path is equal to the global ROOT_DIR, that means this path exists
# but is not actually a submodule. (for example //third_party/go)
return
current_hash = subprocess.check_output(['git', 'rev-parse',
'HEAD']).decode('utf-8').strip()
if current_hash != project.attrib['revision']:
# The current HEAD does not match the revision specified in the manifest
print('Updating %s to %s' %
(project.attrib['path'], project.attrib['revision']))
subprocess.check_call(['git', 'fetch', 'origin'])
subprocess.check_call(['git', 'checkout', project.attrib['revision']])
# Generate a human readable submodule update message
num_commits = hash_distance(current_hash, project.attrib['revision'])
commit_count = ''
if num_commits == 1:
commit_count = ' (1 commit)'
raw_msg = subprocess.check_output([
'git', 'log', '--format=%B', '-n', '1', project.attrib['revision']
]).decode('utf-8').strip()
# Replace commit labels (like: Change-Id: ... with Original-Change-Id: ...)
filtered_msg = re.sub(
r'^([a-zA-Z-_]*: .*$)',
r'Original-\1',
raw_msg,
flags=re.MULTILINE)
commit_message.append('[roll] Roll %s' % filtered_msg)
elif num_commits > 1:
commit_count = ' (%d commits)' % num_commits
update_report.append(
'%s %s..%s%s' %
(project.attrib['name'], short_hash(current_hash),
short_hash(project.attrib['revision']), commit_count))
finally:
os.chdir(savedDir)
def update_from_manifest(manifest_path, commit_message):
"""Iterates through each project in a manifest calling update_from_project for each one.
The top level XML format is:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<projects>
<project ... />
<project ... />
</projects>
</manifest>
"""
print('Parsing %s' % manifest_path)
flower_tree = ElementTree.parse(manifest_path)
root = flower_tree.getroot()
update_report = []
print('Checking through projects...')
for project in root.findall('./projects/project'):
update_from_project(project, update_report, commit_message)
return update_report
def sync_prebuilts(integration_dir, prebuilts_paths, desired_prebuilts):
print(f'Parsing {", ".join(prebuilts_paths)}')
with open(os.path.join(ROOT_DIR, 'cobalt.ensure'), 'w') as ensure:
prebuilts_trees = {}
roots = {}
for path in prebuilts_paths:
tree = ElementTree.parse(os.path.join(integration_dir, path))
prebuilts_trees[path] = tree
roots[path] = tree.getroot()
ensure.write("""
# Copyright 2017 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# THIS FILE IS AUTO-GENERATED BY tools/sync_with_fuchsia.py DO NOT EDIT DIRECTLY.
""")
current_subdir = ''
for prebuilt in desired_prebuilts:
subdir, package_name = prebuilt.split(':', 1)
if current_subdir != subdir:
current_subdir = subdir
ensure.write(f'\n@Subdir {subdir}\n')
found = False
for root in roots.values():
for package in root.findall('./packages/package'):
if package.attrib['name'] in package_name:
ensure.write(
f'{package.attrib["name"]} {package.attrib["version"]}\n')
found = True
if not found:
sys.exit(f'Could not find {package_name} in the fuchsia prebuilts file')
ensure.write('\n\n')
ensure.write('#######################################################\n')
ensure.write('# Below is copied from tools/cobalt.ensure.exceptions #\n')
ensure.write('#######################################################\n')
with open(os.path.join(ROOT_DIR, 'tools/cobalt.ensure.exceptions'),
'r') as ensure_others:
ensure.write(ensure_others.read())
def sync_from_integration(integration_repo, manifest_files, prebuilts_files,
desired_prebuilts, make_commit):
"""Checks out an integration repository and calls update_from_manifest on each manifest_file.
If make_commit is true, the resulting submodule change will be added to a
commit.
"""
with tempfile.TemporaryDirectory(
prefix='fuchsia-integration-') as integration_dir:
print('Checking out integration repo...')
subprocess.check_call(['git', 'clone', integration_repo, integration_dir])
print()
sync_prebuilts(integration_dir, prebuilts_files, desired_prebuilts)
update_report = []
commit_message = []
for manifest_file in manifest_files:
update_report.extend(
update_from_manifest(
os.path.join(integration_dir, manifest_file), commit_message))
commit_msg = ''
if len(update_report) > 1:
commit_msg = """[roll] Roll %d submodules
%s""" % (len(update_report), '\n'.join(sorted(update_report)))
elif len(update_report) == 1:
if len(commit_message) == 1:
commit_msg = commit_message[0]
else:
commit_msg = '[roll] Roll %s' % update_report[0]
if commit_msg != '':
if make_commit:
subprocess.check_call(['git', 'commit', '-am', commit_msg])
else:
print()
print()
print(commit_msg)
print()
print()
def sync_files_from_fuchsia(fuchsia_repo, to_copy, retain_paths):
"""Checks out the fuchsia repo and copies specified files."""
with tempfile.TemporaryDirectory(prefix='fuchsia-repo-') as fuchsia_dir:
print('Checking out fuchsia repo...')
subprocess.check_call(
['git', 'clone', '--depth', '1', fuchsia_repo, fuchsia_dir])
print()
retain_paths = [os.path.join(ROOT_DIR, path) for path in retain_paths]
for filename in to_copy:
print(f'Copying {filename}')
f = os.path.join(fuchsia_dir, filename)
t = os.path.join(ROOT_DIR, filename)
if os.path.isdir(f):
for path in os.listdir(t):
fname = os.path.join(t, path)
if fname not in retain_paths:
if os.path.isdir(fname):
shutil.rmtree(fname)
else:
os.remove(fname)
shutil.copytree(f, t, dirs_exist_ok=True)
else:
shutil.copy2(f, t)
DEFAULT_FUCHSIA_REPO = 'http://fuchsia.googlesource.com/fuchsia'
DEFAULT_INTEGRATION_REPO = 'http://fuchsia.googlesource.com/integration'
DEFAULT_PREBUILTS_FILES = ['prebuilts', 'toolchain']
DEFAULT_TO_COPY_FROM_FUCHSIA = [
'third_party/googletest/BUILD.gn', 'third_party/boringssl'
]
DEFAULT_RETAIN_PATHS = ['third_party/boringssl/src']
DEFAULT_MANIFEST_FILES = ['third_party/flower']
DEFAULT_DESIRED_PREBUILTS = [
r':fuchsia/dart-sdk/${platform}',
r':fuchsia/third_party/clang/${platform}',
r':fuchsia/third_party/rust/${platform}',
r':fuchsia/third_party/sysroot/linux',
r':infra/3pp/tools/cpython3/${platform}',
r'bin:fuchsia/third_party/ninja/${platform}',
r'bin:fuchsia/tools/buildidtool/${platform}',
r'golang:fuchsia/go/${platform}',
r'goma:fuchsia/third_party/goma/client/${platform}',
]
def sync(integration_repo=DEFAULT_INTEGRATION_REPO,
fuchsia_repo=DEFAULT_FUCHSIA_REPO,
manifest_files=DEFAULT_MANIFEST_FILES,
to_copy_from_fuchsia=DEFAULT_TO_COPY_FROM_FUCHSIA,
retain_paths=DEFAULT_RETAIN_PATHS,
prebuilts_files=DEFAULT_PREBUILTS_FILES,
desired_prebuilts=DEFAULT_DESIRED_PREBUILTS,
make_commit=False):
sync_files_from_fuchsia(fuchsia_repo, to_copy_from_fuchsia, retain_paths)
sync_from_integration(integration_repo, manifest_files, prebuilts_files,
desired_prebuilts, make_commit)
if __name__ == '__main__':
sync()