blob: 3197f5d56df322e6d1e54e242f49c6221bf2da11 [file]
#!/usr/bin/python3
import argparse
import glob
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import zipfile
from collections import defaultdict
from pathlib import Path
# See go/fetch_artifact for details on this script.
FETCH_ARTIFACT = '/google/data/ro/projects/android/fetch_artifact'
COMPAT_REPO = Path('prebuilts/sdk')
COMPAT_README = Path('extensions/README.md')
# This build target is used when fetching from a train build (TXXXXXXXX)
BUILD_TARGET_TRAIN = 'train_build'
# This build target is used when fetching from a non-train build (XXXXXXXX)
BUILD_TARGET_CONTINUOUS = 'mainline_modules_sdks-userdebug'
BUILD_TARGET_CONTINUOUS_MAIN = 'mainline_modules_sdks-{release_config}-userdebug'
# The glob of sdk artifacts to fetch from remote build
ARTIFACT_PATTERN = 'mainline-sdks/for-next-build/current/{module_name}/sdk/*.zip'
# The glob of sdk artifacts to fetch from local build
ARTIFACT_LOCAL_PATTERN = 'out/dist/mainline-sdks/for-next-build/current/{module_name}/sdk/*.zip'
ARTIFACT_MODULES_INFO = 'mainline-modules-info.json'
ARTIFACT_LOCAL_MODULES_INFO = 'out/dist/mainline-modules-info.json'
COMMIT_TEMPLATE = """Finalize artifacts for extension SDK %d
Import from build id %s.
Generated with:
$ %s
Bug: %d
Test: presubmit"""
def fail(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
sys.exit(1)
def fetch_artifacts(build_id, target, artifact_path, dest):
print('Fetching %s from %s ...' % (artifact_path, target))
fetch_cmd = [FETCH_ARTIFACT]
fetch_cmd.extend(['--bid', str(build_id)])
fetch_cmd.extend(['--target', target])
fetch_cmd.append(artifact_path)
fetch_cmd.append(str(dest))
print("Running: " + ' '.join(fetch_cmd))
try:
subprocess.check_output(fetch_cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
fail(
'FAIL: Unable to retrieve %s artifact for build ID %s for %s target\n Error: %s'
% (artifact_path, build_id, target, e.output.decode())
)
def fetch_mainline_modules_info_artifact(target, build_id):
tmpdir = Path(tempfile.TemporaryDirectory().name)
tmpdir.mkdir()
if args.local_mode:
artifact_path = ARTIFACT_LOCAL_MODULES_INFO
print('Copying %s to %s ...' % (artifact_path, tmpdir))
shutil.copy(artifact_path, tmpdir)
else:
artifact_path = ARTIFACT_MODULES_INFO
fetch_artifacts(build_id, target, artifact_path, tmpdir)
return tmpdir / ARTIFACT_MODULES_INFO
def fetch_module_sdk_artifacts(target, build_id, module_name):
tmpdir = Path(tempfile.TemporaryDirectory().name)
tmpdir.mkdir()
if args.local_mode:
artifact_path = ARTIFACT_LOCAL_PATTERN.format(module_name='*')
print('Copying %s to %s ...' % (artifact_path, tmpdir))
for file in glob.glob(artifact_path):
shutil.copy(file, tmpdir)
else:
artifact_path = ARTIFACT_PATTERN.format(module_name=module_name)
fetch_artifacts(build_id, target, artifact_path, tmpdir)
return tmpdir
def repo_for_sdk(sdk_filename, mainline_modules_info):
for module in mainline_modules_info:
if mainline_modules_info[module]["sdk_name"] in sdk_filename:
project_path = Path(mainline_modules_info[module]["module_sdk_project"])
if args.gantry_download_dir:
project_path = args.gantry_download_dir / project_path
os.makedirs(project_path , exist_ok = True, mode = 0o777)
print(f"module_sdk_path for {module}: {project_path}")
return project_path
fail('"%s" has no valid mapping to any mainline module.' % sdk_filename)
def dir_for_sdk(filename, version):
base = str(version)
if 'test-exports' in filename:
return os.path.join(base, 'test-exports')
if 'host-exports' in filename:
return os.path.join(base, 'host-exports')
return base
def is_ignored(file):
# Conscrypt has some legacy API tracking files that we don't consider for extensions.
bad_stem_prefixes = ['conscrypt.module.intra.core.api', 'conscrypt.module.platform.api']
return any([file.stem.startswith(p) for p in bad_stem_prefixes])
def maybe_tweak_compat_stem(file):
# For legacy reasons, art and conscrypt txt file names in the SDKs (*.module.public.api)
# do not match their expected filename in prebuilts/sdk (art, conscrypt). So rename them
# to match.
new_stem = file.stem
new_stem = new_stem.replace('art.module.public.api', 'art')
new_stem = new_stem.replace('conscrypt.module.public.api', 'conscrypt')
# The stub jar artifacts from official builds are named '*-stubs.jar', but
# the convention for the copies in prebuilts/sdk is just '*.jar'. Fix that.
new_stem = new_stem.replace('-stubs', '')
return file.with_stem(new_stem)
parser = argparse.ArgumentParser(description=('Finalize an extension SDK with prebuilts'))
parser.add_argument('-a', '--amend_last_commit', action="store_true", help='Amend current HEAD commits instead of making new commits.')
parser.add_argument('-b', '--bug', type=int, required=True, help='The bug number to add to the commit message.')
parser.add_argument('-c', '--release_config', type=str, help='The release config to use to finalize.')
parser.add_argument('-d', '--dry_run', action='store_true', help='Leaves git and repo out it')
parser.add_argument('-f', '--finalize_sdk', type=int, required=True, help='The numbered SDK to finalize.')
# This flag is only required when executed via Gantry. It points to the downloaded directory to be used.
parser.add_argument('-g', '--gantry_download_dir', type=str, help=argparse.SUPPRESS)
parser.add_argument('-l', '--local_mode', action="store_true", help='Local mode: use locally built artifacts and don\'t upload the result to Gerrit.')
parser.add_argument('-m', '--modules', action='append', help='Modules to include. Can be provided multiple times, or not at all for all modules.')
parser.add_argument('-r', '--readme', required=True, help='Version history entry to add to %s' % (COMPAT_REPO / COMPAT_README))
parser.add_argument('-t', '--topic_branch', type=str, help='Name of the topic branch "repo start" will create.')
parser.add_argument('--build_target', type=str, help=f'Which build target to download targets from (e.g. "{BUILD_TARGET_CONTINUOUS}"); used together with the <bid> argument. If not provided, will calculate a default value based on the <release_config> argument.')
parser.add_argument('bid', help='Build server build ID')
args = parser.parse_args()
if not os.path.isdir('build/soong') and not args.gantry_download_dir:
fail("This script must be run from the top of an Android source tree.")
if args.build_target:
BUILD_TARGET_CONTINUOUS = args.build_target
elif args.release_config:
BUILD_TARGET_CONTINUOUS = BUILD_TARGET_CONTINUOUS_MAIN.format(release_config=args.release_config)
build_target = BUILD_TARGET_TRAIN if args.bid[0] == 'T' else BUILD_TARGET_CONTINUOUS
topic_branch = 'finalize-%d' % args.finalize_sdk if args.topic_branch is None else args.topic_branch
cmdline = shlex.join([x for x in sys.argv if x not in ['-a', '--amend_last_commit', '-l', '--local_mode']])
commit_message = COMMIT_TEMPLATE % (args.finalize_sdk, args.bid, cmdline, args.bug)
module_names = args.modules or ['*']
if args.gantry_download_dir:
args.gantry_download_dir = Path(args.gantry_download_dir)
COMPAT_REPO = args.gantry_download_dir / COMPAT_REPO
mainline_modules_info_file = args.gantry_download_dir / ARTIFACT_MODULES_INFO
else:
mainline_modules_info_file = fetch_mainline_modules_info_artifact(build_target, args.bid)
compat_dir = COMPAT_REPO.joinpath('extensions/%d' % args.finalize_sdk)
if compat_dir.is_dir():
print('Removing existing dir %s' % compat_dir)
shutil.rmtree(compat_dir)
created_dirs = defaultdict(set)
with open(mainline_modules_info_file, "r", encoding="utf8",) as file:
mainline_modules_info = json.load(file)
for m in module_names:
if args.gantry_download_dir:
tmpdir = args.gantry_download_dir / "sdk_artifacts"
else:
tmpdir = fetch_module_sdk_artifacts(build_target, args.bid, m)
for f in tmpdir.iterdir():
repo = repo_for_sdk(f.name, mainline_modules_info)
dir = dir_for_sdk(f.name, args.finalize_sdk)
target_dir = repo.joinpath(dir)
if target_dir.is_dir():
print('Removing existing dir %s' % target_dir)
shutil.rmtree(target_dir)
with zipfile.ZipFile(tmpdir.joinpath(f)) as zipFile:
zipFile.extractall(target_dir)
# Disable the Android.bp, but keep it for reference / potential future use.
shutil.move(target_dir.joinpath('Android.bp'), target_dir.joinpath('Android.bp.auto'))
print('Created %s' % target_dir)
created_dirs[repo].add(dir)
# Copy api txt files to compat tracking dir
src_files = [Path(p) for p in glob.glob(os.path.join(target_dir, 'sdk_library/*/*.txt')) + glob.glob(os.path.join(target_dir, 'sdk_library/*/*.jar'))]
for src_file in src_files:
if is_ignored(src_file):
continue
api_type = src_file.parts[-2]
dest_dir = compat_dir.joinpath(api_type, 'api') if src_file.suffix == '.txt' else compat_dir.joinpath(api_type)
dest_file = maybe_tweak_compat_stem(dest_dir.joinpath(src_file.name))
os.makedirs(dest_dir, exist_ok = True)
shutil.copy(src_file, dest_file)
created_dirs[COMPAT_REPO].add(dest_dir.relative_to(COMPAT_REPO))
if args.local_mode:
print('Updated prebuilts using locally built artifacts. Don\'t submit or use for anything besides local testing.')
sys.exit(0)
# Do not commit any changes when the script is executed via Gantry.
if args.gantry_download_dir:
sys.exit(0)
if args.dry_run:
sys.exit(0)
subprocess.check_output(['repo', 'start', topic_branch] + list(created_dirs.keys()))
print('Running git commit')
for repo in created_dirs:
git = ['git', '-C', str(repo)]
subprocess.check_output(git + ['add'] + list(created_dirs[repo]))
if repo == COMPAT_REPO:
with open(COMPAT_REPO / COMPAT_README, "a") as readme:
readme.write(f"- {args.finalize_sdk}: {args.readme}\n")
subprocess.check_output(git + ['add', COMPAT_README])
if args.amend_last_commit:
change_id_match = re.search(r'Change-Id: [^\\n]+', str(subprocess.check_output(git + ['log', '-1'])))
if change_id_match:
change_id = '\n' + change_id_match.group(0)
else:
fail('FAIL: Unable to find change_id of the last commit.')
subprocess.check_output(git + ['commit', '--amend', '-m', commit_message + change_id])
else:
subprocess.check_output(git + ['commit', '-m', commit_message])