| #!/usr/bin/env python3 | |
| # Copyright (C) 2021 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. | |
| """Packages prebuilts and SDK sources for GitHub releases. | |
| Usage: ./tools/release/package-github-release-artifacts v20.0 | |
| IMPORTANT: This script must be run from the git tag being packaged. The SDK | |
| source files are generated from the current working directory, so running this | |
| script from a different commit will result in mismatched SDK sources. | |
| This will generate: | |
| - One .zip file for every os-arch combo (e.g. android-arm.zip) | |
| - SDK source zips (perfetto-cpp-sdk-src.zip, perfetto-c-sdk-src.zip) | |
| All files will be placed into /tmp/perfetto-v20.0-github-release/ . | |
| """ | |
| import argparse | |
| import os | |
| import subprocess | |
| import sys | |
| # Expected LUCI artifact manifest. Must stay in sync with | |
| # infra/luci/recipes/perfetto.py — ARTIFACTS + the platform list in | |
| # RunSteps. Verified after rsync; a mismatch (missing OR extra files) is a | |
| # hard error because it means LUCI's output changed and this script needs to | |
| # be updated. | |
| # | |
| # On Windows, each binary is accompanied by a <name>.pdb (debug symbols), | |
| # expressed as 'windows_pdb': True per artifact. Platform filters match the | |
| # 'exclude_platforms' / 'include_platforms' keys in the recipe. | |
| _ARTIFACTS = [ | |
| { | |
| 'name': 'trace_processor_shell' | |
| }, | |
| { | |
| 'name': 'traceconv' | |
| }, | |
| { | |
| 'name': 'tracebox', | |
| 'exclude_platforms': ['windows-amd64'], | |
| }, | |
| { | |
| 'name': 'perfetto' | |
| }, | |
| { | |
| 'name': 'traced' | |
| }, | |
| { | |
| 'name': 'traced_probes', | |
| 'exclude_platforms': ['windows-amd64'], | |
| }, | |
| { | |
| 'name': 'heapprofd_glibc_preload', | |
| 'file': 'libheapprofd_glibc_preload.so', | |
| 'include_platforms': ['linux-amd64', 'linux-arm', 'linux-arm64'], | |
| }, | |
| ] | |
| _PLATFORMS = [ | |
| 'android-arm', | |
| 'android-arm64', | |
| 'android-x64', | |
| 'android-x86', | |
| 'linux-amd64', | |
| 'linux-arm', | |
| 'linux-arm64', | |
| 'mac-amd64', | |
| 'mac-arm64', | |
| 'windows-amd64', | |
| ] | |
| _SDK_ZIPS = [ | |
| 'perfetto-cpp-sdk-src.zip', | |
| 'perfetto-c-sdk-src.zip', | |
| ] | |
| def exec(*args): | |
| print(' '.join(args)) | |
| subprocess.check_call(args) | |
| def _artifact_filename(artifact, platform): | |
| """Returns the on-disk filename LUCI uploads for `artifact` on `platform`.""" | |
| base = artifact.get('file', artifact['name']) | |
| if platform == 'windows-amd64' and 'file' not in artifact: | |
| return base + '.exe' | |
| return base | |
| def _expected_manifest(): | |
| """Returns {platform: set(filenames)} for all LUCI-produced binaries.""" | |
| manifest = {p: set() for p in _PLATFORMS} | |
| for platform in _PLATFORMS: | |
| for artifact in _ARTIFACTS: | |
| if platform in artifact.get('exclude_platforms', []): | |
| continue | |
| include = artifact.get('include_platforms') | |
| if include is not None and platform not in include: | |
| continue | |
| fname = _artifact_filename(artifact, platform) | |
| manifest[platform].add(fname) | |
| if platform == 'windows-amd64': | |
| manifest[platform].add(fname + '.pdb') | |
| return manifest | |
| def verify_downloads(tmpdir): | |
| """Verifies the rsynced tree matches the expected LUCI manifest. | |
| Fails loudly on anything missing or unexpected — the manifest here must | |
| match LUCI, and drift in either direction should surface immediately. | |
| """ | |
| manifest = _expected_manifest() | |
| expected_dirs = set(manifest.keys()) | {'sdk'} | |
| actual_dirs = { | |
| d for d in os.listdir(tmpdir) if os.path.isdir(os.path.join(tmpdir, d)) | |
| } | |
| errors = [] | |
| missing_dirs = expected_dirs - actual_dirs | |
| unexpected_dirs = actual_dirs - expected_dirs | |
| if missing_dirs: | |
| errors.append('Missing platform directories: %s' % | |
| ', '.join(sorted(missing_dirs))) | |
| if unexpected_dirs: | |
| errors.append('Unexpected directories under GCS path: %s' % | |
| ', '.join(sorted(unexpected_dirs))) | |
| for platform, expected_files in manifest.items(): | |
| pdir = os.path.join(tmpdir, platform) | |
| if not os.path.isdir(pdir): | |
| continue | |
| actual_files = set(os.listdir(pdir)) | |
| missing = expected_files - actual_files | |
| extra = actual_files - expected_files | |
| if missing: | |
| errors.append('%s: missing binaries: %s' % | |
| (platform, ', '.join(sorted(missing)))) | |
| if extra: | |
| errors.append('%s: unexpected binaries: %s' % | |
| (platform, ', '.join(sorted(extra)))) | |
| sdk_dir = os.path.join(tmpdir, 'sdk') | |
| if os.path.isdir(sdk_dir): | |
| actual_sdk = set(os.listdir(sdk_dir)) | |
| expected_sdk = set(_SDK_ZIPS) | |
| missing_sdk = expected_sdk - actual_sdk | |
| extra_sdk = actual_sdk - expected_sdk | |
| if missing_sdk: | |
| errors.append('sdk: missing zips: %s' % ', '.join(sorted(missing_sdk))) | |
| if extra_sdk: | |
| errors.append('sdk: unexpected files: %s' % ', '.join(sorted(extra_sdk))) | |
| if errors: | |
| print('\n'.join('ERROR: ' + e for e in errors), file=sys.stderr) | |
| print( | |
| '\nThe LUCI artifact manifest in this script is out of sync with ' | |
| 'infra/luci/recipes/perfetto.py. Update _ARTIFACTS / _PLATFORMS / ' | |
| '_SDK_ZIPS above and re-run.', | |
| file=sys.stderr) | |
| return False | |
| print('✓ All expected LUCI artifacts present across %d platforms + sdk.' % | |
| len(_PLATFORMS)) | |
| return True | |
| def verify_git_state(expected_version, assume_yes=False): | |
| """Verifies git is on the correct tag with no uncommitted changes.""" | |
| warnings = [] | |
| try: | |
| result = subprocess.run(['git', 'status', '--porcelain'], | |
| capture_output=True, | |
| text=True) | |
| if result.returncode == 0 and result.stdout.strip(): | |
| warnings.append( | |
| f'Working directory has uncommitted changes:\n{result.stdout}') | |
| except Exception as e: | |
| warnings.append(f'Could not check git status: {e}') | |
| try: | |
| result = subprocess.run(['git', 'describe', '--exact-match', '--tags'], | |
| capture_output=True, | |
| text=True) | |
| if result.returncode == 0: | |
| current_tag = result.stdout.strip() | |
| if current_tag != expected_version: | |
| warnings.append( | |
| f'On tag {current_tag}, but packaging {expected_version}') | |
| else: | |
| warnings.append(f'Not on a git tag (expected {expected_version})') | |
| except Exception as e: | |
| warnings.append(f'Could not check git tag: {e}') | |
| if warnings: | |
| print('WARNING: SDK sources may not match the release tag:') | |
| for warning in warnings: | |
| print(f' - {warning}') | |
| if assume_yes: | |
| print('\n--yes passed; proceeding despite warnings.') | |
| return True | |
| return input('\nContinue anyway? [y/N] ').lower().strip() in ['y', 'yes'] | |
| print(f'✓ On tag {expected_version} with clean working directory') | |
| return True | |
| def main(): | |
| parser = argparse.ArgumentParser(epilog='Example: %s v19.0' % __file__) | |
| parser.add_argument('version', help='Version tag (e.g., v20.0)') | |
| parser.add_argument( | |
| '--yes', | |
| action='store_true', | |
| help='Skip all interactive confirmations (for CI use).') | |
| args = parser.parse_args() | |
| if not verify_git_state(args.version, assume_yes=args.yes): | |
| print('Aborted.') | |
| return 1 | |
| tmpdir = '/tmp/perfetto-%s-github-release' % args.version | |
| src = 'gs://perfetto-luci-artifacts/%s/' % args.version | |
| os.makedirs(tmpdir, exist_ok=True) | |
| print('--- Downloading prebuilts from GCS ---') | |
| os.chdir(tmpdir) | |
| exec('gsutil', '-m', 'rsync', '-rc', src, tmpdir + '/') | |
| print('\n--- Verifying artifact manifest ---') | |
| if not verify_downloads(tmpdir): | |
| return 1 | |
| zips = [] | |
| for arch in sorted(os.listdir(tmpdir)): | |
| if arch == 'sdk' or not os.path.isdir(os.path.join(tmpdir, arch)): | |
| continue | |
| exec('zip', '-9r', '%s.zip' % arch, arch) | |
| zips.append(arch + '.zip') | |
| # SDK zips already landed under sdk/ via the rsync above; just move them | |
| # up so everything ready to upload sits in tmpdir's root. | |
| for zip_name in _SDK_ZIPS: | |
| src_path = os.path.join(tmpdir, 'sdk', zip_name) | |
| dst_path = os.path.join(tmpdir, zip_name) | |
| os.rename(src_path, dst_path) | |
| zips.append(zip_name) | |
| print('') | |
| print('=' * 70) | |
| print('%d zip files saved in %s' % (len(zips), tmpdir)) | |
| print('Prebuilt binaries: %d' % (len(zips) - len(_SDK_ZIPS))) | |
| print('SDK sources: %d' % len(_SDK_ZIPS)) | |
| print('Files: %s' % ', '.join(sorted(zips))) | |
| print('=' * 70) | |
| if __name__ == '__main__': | |
| sys.exit(main()) |