chromium/tools/android/avd/3pp/fetch.py

#!/usr/bin/env python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Print a version and prepare the artifacts for "avd" CIPD package.

This script has the following two functions:
  * latest: Prints a version for 3pp framework to decide if creating a new CIPD
    package or not. The version value is calculated by computing the sha256
    hash of all the dependent artifacts. In this way, it is guaranteed that a
    new version will be generated if any of the dependencies get changed.
  * checkout: Prepares the dependent artifacts for 3pp framework to package and
    upload to CIPD by copying all the dependencies to a temporary checkout path
    created by 3pp framework.

This script is normally called by 3pp framework from chromium_3pp recipe module.

"""

import argparse
import hashlib
import os
import re
import shutil
import subprocess

_SRC_PATH = os.path.abspath(
    os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir,
                 os.path.pardir, os.path.pardir))
# The src-relative files and dirs we would like to include in the CIPD.
_BASE_DEPS = [
    # vpython, binaries and avd configs used by //tools/android/avd/avd.py
    '.vpython3',
    'third_party/android_sdk/public/cmdline-tools/',
    'third_party/android_sdk/public/platform-tools/',
    'tools/android/avd/proto/',

    # Should be the same as python_library("devil_chromium_py") in
    # //build/android/BUILD.gn
    'build/android/devil_chromium.json',
    'third_party/catapult/devil/devil/devil_dependencies.json',
    'third_party/catapult/third_party/gsutil/',

    # Read by gn_helpers.BuildWithChromium()
    # Needed to recognize the adb binary in third_party/android_sdk
    'build/config/gclient_args.gni',
]


def _file_hash(sha, rel_path, base_path):
  path = os.path.join(base_path, rel_path)
  with open(path, 'rb') as f:
    sha.update(str(len(rel_path)).encode('utf-8'))
    sha.update(rel_path.encode('utf-8'))
    while True:
      f_stream = f.read(4096)
      if not f_stream:
        break
      sha.update(str(len(f_stream)).encode('utf-8'))
      sha.update(f_stream)


# This function computes sha256 hash of provided directories and/or files that
# are relative to the given base_path.
# Copied from the compute_hash function in https://chromium.googlesource.com/
# infra/luci/recipes-py/+/HEAD/recipe_modules/file/resources/fileutil.py
def _compute_hash_paths(base_path, *rel_paths):
  sha = hashlib.sha256()
  for rel_path in rel_paths:
    path = os.path.join(base_path, rel_path)
    if os.path.isfile(path):
      _file_hash(sha, rel_path, base_path)
    elif os.path.isdir(path):
      for root, dirs, files in os.walk(path, topdown=True):
        dirs.sort()  # ensure we walk dirs in sorted order
        files.sort()
        for f_name in files:
          f_path = os.path.join(root, f_name)
          # Check if it's a file to prevent following symlinks.
          if os.path.isfile(f_path):
            rel_file_path = os.path.relpath(f_path, base_path)
            _file_hash(sha, rel_file_path, base_path)

  return sha.hexdigest()


# Return a list of src-relative paths for the dependent files and dirs for avd
def _get_deps(chromium_src_path):
  deps_cmds = [
      os.path.join(chromium_src_path, 'build', 'print_python_deps.py'),
      '--root',
      chromium_src_path,
      os.path.join(chromium_src_path, 'tools', 'android', 'avd', 'avd.py'),
  ]
  deps_output = subprocess.check_output(deps_cmds, universal_newlines=True)
  # Filter out comments in deps_output
  deps_lines = deps_output.strip().split('\n')
  deps_entries = [dep for dep in deps_lines if not dep.startswith('#')]
  return sorted(_BASE_DEPS + deps_entries)


def do_latest():
  deps = _get_deps(_SRC_PATH)
  print(_compute_hash_paths(_SRC_PATH, *deps))


def do_checkout(checkout_path):
  deps = _get_deps(_SRC_PATH)
  # Copy all contents under deps to `<checkout_path>/src`
  for dep in deps:
    dep_pieces = dep.split('/')
    chromium_dep_path = os.path.join(_SRC_PATH, *dep_pieces)
    checkout_dep_path = os.path.join(checkout_path, 'src', *dep_pieces)

    checkout_dep_par_path = os.path.abspath(
        os.path.join(checkout_dep_path, os.path.pardir))
    if not os.path.exists(checkout_dep_par_path):
      os.makedirs(checkout_dep_par_path)

    # Since _BASE_DEPS and deps may have overlaps, continue if a path exists
    if os.path.exists(checkout_dep_path):
      continue

    if os.path.isdir(chromium_dep_path):
      shutil.copytree(chromium_dep_path, checkout_dep_path, symlinks=True)
    else:
      shutil.copy(chromium_dep_path, checkout_dep_path)


def main():
  ap = argparse.ArgumentParser(
      description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
  sub = ap.add_subparsers()

  latest = sub.add_parser("latest")
  latest.set_defaults(func=lambda _opts: do_latest())

  checkout = sub.add_parser("checkout")
  checkout.add_argument("checkout_path")
  checkout.set_defaults(func=lambda opts: do_checkout(opts.checkout_path))

  opts = ap.parse_args()
  opts.func(opts)


if __name__ == '__main__':
  main()