blob: 15b282f49b9935a42cce03b08f87f95024e6a504 [file] [log] [blame]
#
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Package to manage LLVM sources when building a toolchain.
"""
import logging
from pathlib import Path
from typing import List, Optional
import os
import re
import shutil
import string
import subprocess
import sys
import android_version
from toolchain_errors import ToolchainErrorCode, ToolchainError
import hosts
import paths
import utils
def logger():
"""Returns the module level logger."""
return logging.getLogger(__name__)
def apply_patches(source_dir, svn_version, patch_json, patch_dir, git_am,
failure_mode):
"""Apply patches in $patch_dir/$patch_json to $source_dir.
Invokes external/toolchain-utils/llvm_tools/patch_manager.py to apply the
patches.
"""
assert failure_mode, "Invalid failure_mode"
patch_manager_cmd = [
sys.executable,
str(paths.TOOLCHAIN_UTILS_DIR / 'llvm_tools' / 'patch_manager.py'),
'--svn_version', str(svn_version),
'--patch_metadata_file', str(patch_json),
'--src_path', str(source_dir),
'--failure_mode', failure_mode
]
patch_dir = os.getcwd()
if git_am:
patch_manager_cmd.append('--git_am')
"""Run git am in the source directory"""
patch_dir=source_dir
return utils.check_output(patch_manager_cmd, cwd=patch_dir)
class PatchInfo:
"""Holds info for a round of patch applications."""
def __init__(self) -> None:
self.applied_patches = []
self.failed_patches = []
self.inapplicable_patches = []
@staticmethod
def get_subject(patch_file: Path) -> str:
contents = patch_file.read_text()
# Parse patch generated by `git format-patch`.
matches = re.search('Subject: (.*)\n', contents)
if matches:
subject = matches.group(1)
trim_str = '[PATCH] '
if subject.startswith(trim_str):
subject = subject[len(trim_str):]
return subject
# Parse patch generated by `git show`. The format is used by patches synced from chromiumos.
matches = re.search(r'^Date:.+\n\s*\n\s*(.+)', contents, re.MULTILINE)
assert matches, f'failed to parse subject from {patch_file}'
subject = matches.group(1)
return subject
@staticmethod
def format_patch_line(patch_file: Path) -> str:
url_prefix = 'https://android.googlesource.com/toolchain/llvm_android/+/' +\
'{{scripts_sha}}'
assert patch_file.is_file(), f"patch file doesn't exist: {patch_file}"
patch_name = patch_file.name
if re.match('([0-9a-f]+)(_v[0-9]+)?\.patch$', patch_name):
url_suffix = '/patches/cherry/' + patch_name
link_text = PatchInfo.get_subject(patch_file)
else:
url_suffix = '/patches/' + patch_name
link_text = patch_name
return f'- [{link_text}]({url_prefix}{url_suffix})'
def write_source_info(source_dir: str, pi: PatchInfo) -> None:
output = []
base_revision = android_version.get_git_sha()
github_url = 'https://github.com/llvm/llvm-project/commits/' + base_revision
output.append(f'Base revision: [{base_revision}]({github_url})')
output.append('')
applied_patches = []
for patch in pi.applied_patches:
applied_patches.append(pi.format_patch_line(Path(patch)))
output.extend(sorted(applied_patches))
with open(paths.OUT_DIR / 'clang_source_info.md', 'w') as outfile:
outfile.write('\n'.join(output))
def get_source_info(source_dir: str, patch_output: str) -> PatchInfo:
pi = PatchInfo()
patches = patch_output.strip().splitlines()
patches_iter = iter(patches)
assert next(patches_iter) == 'The following patches applied successfully:'
applied = True # applied/successful patches
failed = False # failed patches
inapplicable = False # inapplicable patches
while True:
patch = next(patches_iter, None)
# We may optionally have an empty line followed by patches that were not
# applicable.
if patch == '':
ni = next(patches_iter)
if ni == 'The following patches were not applicable:':
applied = False
failed = False
inapplicable = True
continue
if ni == 'The following patches failed to apply:':
applied = False
failed = True
inapplicable = False
continue
elif patch is None:
break
print("patch={},{},{}$$".format(patch, type(patch), len(patch)))
assert patch.endswith('.patch')
if applied:
pi.applied_patches.append(patch)
if failed:
pi.failed_patches.append(patch)
if inapplicable:
pi.inapplicable_patches.append(patch)
return pi
def setup_sources(git_am=False, llvm_rev=None, skip_apply_patches=False, continue_on_patch_errors=False) -> Optional[ToolchainError]:
"""Setup toolchain sources into paths.LLVM_PATH.
Copy toolchain/llvm-project into paths.LLVM_PATH or clone from upstream.
Apply patches per the specification in
toolchain/llvm_android/patches/PATCHES.json. The function overwrites
paths.LLVM_PATH only if necessary to avoid recompiles during incremental builds.
"""
# Return the error messages upon failure.
ret: Optional[ToolchainError] = []
source_dir = paths.LLVM_PATH
tmp_source_dir = source_dir.parent / (source_dir.name + '.tmp')
if os.path.exists(tmp_source_dir):
shutil.rmtree(tmp_source_dir)
# mkdir parent of tmp_source_dir if necessary.
tmp_source_parent = os.path.dirname(tmp_source_dir)
if not os.path.exists(tmp_source_parent):
os.makedirs(tmp_source_parent)
if not llvm_rev:
# Copy llvm source tree to a temporary directory.
copy_from = paths.TOOLCHAIN_LLVM_PATH
logger().info(f'No llvm revision provided, copying from {copy_from}')
# Use 'cp' instead of shutil.copytree. The latter uses copystat and retains
# timestamps from the source. We instead use rsync below to only update
# changed files into source_dir. Using 'cp' will ensure all changed files
# get a newer timestamp than files in $source_dir.
# Note: Darwin builds don't copy symlinks with -r. Use -R instead.
reflink = '--reflink=auto' if hosts.build_host().is_linux else '-c'
try:
cmd = ['cp', '-Rf', reflink, copy_from, tmp_source_dir]
subprocess.check_call(cmd)
except subprocess.CalledProcessError:
# Fallback to normal copy.
cmd = ['cp', '-Rf', copy_from, tmp_source_dir]
subprocess.check_call(cmd)
if git_am:
# To avoid clobbering the source tree's git objects, remove
# out/llvm-project/.git and copy .repo/projects/toolchain/llvm-project.git there
tmp_out_git_dir = tmp_source_dir / '.git'
copy_from_git = os.path.abspath(os.path.join(tmp_source_dir, os.readlink(tmp_out_git_dir)))
cmd = ['rm', '-rf', tmp_out_git_dir]
subprocess.check_call(cmd)
cmd = ['cp', '-Rf', '-L', copy_from_git, tmp_out_git_dir]
subprocess.check_call(cmd)
else:
logger().info(f'Fetching {llvm_rev} from https://github.com/llvm/llvm-project.git')
if not os.path.exists(tmp_source_dir):
os.makedirs(tmp_source_dir)
with utils.chdir_context(tmp_source_dir):
cmd = ['git', 'init']
subprocess.check_call(cmd)
cmd = ['git', 'remote', 'add', 'origin', 'https://github.com/llvm/llvm-project.git']
subprocess.check_call(cmd)
cmd = ['git', 'fetch', '--depth=1', 'origin', llvm_rev]
subprocess.check_call(cmd)
cmd = ['git', 'reset', '--hard', 'FETCH_HEAD']
subprocess.check_call(cmd)
# patch source tree
patch_dir = paths.SCRIPTS_DIR / 'patches'
patch_json = os.path.join(patch_dir, 'PATCHES.json')
svn_version = android_version.get_svn_revision_number()
if not skip_apply_patches:
failure_mode = 'continue' if continue_on_patch_errors else 'fail'
patch_output = apply_patches(tmp_source_dir, svn_version, patch_json,
patch_dir, git_am, failure_mode)
logger().info(patch_output)
pi = get_source_info(tmp_source_dir, patch_output)
write_source_info(tmp_source_dir, pi)
ret = ToolchainError(ToolchainErrorCode.PATCH_ERROR, str(pi.failed_patches))
# Copy tmp_source_dir to source_dir if they are different. This avoids
# invalidating prior build outputs.
if not os.path.exists(source_dir):
os.rename(tmp_source_dir, source_dir)
else:
# Without a trailing '/' in $SRC, rsync copies $SRC to
# $DST/BASENAME($SRC) instead of $DST.
tmp_source_dir_str = str(tmp_source_dir) + '/'
# rsync to update only changed files. Use '-c' to use checksums to find
# if files have changed instead of only modification time and size -
# which could have inconsistencies. Use '--delete' to ensure files not
# in tmp_source_dir are deleted from $source_dir.
subprocess.check_call(['rsync', '-r', '--delete', '--links', '-c',
tmp_source_dir_str, source_dir])
shutil.rmtree(tmp_source_dir)
remote, url = try_set_git_remote(source_dir)
logger().info(f'git remote url: remote: {remote} url: {url}')
return ret
def try_set_git_remote(source_dir):
AOSP_URL = 'https://android.googlesource.com/toolchain/llvm-project'
def get_git_remote_url(remote=None):
try:
if not remote:
remote = utils.check_output([
'git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name',
'@{upstream}'
]).strip()
remote = remote.split('/')[0]
url = utils.check_output(['git', 'remote', 'get-url',
remote]).strip()
return (remote, url)
except:
return (remote, None)
git_dir = source_dir / '.git'
if not git_dir.is_dir():
return (None, None)
with utils.chdir_context(git_dir):
remote, url = get_git_remote_url()
if url != AOSP_URL:
if not remote:
remote = 'origin'
try:
if Path('config').is_symlink():
link = utils.check_output(['readlink', 'config']).strip()
utils.check_call(['rm', '-f', 'config'])
utils.check_call(['cp', link, 'config'])
if remote:
utils.check_call(
['git', 'remote', 'set-url', remote, AOSP_URL])
else:
utils.check_call(['git', 'remote', 'add', remote, AOSP_URL])
remote, url = get_git_remote_url(remote)
except:
pass
return (remote, url)