blob: f36bc355450a672441c322a7d44b06247bf0e580 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Downloads symbols for official, Google Chrome builds.
Usage:
./download_symbols.py -v 91.0.4449.6 -a x86_64 -o /dest/path
This can also be used as a Python module.
"""
import argparse
import json
import os.path
import platform
import re
import shutil
import subprocess
import sys
import urllib.request as request
_VERSION_HISTORY = 'https://versionhistory.googleapis.com/v1/chrome/platforms/{platform}/channels/all/versions'
_CHANNEL_REGEX = r'channels/(\w+)/versions'
_DSYM_URL_TEMPLATE = 'https://dl.google.com/chrome/mac/{channel}/dsym/googlechrome-{version}-{arch}-dsym.tar.bz2'
def download_chrome_symbols(version, channel, arch, dest_dir):
"""Downloads and extracts the official Google Chrome dSYM files to a
subdirectory of `dest_dir`.
Args:
version: The version to download symbols for.
channel: The release channel (extended, stable, beta, dev, canary,
canary_asan) to download symbols for. If None, attempts to
guess the channel.
arch: The CPU architecture (x86_64, arm64 / aarch64) to download
symbols for.
dest_dir: The location to download symbols to. The dSYMs will be
extracted to a subdirectory of this directory.
Returns:
The path to the directory containing the dSYMs, which will be a
subdirectory of `dest_dir`, or None if there is an error.
"""
if channel is None:
channel = _identify_channel(version, arch)
if channel:
print(
f'Using release channel "{channel}" for {version}',
file=sys.stderr)
else:
print(
f'Could not identify channel for Chrome version {version}',
file=sys.stderr)
return None
# The symbol storage uses "arm64" rather than "aarch64".
if arch == 'aarch64':
arch = 'arm64'
try:
return _download_and_extract(version, channel, arch, dest_dir)
except Exception as err:
print(f'Could not find dSYMs for Chrome {version} {arch}: '
f'{err}',
file=sys.stderr)
return None
def get_symbol_directory(version, channel, arch, dest_dir):
"""Returns the parent directory for dSYMs given the specified parameters."""
_, dest = _get_url_and_dest(version, channel, arch, dest_dir)
return dest
def _identify_channel(version, arch):
"""Attempts to guess the release channel given a Chrome version and CPU
architecture."""
# First try querying versionhistory for all versions across channels.
# https://developer.chrome.com/docs/web-platform/versionhistory/guide
# Query the correct platform in case a release was skipped on one arch.
# TODO(kbr): "early stable" releases might be breaking this algorithm.
platform = 'mac' if arch == 'x86_64' else 'mac_arm64'
formatted_platform = _VERSION_HISTORY.format(platform=platform)
with request.urlopen(formatted_platform) as history_resp:
history = json.loads(history_resp.read().decode('utf-8'))
for entry in history['versions']:
if entry['version'] == version:
match = re.search(_CHANNEL_REGEX, entry['name'])
if match:
return match[1]
# Fall back to sending HEAD HTTP requests to each of the possible symbol
# locations.
print(
f'Unable to identify release channel for {version}, '
'now brute-force searching',
file=sys.stderr)
for channel in ('extended', 'stable', 'beta', 'dev', 'canary',
'canary_asan'):
url, _ = _get_url_and_dest(version, channel, arch, '')
req = request.Request(url, method='HEAD')
try:
resp = request.urlopen(req)
if resp.code == 200:
return channel
except:
continue
return None
def _get_url_and_dest(version, channel, arch, dest_dir):
"""Returns a the symbol archive URL and local destination directory given
the format parameters."""
args = {'channel': channel, 'arch': arch, 'version': version}
url = _DSYM_URL_TEMPLATE.format(**args)
dest_dir = os.path.join(dest_dir,
'googlechrome-{version}-{arch}-dsym'.format(**args))
return url, dest_dir
def _download_and_extract(version, channel, arch, dest_dir):
"""Performs the download and extraction of the symbol files. Returns the
path to the extracted symbol files on success, raises on error.
"""
url, dest_dir = _get_url_and_dest(version, channel, arch, dest_dir)
remove_on_failure = False
if not os.path.isdir(dest_dir):
os.mkdir(dest_dir)
remove_on_failure = True
try:
with request.urlopen(url) as symbol_request:
print(
f'Downloading and extracting symbols to {dest_dir}',
file=sys.stderr)
print('This will take a minute...', file=sys.stderr)
_extract_symbols_to(symbol_request, dest_dir)
return dest_dir
except:
if remove_on_failure:
shutil.rmtree(dest_dir)
raise
def _extract_symbols_to(symbol_request, dest_dir):
"""Performs a streaming extract of the symbol files.
Args:
symbol_request: The HTTPResponse object for the symbol URL.
dest_dir: The destination directory into which the files will be
extracted.
Raises:
Exception if there is an error.
"""
proc = subprocess.Popen(['tar', 'xjf', '-'],
cwd=dest_dir,
stdin=subprocess.PIPE,
stdout=sys.stderr,
stderr=sys.stderr)
while True:
data = symbol_request.read(4096)
if not data:
proc.stdin.close()
break
proc.stdin.write(data)
proc.wait()
if proc.returncode != 0:
raise Exception(f"Untarring failed with exit code {proc.returncode}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--version', '-v', required=True, help='Version to download.')
parser.add_argument(
'--channel',
'-c',
choices=['stable', 'beta', 'dev', 'canary'],
help='Chrome release channel for the version The channel will be ' \
'guessed if not specified.'
)
parser.add_argument(
'--arch',
'-a',
choices=['aarch64', 'arm64', 'x86_64'],
help='CPU architecture to download. Defaults to that of the current OS.'
)
parser.add_argument(
'--out',
'-o',
required=True,
help='Directory to download the symbols to.')
args = parser.parse_args()
arch = args.arch
if not arch:
arch = platform.machine()
if not os.path.isdir(args.out):
print('--out destination is not a directory.', file=sys.stderr)
return False
return download_chrome_symbols(args.version, args.channel, arch, args.out)
if __name__ == '__main__':
sys.exit(0 if main() else 1)