blob: bc0bdca6f5870e3d1ec1b3dafbfe73d2195df301 [file] [log] [blame]
Rob Mohrdc70d1c2019-12-04 10:05:28 -08001#!/usr/bin/env python
Rob Mohrcacb8772019-11-22 13:14:01 -08002# Copyright 2019 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
Wyatt Hepler1a960942019-11-26 14:13:38 -08005# use this file except in compliance with the License. You may obtain a copy of
6# the License at
Rob Mohrcacb8772019-11-22 13:14:01 -08007#
8# https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
Wyatt Hepler1a960942019-11-26 14:13:38 -080013# License for the specific language governing permissions and limitations under
14# the License.
Rob Mohrcacb8772019-11-22 13:14:01 -080015"""Installs and then runs cipd.
16
17This script installs cipd in ./tools/ (if necessary) and then executes it,
18passing through all arguments.
19
20Must be tested with Python 2 and Python 3.
21"""
22
23from __future__ import print_function
24
25import hashlib
26import os
27import platform
28import subprocess
29import sys
30
31try:
32 import httplib
33except ImportError:
34 import http.client as httplib
35
36try:
37 import urlparse # Python 2.
38except ImportError:
39 import urllib.parse as urlparse
40
41SCRIPT_DIR = os.path.dirname(__file__)
42VERSION_FILE = os.path.join(SCRIPT_DIR, '.cipd_version')
43DIGESTS_FILE = VERSION_FILE + '.digests'
44# Put CIPD client in tools so that users can easily get it in their PATH.
Rob Mohrcacb8772019-11-22 13:14:01 -080045CIPD_HOST = 'chrome-infra-packages.appspot.com'
46
Rob Mohra44ed272020-01-13 12:49:26 -080047try:
48 PW_ROOT = os.environ['PW_ROOT']
49except KeyError:
Rob Mohr8f21e5e2020-01-21 12:08:26 -080050 try:
51 with open(os.devnull, 'w') as outs:
52 PW_ROOT = subprocess.check_output(
53 ['git', 'rev-parse', '--show-toplevel'], stderr=outs).strip()
54 except subprocess.CalledProcessError:
55 PW_ROOT = None
Rob Mohra44ed272020-01-13 12:49:26 -080056
Rob Mohr4e709512020-01-09 15:26:57 -080057# Get default install dir from environment since args cannot always be passed
58# through this script (args are passed as-is to cipd).
59if 'CIPD_PY_INSTALL_DIR' in os.environ:
60 DEFAULT_INSTALL_DIR = os.environ['CIPD_PY_INSTALL_DIR']
Rob Mohr8f21e5e2020-01-21 12:08:26 -080061elif PW_ROOT:
Rob Mohra44ed272020-01-13 12:49:26 -080062 DEFAULT_INSTALL_DIR = os.path.join(PW_ROOT, '.cipd')
Rob Mohr8f21e5e2020-01-21 12:08:26 -080063else:
64 DEFAULT_INSTALL_DIR = None
Rob Mohr91b43422019-11-25 12:09:03 -080065
Rob Mohrcacb8772019-11-22 13:14:01 -080066
67def platform_normalized():
68 """Normalize platform into format expected in CIPD paths."""
69
70 try:
71 os_name = platform.system().lower()
72 return {
73 'linux': 'linux',
74 'mac': 'mac',
75 'darwin': 'mac',
76 'windows': 'windows',
77 }[os_name]
78 except KeyError:
79 raise Exception('unrecognized os: {}'.format(os_name))
80
81
82def arch_normalized():
83 """Normalize arch into format expected in CIPD paths."""
84
85 machine = platform.machine()
86 if machine.startswith('arm'):
87 return machine
88 if machine.endswith('64'):
89 return 'amd64'
90 if machine.endswith('86'):
91 return '386'
92 raise Exception('unrecognized arch: {}'.format(machine))
93
94
95def user_agent():
96 """Generate a user-agent based on the project name and current hash."""
97
98 try:
99 rev = subprocess.check_output(
100 ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']).strip()
101 except subprocess.CalledProcessError:
102 rev = '???'
Rob Mohr4ef79f72020-01-13 14:17:45 -0800103
104 if isinstance(rev, bytes):
105 rev = rev.decode()
106
Rob Mohrcacb8772019-11-22 13:14:01 -0800107 return 'pigweed-infra/tools/{}'.format(rev)
108
109
110def actual_hash(path):
111 """Hash the file at path and return it."""
112
113 hasher = hashlib.sha256()
114 with open(path, 'rb') as ins:
115 hasher.update(ins.read())
116 return hasher.hexdigest()
117
118
119def expected_hash():
120 """Pulls expected hash from digests file."""
121
122 expected_plat = '{}-{}'.format(platform_normalized(), arch_normalized())
123
124 with open(DIGESTS_FILE, 'r') as ins:
125 for line in ins:
126 line = line.strip()
127 if line.startswith('#') or not line:
128 continue
129 plat, hashtype, hashval = line.split()
130 if (hashtype == 'sha256' and plat == expected_plat):
131 return hashval
132 raise Exception('platform {} not in {}'.format(expected_plat,
133 DIGESTS_FILE))
134
135
136def client_bytes():
137 """Pull down the CIPD client and return it as a bytes object.
138
139 Often CIPD_HOST returns a 302 FOUND with a pointer to
140 storage.googleapis.com, so this needs to handle redirects, but it
141 shouldn't require the initial response to be a redirect either.
142 """
143
144 with open(VERSION_FILE, 'r') as ins:
145 version = ins.read().strip()
146
Rob Mohr99c86062019-12-02 11:17:42 -0800147 try:
148 conn = httplib.HTTPSConnection(CIPD_HOST)
149 except AttributeError:
Wyatt Heplerbecb4312019-12-04 09:12:15 -0800150 print('=' * 70)
Rob Mohr99c86062019-12-02 11:17:42 -0800151 print('''
152It looks like this version of Python does not support SSL. This is common
153when using Homebrew. If using Homebrew please run the following commands.
154If not using Homebrew check how your version of Python was built.
155
156brew install openssl # Probably already installed, but good to confirm.
157brew uninstall python && brew install python
158'''.strip())
Wyatt Heplerbecb4312019-12-04 09:12:15 -0800159 print('=' * 70)
Rob Mohr99c86062019-12-02 11:17:42 -0800160 raise
161
Rob Mohrcacb8772019-11-22 13:14:01 -0800162 path = '/client?platform={platform}-{arch}&version={version}'.format(
163 platform=platform_normalized(),
164 arch=arch_normalized(),
165 version=version)
166
167 for _ in range(10):
168 conn.request('GET', path)
169 res = conn.getresponse()
170 # Have to read the response before making a new request, so make sure
171 # we always read it.
172 content = res.read()
173
174 # Found client bytes.
175 if res.status == httplib.OK: # pylint: disable=no-else-return
176 return content
177
178 # Redirecting to another location.
179 elif res.status == httplib.FOUND:
180 location = res.getheader('location')
181 url = urlparse.urlparse(location)
182 if url.netloc != conn.host:
183 conn = httplib.HTTPSConnection(url.netloc)
184 path = '{}?{}'.format(url.path, url.query)
185
186 # Some kind of error in this response.
187 else:
188 break
189
190 raise Exception('failed to download client')
191
192
Rob Mohr54554502020-01-09 14:41:48 -0800193def bootstrap(client):
Rob Mohrcacb8772019-11-22 13:14:01 -0800194 """Bootstrap cipd client installation."""
195
Rob Mohr54554502020-01-09 14:41:48 -0800196 client_dir = os.path.dirname(client)
Rob Mohrcacb8772019-11-22 13:14:01 -0800197 if not os.path.isdir(client_dir):
198 os.makedirs(client_dir)
199
200 print('Bootstrapping cipd client for {}-{}'.format(platform_normalized(),
201 arch_normalized()))
Rob Mohr54554502020-01-09 14:41:48 -0800202 tmp_path = client + '.tmp'
Rob Mohrcacb8772019-11-22 13:14:01 -0800203 with open(tmp_path, 'wb') as tmp:
204 tmp.write(client_bytes())
205
206 expected = expected_hash()
207 actual = actual_hash(tmp_path)
208
209 if expected != actual:
210 raise Exception('digest of downloaded CIPD client is incorrect, '
211 'check that digests file is current')
212
213 os.chmod(tmp_path, 0o755)
Rob Mohr54554502020-01-09 14:41:48 -0800214 os.rename(tmp_path, client)
Rob Mohrcacb8772019-11-22 13:14:01 -0800215
216
Rob Mohr54554502020-01-09 14:41:48 -0800217def selfupdate(client):
Rob Mohrcacb8772019-11-22 13:14:01 -0800218 """Update cipd client."""
219
220 cmd = [
Rob Mohr54554502020-01-09 14:41:48 -0800221 client,
Rob Mohrcacb8772019-11-22 13:14:01 -0800222 'selfupdate',
223 '-version-file', VERSION_FILE,
224 '-service-url', 'https://{}'.format(CIPD_HOST),
225 ] # yapf: disable
226 subprocess.check_call(cmd)
227
228
Rob Mohr54554502020-01-09 14:41:48 -0800229def init(install_dir=DEFAULT_INSTALL_DIR):
Rob Mohrcacb8772019-11-22 13:14:01 -0800230 """Install/update cipd client."""
231
232 os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent()
233
Rob Mohr54554502020-01-09 14:41:48 -0800234 client = os.path.join(install_dir, 'cipd')
235
Rob Mohrcacb8772019-11-22 13:14:01 -0800236 try:
Rob Mohr54554502020-01-09 14:41:48 -0800237 if not os.path.isfile(client):
238 bootstrap(client)
Rob Mohrcacb8772019-11-22 13:14:01 -0800239
240 try:
Rob Mohr54554502020-01-09 14:41:48 -0800241 selfupdate(client)
Rob Mohrcacb8772019-11-22 13:14:01 -0800242 except subprocess.CalledProcessError:
243 print('CIPD selfupdate failed. Bootstrapping then retrying...',
244 file=sys.stderr)
Rob Mohr54554502020-01-09 14:41:48 -0800245 bootstrap(client)
246 selfupdate(client)
Rob Mohrcacb8772019-11-22 13:14:01 -0800247
248 except Exception:
249 print('Failed to initialize CIPD. Run '
250 '`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} '
251 "selfupdate -version-file '{version_file}'` "
252 'to diagnose if this is persistent.'.format(
253 user_agent=user_agent(),
Rob Mohr54554502020-01-09 14:41:48 -0800254 client=client,
Rob Mohrcacb8772019-11-22 13:14:01 -0800255 version_file=VERSION_FILE,
256 ),
257 file=sys.stderr)
258 raise
259
Rob Mohr54554502020-01-09 14:41:48 -0800260 return client
261
Rob Mohrcacb8772019-11-22 13:14:01 -0800262
263if __name__ == '__main__':
Rob Mohr54554502020-01-09 14:41:48 -0800264 client_exe = init()
265 subprocess.check_call([client_exe] + sys.argv[1:])