| #!/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()) |