blob: b906337da58c9a3acebd03e1da41bd37a0005ac2 [file] [log] [blame]
#!/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:])