Rob Mohr | dc70d1c | 2019-12-04 10:05:28 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 2 | # Copyright 2019 The Pigweed Authors |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
Wyatt Hepler | 1a96094 | 2019-11-26 14:13:38 -0800 | [diff] [blame] | 5 | # use this file except in compliance with the License. You may obtain a copy of |
| 6 | # the License at |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 7 | # |
| 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 Hepler | 1a96094 | 2019-11-26 14:13:38 -0800 | [diff] [blame] | 13 | # License for the specific language governing permissions and limitations under |
| 14 | # the License. |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 15 | """Installs and then runs cipd. |
| 16 | |
| 17 | This script installs cipd in ./tools/ (if necessary) and then executes it, |
| 18 | passing through all arguments. |
| 19 | |
| 20 | Must be tested with Python 2 and Python 3. |
| 21 | """ |
| 22 | |
| 23 | from __future__ import print_function |
| 24 | |
| 25 | import hashlib |
| 26 | import os |
| 27 | import platform |
| 28 | import subprocess |
| 29 | import sys |
| 30 | |
| 31 | try: |
| 32 | import httplib |
| 33 | except ImportError: |
| 34 | import http.client as httplib |
| 35 | |
| 36 | try: |
| 37 | import urlparse # Python 2. |
| 38 | except ImportError: |
| 39 | import urllib.parse as urlparse |
| 40 | |
| 41 | SCRIPT_DIR = os.path.dirname(__file__) |
| 42 | VERSION_FILE = os.path.join(SCRIPT_DIR, '.cipd_version') |
| 43 | DIGESTS_FILE = VERSION_FILE + '.digests' |
| 44 | # Put CIPD client in tools so that users can easily get it in their PATH. |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 45 | CIPD_HOST = 'chrome-infra-packages.appspot.com' |
| 46 | |
Rob Mohr | a44ed27 | 2020-01-13 12:49:26 -0800 | [diff] [blame] | 47 | try: |
| 48 | PW_ROOT = os.environ['PW_ROOT'] |
| 49 | except KeyError: |
Rob Mohr | 8f21e5e | 2020-01-21 12:08:26 -0800 | [diff] [blame] | 50 | 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 Mohr | a44ed27 | 2020-01-13 12:49:26 -0800 | [diff] [blame] | 56 | |
Rob Mohr | 4e70951 | 2020-01-09 15:26:57 -0800 | [diff] [blame] | 57 | # Get default install dir from environment since args cannot always be passed |
| 58 | # through this script (args are passed as-is to cipd). |
| 59 | if 'CIPD_PY_INSTALL_DIR' in os.environ: |
| 60 | DEFAULT_INSTALL_DIR = os.environ['CIPD_PY_INSTALL_DIR'] |
Rob Mohr | 8f21e5e | 2020-01-21 12:08:26 -0800 | [diff] [blame] | 61 | elif PW_ROOT: |
Rob Mohr | a44ed27 | 2020-01-13 12:49:26 -0800 | [diff] [blame] | 62 | DEFAULT_INSTALL_DIR = os.path.join(PW_ROOT, '.cipd') |
Rob Mohr | 8f21e5e | 2020-01-21 12:08:26 -0800 | [diff] [blame] | 63 | else: |
| 64 | DEFAULT_INSTALL_DIR = None |
Rob Mohr | 91b4342 | 2019-11-25 12:09:03 -0800 | [diff] [blame] | 65 | |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 66 | |
| 67 | def 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 | |
| 82 | def 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 | |
| 95 | def 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 Mohr | 4ef79f7 | 2020-01-13 14:17:45 -0800 | [diff] [blame] | 103 | |
| 104 | if isinstance(rev, bytes): |
| 105 | rev = rev.decode() |
| 106 | |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 107 | return 'pigweed-infra/tools/{}'.format(rev) |
| 108 | |
| 109 | |
| 110 | def 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 | |
| 119 | def 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 | |
| 136 | def 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 Mohr | 99c8606 | 2019-12-02 11:17:42 -0800 | [diff] [blame] | 147 | try: |
| 148 | conn = httplib.HTTPSConnection(CIPD_HOST) |
| 149 | except AttributeError: |
Wyatt Hepler | becb431 | 2019-12-04 09:12:15 -0800 | [diff] [blame] | 150 | print('=' * 70) |
Rob Mohr | 99c8606 | 2019-12-02 11:17:42 -0800 | [diff] [blame] | 151 | print(''' |
| 152 | It looks like this version of Python does not support SSL. This is common |
| 153 | when using Homebrew. If using Homebrew please run the following commands. |
| 154 | If not using Homebrew check how your version of Python was built. |
| 155 | |
| 156 | brew install openssl # Probably already installed, but good to confirm. |
| 157 | brew uninstall python && brew install python |
| 158 | '''.strip()) |
Wyatt Hepler | becb431 | 2019-12-04 09:12:15 -0800 | [diff] [blame] | 159 | print('=' * 70) |
Rob Mohr | 99c8606 | 2019-12-02 11:17:42 -0800 | [diff] [blame] | 160 | raise |
| 161 | |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 162 | 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 Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 193 | def bootstrap(client): |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 194 | """Bootstrap cipd client installation.""" |
| 195 | |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 196 | client_dir = os.path.dirname(client) |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 197 | 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 Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 202 | tmp_path = client + '.tmp' |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 203 | 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 Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 214 | os.rename(tmp_path, client) |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 215 | |
| 216 | |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 217 | def selfupdate(client): |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 218 | """Update cipd client.""" |
| 219 | |
| 220 | cmd = [ |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 221 | client, |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 222 | 'selfupdate', |
| 223 | '-version-file', VERSION_FILE, |
| 224 | '-service-url', 'https://{}'.format(CIPD_HOST), |
| 225 | ] # yapf: disable |
| 226 | subprocess.check_call(cmd) |
| 227 | |
| 228 | |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 229 | def init(install_dir=DEFAULT_INSTALL_DIR): |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 230 | """Install/update cipd client.""" |
| 231 | |
| 232 | os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent() |
| 233 | |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 234 | client = os.path.join(install_dir, 'cipd') |
| 235 | |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 236 | try: |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 237 | if not os.path.isfile(client): |
| 238 | bootstrap(client) |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 239 | |
| 240 | try: |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 241 | selfupdate(client) |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 242 | except subprocess.CalledProcessError: |
| 243 | print('CIPD selfupdate failed. Bootstrapping then retrying...', |
| 244 | file=sys.stderr) |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 245 | bootstrap(client) |
| 246 | selfupdate(client) |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 247 | |
| 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 Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 254 | client=client, |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 255 | version_file=VERSION_FILE, |
| 256 | ), |
| 257 | file=sys.stderr) |
| 258 | raise |
| 259 | |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 260 | return client |
| 261 | |
Rob Mohr | cacb877 | 2019-11-22 13:14:01 -0800 | [diff] [blame] | 262 | |
| 263 | if __name__ == '__main__': |
Rob Mohr | 5455450 | 2020-01-09 14:41:48 -0800 | [diff] [blame] | 264 | client_exe = init() |
| 265 | subprocess.check_call([client_exe] + sys.argv[1:]) |