| #!/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/ (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 |
| |
| try: |
| import httplib |
| except ImportError: |
| import http.client as httplib # type: ignore |
| |
| try: |
| import urlparse # Python 2. |
| except ImportError: |
| import urllib.parse as urlparse # type: ignore |
| |
| 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", "aarch")): |
| return machine.replace("aarch", "arm") |
| if machine.endswith("64"): |
| return "amd64" |
| if machine.endswith("86"): |
| return "386" |
| raise Exception("unrecognized arch: {}".format(machine)) |
| |
| |
| 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 "fuchsia-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(): |
| """Pulls expected hash from digests file.""" |
| |
| expected_plat = "{}-{}".format(platform_normalized(), arch_normalized()) |
| |
| 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 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() |
| |
| try: |
| conn = httplib.HTTPSConnection(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 |
| |
| path = "/client?platform={platform}-{arch}&version={version}".format( |
| platform=platform_normalized(), arch=arch_normalized(), version=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" |
| "SSL error in Python when downloading CIPD client.\n" |
| "If using system Python try\n" |
| "\n" |
| " sudo pip install certifi\n" |
| "\n" |
| "If using Homebrew Python try\n" |
| "\n" |
| " brew install openssl\n" |
| " brew uninstall python\n" |
| " brew install python\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 = 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(client, silent=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_normalized(), arch_normalized() |
| ) |
| ) |
| |
| tmp_path = client + ".tmp" |
| 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, 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), |
| ] |
| subprocess.check_call(cmd) |
| |
| |
| def init(client=CLIENT, silent=False): |
| """Install/update cipd client.""" |
| |
| os.environ["CIPD_HTTP_USER_AGENT_PREFIX"] = user_agent() |
| |
| try: |
| if not os.path.isfile(client): |
| bootstrap(client, silent) |
| |
| try: |
| selfupdate(client) |
| except subprocess.CalledProcessError: |
| print( |
| "CIPD selfupdate failed. Bootstrapping then retrying...", |
| file=sys.stderr, |
| ) |
| bootstrap(client) |
| selfupdate(client) |
| |
| 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 = init() |
| subprocess.check_call([client_exe] + sys.argv[1:]) |