| #!/usr/bin/env python |
| # Copyright 2020 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Installs and then runs cipd. |
| |
| This script installs cipd in ./tools/ (if necessary) and then executes it, |
| passing through all arguments. |
| |
| Must be tested with Python 2 and Python 3. |
| """ |
| |
| from __future__ import print_function |
| |
| import hashlib |
| import os |
| import platform |
| import ssl |
| import subprocess |
| import sys |
| import base64 |
| |
| try: |
| import httplib # type: ignore |
| except ImportError: |
| import http.client as httplib # type: ignore[no-redef] |
| |
| try: |
| import urlparse # type: ignore |
| except ImportError: |
| import urllib.parse as urlparse # type: ignore[no-redef] |
| |
| # Generated from the following command. May need to be periodically rerun. |
| # $ cipd ls infra/tools/cipd | perl -pe "s[.*/][];s/^/ '/;s/\s*$/',\n/;" |
| SUPPORTED_PLATFORMS = ( |
| 'aix-ppc64', |
| 'linux-386', |
| 'linux-amd64', |
| 'linux-arm64', |
| 'linux-armv6l', |
| 'linux-mips64', |
| 'linux-mips64le', |
| 'linux-mipsle', |
| 'linux-ppc64', |
| 'linux-ppc64le', |
| 'linux-s390x', |
| 'mac-amd64', |
| 'mac-arm64', |
| 'windows-386', |
| 'windows-amd64', |
| ) |
| |
| |
| class UnsupportedPlatform(Exception): |
| pass |
| |
| |
| try: |
| SCRIPT_DIR = os.path.dirname(__file__) |
| except NameError: # __file__ not defined. |
| try: |
| SCRIPT_DIR = os.path.join(os.environ['PW_ROOT'], 'pw_env_setup', 'py', |
| 'pw_env_setup', 'cipd_setup') |
| except KeyError: |
| raise Exception('Environment variable PW_ROOT not set') |
| |
| 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. |
| CIPD_HOST = 'chrome-infra-packages.appspot.com' |
| |
| try: |
| PW_ROOT = os.environ['PW_ROOT'] |
| except KeyError: |
| try: |
| with open(os.devnull, 'w') as outs: |
| PW_ROOT = subprocess.check_output( |
| ['git', 'rev-parse', '--show-toplevel'], |
| stderr=outs, |
| ).strip().decode('utf-8') |
| except subprocess.CalledProcessError: |
| PW_ROOT = '' |
| |
| # Get default install dir from environment since args cannot always be passed |
| # through this script (args are passed as-is to cipd). |
| if 'CIPD_PY_INSTALL_DIR' in os.environ: |
| DEFAULT_INSTALL_DIR = os.environ['CIPD_PY_INSTALL_DIR'] |
| elif PW_ROOT: |
| DEFAULT_INSTALL_DIR = os.path.join(PW_ROOT, '.cipd') |
| else: |
| DEFAULT_INSTALL_DIR = '' |
| |
| |
| 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(rosetta=False): |
| """Normalize arch into format expected in CIPD paths.""" |
| |
| machine = platform.machine() |
| if platform_normalized() == 'mac' and rosetta: |
| return 'amd64' |
| if machine.startswith(('arm', 'aarch')): |
| machine = machine.replace('aarch', 'arm') |
| if machine == 'arm64': |
| return machine |
| return 'armv6l' |
| if machine.endswith('64'): |
| return 'amd64' |
| if machine.endswith('86'): |
| return '386' |
| raise Exception('unrecognized arch: {}'.format(machine)) |
| |
| |
| def platform_arch_normalized(rosetta=False): |
| return '{}-{}'.format(platform_normalized(), arch_normalized(rosetta)) |
| |
| |
| def user_agent(): |
| """Generate a user-agent based on the project name and current hash.""" |
| |
| try: |
| rev = subprocess.check_output( |
| ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']).strip() |
| except subprocess.CalledProcessError: |
| rev = '???' |
| |
| if isinstance(rev, bytes): |
| rev = rev.decode() |
| |
| return 'pigweed-infra/tools/{}'.format(rev) |
| |
| |
| def actual_hash(path): |
| """Hash the file at path and return it.""" |
| |
| hasher = hashlib.sha256() |
| with open(path, 'rb') as ins: |
| hasher.update(ins.read()) |
| return hasher.hexdigest() |
| |
| |
| def expected_hash(rosetta=False): |
| """Pulls expected hash from digests file.""" |
| |
| expected_plat = platform_arch_normalized(rosetta) |
| |
| 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 == expected_plat): |
| return hashval |
| raise Exception('platform {} not in {}'.format(expected_plat, |
| DIGESTS_FILE)) |
| |
| |
| def https_connect_with_proxy(target_url): |
| """Create HTTPSConnection with proxy support.""" |
| |
| proxy_env = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') |
| if proxy_env in (None, ''): |
| conn = httplib.HTTPSConnection(target_url) |
| return conn |
| |
| url = urlparse.urlparse(proxy_env) |
| conn = httplib.HTTPSConnection(url.hostname, url.port) |
| headers = {} |
| if url.username and url.password: |
| auth = '%s:%s' % (url.username, url.password) |
| py_version = sys.version_info.major |
| if py_version >= 3: |
| headers['Proxy-Authorization'] = 'Basic ' + str( |
| base64.b64encode(auth.encode()).decode()) |
| else: |
| headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth) |
| conn.set_tunnel(target_url, 443, headers) |
| return conn |
| |
| |
| def client_bytes(rosetta=False): |
| """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() |
| |
| try: |
| conn = https_connect_with_proxy(CIPD_HOST) |
| except AttributeError: |
| print('=' * 70) |
| print(''' |
| It looks like this version of Python does not support SSL. This is common |
| when using Homebrew. If using Homebrew please run the following commands. |
| If not using Homebrew check how your version of Python was built. |
| |
| brew install openssl # Probably already installed, but good to confirm. |
| brew uninstall python && brew install python |
| '''.strip()) |
| print('=' * 70) |
| raise |
| |
| full_platform = platform_arch_normalized(rosetta) |
| if full_platform not in SUPPORTED_PLATFORMS: |
| raise UnsupportedPlatform(full_platform) |
| |
| path = '/client?platform={}&version={}'.format(full_platform, version) |
| |
| for _ in range(10): |
| try: |
| 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() |
| except ssl.SSLError: |
| print( |
| '\n' |
| 'Bootstrap: SSL error in Python when downloading CIPD client.\n' |
| 'If using system Python try\n' |
| '\n' |
| ' sudo pip install certifi\n' |
| '\n' |
| 'And if on the system Python on a Mac try\n' |
| '\n' |
| ' /Applications/Python 3.6/Install Certificates.command\n' |
| '\n' |
| 'If using Homebrew Python try\n' |
| '\n' |
| ' brew install openssl\n' |
| ' brew uninstall python\n' |
| ' brew install python\n' |
| '\n' |
| "If those don't work, address all the potential issues shown \n" |
| 'by the following command.\n' |
| '\n' |
| ' brew doctor\n' |
| '\n' |
| "Otherwise, check that your machine's Python can use SSL, " |
| 'testing with the httplib module on Python 2 or http.client on ' |
| 'Python 3.', |
| file=sys.stderr) |
| raise |
| |
| # Found client bytes. |
| if res.status == httplib.OK: # pylint: disable=no-else-return |
| 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 = https_connect_with_proxy(url.netloc) |
| path = '{}?{}'.format(url.path, url.query) |
| |
| # Some kind of error in this response. |
| else: |
| break |
| |
| raise Exception('failed to download client from https://{}{}'.format( |
| CIPD_HOST, path)) |
| |
| |
| def bootstrap( |
| client, |
| silent=('PW_ENVSETUP_QUIET' in os.environ), |
| rosetta=False, |
| ): |
| """Bootstrap cipd client installation.""" |
| |
| client_dir = os.path.dirname(client) |
| if not os.path.isdir(client_dir): |
| os.makedirs(client_dir) |
| |
| if not silent: |
| print('Bootstrapping cipd client for {}'.format( |
| platform_arch_normalized(rosetta))) |
| |
| tmp_path = client + '.tmp' |
| with open(tmp_path, 'wb') as tmp: |
| tmp.write(client_bytes(rosetta)) |
| |
| expected = expected_hash(rosetta=rosetta) |
| 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, 0o755) |
| os.rename(tmp_path, client) |
| |
| |
| def selfupdate(client): |
| """Update cipd client.""" |
| |
| cmd = [ |
| client, |
| 'selfupdate', |
| '-version-file', VERSION_FILE, |
| '-service-url', 'https://{}'.format(CIPD_HOST), |
| ] # yapf: disable |
| subprocess.check_call(cmd) |
| |
| |
| def _default_client(install_dir): |
| client = os.path.join(install_dir, 'cipd') |
| if os.name == 'nt': |
| client += '.exe' |
| return client |
| |
| |
| def init( |
| install_dir=DEFAULT_INSTALL_DIR, |
| silent=False, |
| client=None, |
| rosetta=False, |
| ): |
| """Install/update cipd client.""" |
| |
| if not client: |
| client = _default_client(install_dir) |
| |
| os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent() |
| |
| if not os.path.isfile(client): |
| bootstrap(client, silent, rosetta=rosetta) |
| |
| try: |
| selfupdate(client) |
| except subprocess.CalledProcessError: |
| print('CIPD selfupdate failed. Bootstrapping then retrying...', |
| file=sys.stderr) |
| bootstrap(client, rosetta=rosetta) |
| selfupdate(client) |
| |
| return client |
| |
| |
| def main(install_dir=DEFAULT_INSTALL_DIR, silent=False): |
| """Install/update cipd client.""" |
| |
| client = _default_client(install_dir) |
| |
| try: |
| init(install_dir=install_dir, silent=silent, client=client) |
| |
| except UnsupportedPlatform: |
| # Don't show help message below for this exception. |
| raise |
| |
| 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 |
| |
| return client |
| |
| |
| if __name__ == '__main__': |
| client_exe = main() |
| subprocess.check_call([client_exe] + sys.argv[1:]) |