| #!/usr/bin/env python |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Installs and then runs cipd. |
| |
| This script installs cipd in ./tools/ and then executes it, passing through |
| all arguments. |
| """ |
| |
| from __future__ import print_function |
| |
| import hashlib |
| import httplib |
| import os |
| import platform |
| import subprocess |
| import sys |
| import urlparse |
| |
| |
| SCRIPT_DIR = os.path.dirname(__file__) |
| VERSION_FILE = os.path.join(SCRIPT_DIR, '.cipd_version') |
| DIGESTS_FILE = VERSION_FILE + '.digests' |
| # Put CIPD client in tools so that users can easily get it in their PATH. |
| CLIENT = os.path.join(SCRIPT_DIR, 'tools', 'cipd') |
| CIPD_HOST = 'chrome-infra-packages.appspot.com' |
| |
| |
| def platform_normalized(): |
| """Normalize platform into format expected in CIPD paths.""" |
| |
| try: |
| os_name = platform.system().lower() |
| return { |
| 'linux': 'linux', |
| 'mac': 'mac', |
| 'darwin': 'mac', |
| 'windows': 'windows', |
| }[os_name] |
| except KeyError: |
| raise Exception('unrecognized os: {}'.format(os_name)) |
| |
| |
| def arch_normalized(): |
| """Normalize arch into format expected in CIPD paths.""" |
| |
| machine = platform.machine() |
| if machine.startswith('arm'): |
| return machine |
| if machine.endswith('64'): |
| return 'amd64' |
| if machine.endswith('86'): |
| return '386' |
| raise Exception('unrecognized arch: {}'.format(machine)) |
| |
| |
| def user_agent(): |
| try: |
| rev = subprocess.check_output( |
| ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']).strip() |
| except subprocess.CalledProcessError: |
| rev = '???' |
| return 'fuchsia-infra/tools/{}'.format(rev) |
| |
| |
| def actual_hash(path): |
| hasher = hashlib.sha256() |
| with open(path, 'rb') as ins: |
| hasher.update(ins.read()) |
| return hasher.hexdigest() |
| |
| |
| def expected_hash(): |
| """Pulls expected hash from digests file.""" |
| |
| with open(DIGESTS_FILE, 'r') as ins: |
| for line in ins: |
| line = line.strip() |
| if line.startswith('#') or not line: |
| continue |
| plat, hashtype, hashval = line.split() |
| if (hashtype == 'sha256' and |
| plat == '{}-{}'.format(platform_normalized(), arch_normalized())): |
| return hashval |
| raise Exception( |
| 'platform {} not in {}'.format(platform_normalized(), DIGESTS_FILE)) |
| |
| |
| def client_bytes(): |
| """Pull down the CIPD client and return it as a bytes object. |
| |
| Often CIPD_HOST returns a 302 FOUND with a pointer to storage.googleapis.com, |
| so this needs to handle redirects, but it shouldn't require the initial |
| response to be a redirect either. |
| """ |
| |
| with open(VERSION_FILE, 'r') as ins: |
| version = ins.read().strip() |
| |
| conn = httplib.HTTPSConnection(CIPD_HOST) |
| path = '/client?platform={platform}-{arch}&version={version}'.format( |
| platform=platform_normalized(), |
| arch=arch_normalized(), |
| version=version) |
| |
| for _ in range(10): |
| conn.request('GET', path) |
| res = conn.getresponse() |
| # Have to read the response before making a new request, so make sure |
| # we always read it. |
| content = res.read() |
| |
| # Found client bytes. |
| if res.status == httplib.OK: |
| return content |
| |
| # Redirecting to another location. |
| elif res.status == httplib.FOUND: |
| location = res.getheader('location') |
| url = urlparse.urlparse(location) |
| if url.netloc != conn.host: |
| conn = httplib.HTTPSConnection(url.netloc) |
| path = '{}?{}'.format(url.path, url.query) |
| |
| # Some kind of error in this response. |
| else: |
| break |
| |
| raise Exception('failed to download client') |
| |
| |
| def bootstrap(): |
| """Bootstrap cipd client installation.""" |
| |
| client_dir = os.path.dirname(CLIENT) |
| if not os.path.isdir(client_dir): |
| os.makedirs(client_dir) |
| |
| print( |
| 'Bootstrapping cipd client for {}-{}'.format( |
| platform_normalized(), arch_normalized())) |
| tmp_path = os.path.join(SCRIPT_DIR, 'tools', '.cipd') |
| with open(tmp_path, 'wb') as tmp: |
| tmp.write(client_bytes()) |
| |
| expected = expected_hash() |
| actual = actual_hash(tmp_path) |
| |
| if expected != actual: |
| raise Exception('digest of downloaded CIPD client is incorrect, check that ' |
| 'digests file is current') |
| |
| os.chmod(tmp_path, 0755) |
| os.rename(tmp_path, CLIENT) |
| |
| |
| def selfupdate(): |
| """Update cipd client.""" |
| |
| cmd = [ |
| CLIENT, |
| 'selfupdate', |
| '-version-file', VERSION_FILE, |
| '-service-url', 'https://{}'.format(CIPD_HOST), |
| ] |
| subprocess.check_call(cmd) |
| |
| |
| def init(): |
| """Install/update cipd client.""" |
| |
| os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent() |
| |
| try: |
| if not os.path.isfile(CLIENT): |
| bootstrap() |
| |
| try: |
| selfupdate() |
| except subprocess.CalledProcessError: |
| print('CIPD selfupdate failed. Bootstrapping then retrying...', |
| file=sys.stderr) |
| bootstrap() |
| selfupdate() |
| |
| except Exception: |
| print('Failed to initialize CIPD. Run ' |
| '`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} ' |
| "selfupdate -version-file '{version_file}'` " |
| 'to diagnose if this is persistent.'.format( |
| user_agent=user_agent(), |
| client=CLIENT, |
| version_file=VERSION_FILE, |
| ), file=sys.stderr) |
| raise |
| |
| |
| if __name__ == '__main__': |
| init() |
| subprocess.check_call([CLIENT] + sys.argv[1:]) |