2 # Copyright 2020 The Pigweed Authors
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 # use this file except in compliance with the License. You may obtain a copy of
8 # https://www.apache.org/licenses/LICENSE-2.0
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
13 # License for the specific language governing permissions and limitations under
15 """Installs and then runs cipd.
17 This script installs cipd in ./tools/ (if necessary) and then executes it,
18 passing through all arguments.
20 Must be tested with Python 2 and Python 3.
23 from __future__ import print_function
34 import httplib # type: ignore
36 import http.client as httplib # type: ignore[no-redef]
39 import urlparse # type: ignore
41 import urllib.parse as urlparse # type: ignore[no-redef]
44 SCRIPT_DIR = os.path.dirname(__file__)
45 except NameError: # __file__ not defined.
47 SCRIPT_DIR = os.path.join(os.environ['PW_ROOT'], 'pw_env_setup', 'py',
48 'pw_env_setup', 'cipd_setup')
50 raise Exception('Environment variable PW_ROOT not set')
52 VERSION_FILE = os.path.join(SCRIPT_DIR, '.cipd_version')
53 DIGESTS_FILE = VERSION_FILE + '.digests'
55 # Put CIPD client in tools so that users can easily get it in their PATH.
56 CIPD_HOST = 'chrome-infra-packages.appspot.com'
59 PW_ROOT = os.environ['PW_ROOT']
62 with open(os.devnull, 'w') as outs:
63 PW_ROOT = subprocess.check_output(
64 ['git', 'rev-parse', '--show-toplevel'],
66 ).strip().decode('utf-8')
67 except subprocess.CalledProcessError:
70 # Get default install dir from environment since args cannot always be passed
71 # through this script (args are passed as-is to cipd).
72 if 'CIPD_PY_INSTALL_DIR' in os.environ:
73 DEFAULT_INSTALL_DIR = os.environ['CIPD_PY_INSTALL_DIR']
75 DEFAULT_INSTALL_DIR = os.path.join(PW_ROOT, '.cipd')
77 DEFAULT_INSTALL_DIR = ''
80 def platform_normalized():
81 """Normalize platform into format expected in CIPD paths."""
84 os_name = platform.system().lower()
92 raise Exception('unrecognized os: {}'.format(os_name))
95 def arch_normalized():
96 """Normalize arch into format expected in CIPD paths."""
98 machine = platform.machine()
99 if machine.startswith(('arm', 'aarch')):
100 return machine.replace('aarch', 'arm')
101 if machine.endswith('64'):
103 if machine.endswith('86'):
105 raise Exception('unrecognized arch: {}'.format(machine))
109 """Generate a user-agent based on the project name and current hash."""
112 rev = subprocess.check_output(
113 ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']).strip()
114 except subprocess.CalledProcessError:
117 if isinstance(rev, bytes):
120 return 'pigweed-infra/tools/{}'.format(rev)
123 def actual_hash(path):
124 """Hash the file at path and return it."""
126 hasher = hashlib.sha256()
127 with open(path, 'rb') as ins:
128 hasher.update(ins.read())
129 return hasher.hexdigest()
133 """Pulls expected hash from digests file."""
135 expected_plat = '{}-{}'.format(platform_normalized(), arch_normalized())
137 with open(DIGESTS_FILE, 'r') as ins:
140 if line.startswith('#') or not line:
142 plat, hashtype, hashval = line.split()
143 if (hashtype == 'sha256' and plat == expected_plat):
145 raise Exception('platform {} not in {}'.format(expected_plat,
149 def https_connect_with_proxy(target_url):
150 """Create HTTPSConnection with proxy support."""
152 proxy_env = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
153 if proxy_env in (None, ''):
154 conn = httplib.HTTPSConnection(target_url)
157 url = urlparse.urlparse(proxy_env)
158 conn = httplib.HTTPSConnection(url.hostname, url.port)
160 if url.username and url.password:
161 auth = '%s:%s' % (url.username, url.password)
162 py_version = sys.version_info.major
164 headers['Proxy-Authorization'] = 'Basic ' + str(
165 base64.b64encode(auth.encode()).decode())
167 headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth)
168 conn.set_tunnel(target_url, 443, headers)
173 """Pull down the CIPD client and return it as a bytes object.
175 Often CIPD_HOST returns a 302 FOUND with a pointer to
176 storage.googleapis.com, so this needs to handle redirects, but it
177 shouldn't require the initial response to be a redirect either.
180 with open(VERSION_FILE, 'r') as ins:
181 version = ins.read().strip()
184 conn = https_connect_with_proxy(CIPD_HOST)
185 except AttributeError:
188 It looks like this version of Python does not support SSL. This is common
189 when using Homebrew. If using Homebrew please run the following commands.
190 If not using Homebrew check how your version of Python was built.
192 brew install openssl # Probably already installed, but good to confirm.
193 brew uninstall python && brew install python
198 path = '/client?platform={platform}-{arch}&version={version}'.format(
199 platform=platform_normalized(),
200 arch=arch_normalized(),
205 conn.request('GET', path)
206 res = conn.getresponse()
207 # Have to read the response before making a new request, so make
208 # sure we always read it.
213 'Bootstrap: SSL error in Python when downloading CIPD client.\n'
214 'If using system Python try\n'
216 ' sudo pip install certifi\n'
218 'If using Homebrew Python try\n'
220 ' brew install openssl\n'
221 ' brew uninstall python\n'
222 ' brew install python\n'
224 "Otherwise, check that your machine's Python can use SSL, "
225 'testing with the httplib module on Python 2 or http.client on '
230 # Found client bytes.
231 if res.status == httplib.OK: # pylint: disable=no-else-return
234 # Redirecting to another location.
235 elif res.status == httplib.FOUND:
236 location = res.getheader('location')
237 url = urlparse.urlparse(location)
238 if url.netloc != conn.host:
239 conn = https_connect_with_proxy(url.netloc)
240 path = '{}?{}'.format(url.path, url.query)
242 # Some kind of error in this response.
246 raise Exception('failed to download client')
249 def bootstrap(client, silent=('PW_ENVSETUP_QUIET' in os.environ)):
250 """Bootstrap cipd client installation."""
252 client_dir = os.path.dirname(client)
253 if not os.path.isdir(client_dir):
254 os.makedirs(client_dir)
257 print('Bootstrapping cipd client for {}-{}'.format(
258 platform_normalized(), arch_normalized()))
260 tmp_path = client + '.tmp'
261 with open(tmp_path, 'wb') as tmp:
262 tmp.write(client_bytes())
264 expected = expected_hash()
265 actual = actual_hash(tmp_path)
267 if expected != actual:
268 raise Exception('digest of downloaded CIPD client is incorrect, '
269 'check that digests file is current')
271 os.chmod(tmp_path, 0o755)
272 os.rename(tmp_path, client)
275 def selfupdate(client):
276 """Update cipd client."""
281 '-version-file', VERSION_FILE,
282 '-service-url', 'https://{}'.format(CIPD_HOST),
284 subprocess.check_call(cmd)
287 def init(install_dir=DEFAULT_INSTALL_DIR, silent=False):
288 """Install/update cipd client."""
290 os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent()
292 client = os.path.join(install_dir, 'cipd')
297 if not os.path.isfile(client):
298 bootstrap(client, silent)
302 except subprocess.CalledProcessError:
303 print('CIPD selfupdate failed. Bootstrapping then retrying...',
309 print('Failed to initialize CIPD. Run '
310 '`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} '
311 "selfupdate -version-file '{version_file}'` "
312 'to diagnose if this is persistent.'.format(
313 user_agent=user_agent(),
315 version_file=VERSION_FILE,
323 if __name__ == '__main__':
325 subprocess.check_call([client_exe] + sys.argv[1:])