Replace cipd.sh with cipd.py.

I want to be able to use this for another project which wants to
support development on Windows. There are ways to run bash scripts
on Windows, but they require more setup than just installing
Python.

Leaving a symlink from cipd.sh to cipd.py in case other tools
reference cipd.sh.

Change-Id: Idab02b5d1744b0547f6166eed932141df7517b5a
diff --git a/cipd.py b/cipd.py
new file mode 100755
index 0000000..d81beeb
--- /dev/null
+++ b/cipd.py
@@ -0,0 +1,178 @@
+#!/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 tempfile
+
+
+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():
+  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():
+  with open(VERSION_FILE, 'r') as ins:
+    version = ins.read().strip()
+
+  # TODO(mohrr) do this with httplib instead of curl
+  cmd = [
+      'curl',
+      '--fail',
+      '--progress-bar',
+      'https://{host}/client?platform={platform}-{arch}&'
+      'version={version}'.format(
+          host=CIPD_HOST,
+          platform=platform_normalized(),
+          arch=arch_normalized(),
+          version=version),
+      '--user-agent', user_agent(),
+      '--location',
+      '--output', '-',
+  ]
+  return subprocess.check_output(cmd)
+
+
+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:])
diff --git a/cipd.sh b/cipd.sh
deleted file mode 100755
index d3307e3..0000000
--- a/cipd.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-# Copyright 2018 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.
-
-# This script installs cipd in ./tools/ and then executes it, passing through
-# all arguments.
-
-set -eo pipefail; [[ "${TRACE}" ]] && set -x
-
-readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-
-readonly VERSION_FILE="${SCRIPT_DIR}/.cipd_version"
-readonly CIPD_BACKEND="https://chrome-infra-packages.appspot.com"
-
-declare HOST_PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')"
-case "${HOST_PLATFORM}" in
-  linux)
-    readonly HOST_PLATFORM="linux"
-    ;;
-  darwin)
-    readonly HOST_PLATFORM="mac"
-    ;;
-  *)
-    echo "Unknown operating system." >&2
-    exit 1
-esac
-
-HOST_ARCH="$(uname -m | tr '[:upper:]' '[:lower:]')"
-case "${HOST_ARCH}" in
-  x86_64|amd64)
-    readonly HOST_ARCH="amd64"
-    ;;
-  arm*)
-    readonly HOST_ARCH="${HOST_ARCH}"
-    ;;
-  *86)
-    readonly HOST_ARCH=386
-    ;;
-  *)
-    echo "Unknown machine architecture." >&2
-    exit 1
-esac
-
-readonly PLATFORM="${HOST_PLATFORM}-${HOST_ARCH}"
-readonly VERSION="$(cat ${VERSION_FILE})"
-readonly URL="${CIPD_BACKEND}/client?platform=${PLATFORM}&version=${VERSION}"
-# Put it in tools so that users can easily get it in their PATH.
-readonly CLIENT="${SCRIPT_DIR}/tools/cipd"
-
-readonly USER_AGENT="fuchsia-infra/prebuilt/$(git -C ${SCRIPT_DIR} rev-parse HEAD 2>/dev/null || echo "???")"
-
-function actual_sha256() {
-  if type shasum >/dev/null ; then
-    shasum -a 256 "$1" | cut -d' ' -f1
-  else
-    echo "The \`shasum\` command is missing. Please use your package manager to install it." >&2
-    return 1
-  fi
-}
-
-function expected_sha256() {
-  local line
-  while read -r line; do
-    if [[ "${line}" =~ ^([0-9a-z\-]+)[[:blank:]]+sha256[[:blank:]]+([0-9a-f]+)$ ]] ; then
-      local platform="${BASH_REMATCH[1]}"
-      local hash="${BASH_REMATCH[2]}"
-      if [[ "${platform}" ==  "$1" ]]; then
-        echo "${hash}"
-        return 0
-      fi
-    fi
-  done < "${VERSION_FILE}.digests"
-
-  echo "Platform $1 is not supported by the CIPD client bootstrap. There's no pinned hash for it in the *.digests file." >&2
-
-  return 1
-}
-
-# bootstraps the client from scratch using 'curl'.
-function bootstrap() {
-  local expected_hash="$(expected_sha256 "${PLATFORM}")"
-  if [[ -z "${expected_hash}" ]] ; then
-    exit 1
-  fi
-
-  echo "Bootstrapping cipd client for ${HOST_PLATFORM}-${HOST_ARCH}..."
-  local CLIENT_TMP="$(mktemp -p "${SCRIPT_DIR}" 2>/dev/null || mktemp "${SCRIPT_DIR}/.cipd_client.XXXXXXX")"
-  if type curl >/dev/null ; then
-    curl -f --progress-bar "${URL}" -A "${USER_AGENT}"  -L -o "${CLIENT_TMP}"
-  else
-    echo "The \`curl\` command is missing. Please use your package manager to install it." >&2
-    exit 1
-  fi
-  trap "rm -f '${CLIENT_TMP}'" EXIT ERR HUP INT TERM
-
-  local actual_hash="$(actual_sha256 "${CLIENT_TMP}")"
-  if [[ -z "${actual_hash}" ]] ; then
-    exit 1
-  fi
-
-  if [[ "${actual_hash}" != "${expected_hash}" ]]; then
-    echo "SHA256 digest of the downloaded CIPD client is incorrect. Check that *.digests file is up-to-date." >&2
-    exit 1
-  fi
-
-  chmod +x "${CLIENT_TMP}"
-  mkdir -p $(dirname ${CLIENT})
-  mv -f "${CLIENT_TMP}" "${CLIENT}"
-  trap - EXIT
-}
-
-# self_update asks the existing client to update itself, if necessary.
-function self_update() {
-  "${CLIENT}" selfupdate -version-file "${VERSION_FILE}" -service-url "${CIPD_BACKEND}"
-}
-
-if [[ ! -x "${CLIENT}" ]]; then
-  bootstrap
-fi
-
-export CIPD_HTTP_USER_AGENT_PREFIX="${USER_AGENT}"
-if ! self_update ; then
-  echo "CIPD selfupdate failed. Trying to bootstrap the CIPD client from scratch..." >&2
-  bootstrap
-  if ! self_update ; then  # we need to run it again to setup .cipd_version file
-    echo "Bootstrap from scratch failed. Run \`CIPD_HTTP_USER_AGENT_PREFIX=${USER_AGENT}/manual ${CLIENT} selfupdate -version-file '${VERSION_FILE}'\` to diagnose if this is repeating." >&2
-  fi
-fi
-
-exec "${CLIENT}" "${@}"
diff --git a/cipd.sh b/cipd.sh
new file mode 120000
index 0000000..0d6c1e3
--- /dev/null
+++ b/cipd.sh
@@ -0,0 +1 @@
+cipd.py
\ No newline at end of file
diff --git a/update.sh b/update.sh
index 041918a..01896b0 100755
--- a/update.sh
+++ b/update.sh
@@ -10,5 +10,5 @@
 
 readonly SCRIPT_ROOT="$(cd $(dirname ${BASH_SOURCE[0]} ) && pwd)"
 
-"${SCRIPT_ROOT}/cipd.sh" ensure -ensure-file "${SCRIPT_ROOT}/cipd.ensure" -root "${SCRIPT_ROOT}" \
+"${SCRIPT_ROOT}/cipd.py" ensure -ensure-file "${SCRIPT_ROOT}/cipd.ensure" -root "${SCRIPT_ROOT}" \
   -log-level warning