| #!/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/ and then executes it, passing through |
| all arguments. |
| """ |
| |
| from __future__ import print_function |
| |
| import hashlib |
| import httplib |
| import os |
| import platform |
| import subprocess |
| import sys |
| import urlparse |
| |
| |
| 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"): |
| return machine |
| if machine.endswith("64"): |
| return "amd64" |
| if machine.endswith("86"): |
| return "386" |
| raise Exception("unrecognized arch: {}".format(machine)) |
| |
| |
| def user_agent(): |
| try: |
| rev = subprocess.check_output( |
| ["git", "-C", SCRIPT_DIR, "rev-parse", "HEAD"] |
| ).strip() |
| except subprocess.CalledProcessError: |
| rev = "???" |
| return "fuchsia-infra/tools/{}".format(rev) |
| |
| |
| def actual_hash(path): |
| 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.""" |
| |
| 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 == "{}-{}".format( |
| platform_normalized(), arch_normalized() |
| ): |
| return hashval |
| raise Exception( |
| "platform {} not in {}".format(platform_normalized(), 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() |
| |
| conn = httplib.HTTPSConnection(CIPD_HOST) |
| path = "/client?platform={platform}-{arch}&version={version}".format( |
| platform=platform_normalized(), arch=arch_normalized(), version=version |
| ) |
| |
| for _ in range(10): |
| 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() |
| |
| # Found client bytes. |
| if res.status == httplib.OK: |
| 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(): |
| """Bootstrap cipd client installation.""" |
| |
| client_dir = os.path.dirname(CLIENT) |
| if not os.path.isdir(client_dir): |
| os.makedirs(client_dir) |
| |
| print( |
| "Bootstrapping cipd client for {}-{}".format( |
| platform_normalized(), arch_normalized() |
| ) |
| ) |
| tmp_path = os.path.join(SCRIPT_DIR, "tools", ".cipd") |
| 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, 0755) |
| os.rename(tmp_path, CLIENT) |
| |
| |
| def selfupdate(): |
| """Update cipd client.""" |
| |
| cmd = [ |
| CLIENT, |
| "selfupdate", |
| "-version-file", |
| VERSION_FILE, |
| "-service-url", |
| "https://{}".format(CIPD_HOST), |
| ] |
| subprocess.check_call(cmd) |
| |
| |
| def init(): |
| """Install/update cipd client.""" |
| |
| os.environ["CIPD_HTTP_USER_AGENT_PREFIX"] = user_agent() |
| |
| try: |
| if not os.path.isfile(CLIENT): |
| bootstrap() |
| |
| try: |
| selfupdate() |
| except subprocess.CalledProcessError: |
| print( |
| "CIPD selfupdate failed. Bootstrapping then retrying...", |
| file=sys.stderr, |
| ) |
| bootstrap() |
| selfupdate() |
| |
| 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 |
| |
| |
| if __name__ == "__main__": |
| init() |
| subprocess.check_call([CLIENT] + sys.argv[1:]) |