SysrootService: use package_indexes list when present.

If package_indexes is provided, then that is used instead of the
BINHOST.conf from the overlays.

BUG=chromium:1088059
TEST=unit tests pass.

Change-Id: Ic2041e7bcc93242403d01e1f12d93fbd44539d13
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2341833
Tested-by: LaMont Jones <lamontjones@chromium.org>
Reviewed-by: Alex Klein <saklein@chromium.org>
Commit-Queue: LaMont Jones <lamontjones@chromium.org>
diff --git a/api/controller/sysroot.py b/api/controller/sysroot.py
index b0901ab..d632f06 100644
--- a/api/controller/sysroot.py
+++ b/api/controller/sysroot.py
@@ -15,6 +15,7 @@
 from chromite.api import validate
 from chromite.api.controller import controller_util
 from chromite.api.metrics import deserialize_metrics_log
+from chromite.lib import binpkg
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
 from chromite.lib import goma_lib
@@ -41,8 +42,13 @@
 
   build_target = controller_util.ParseBuildTarget(input_proto.build_target,
                                                   input_proto.profile)
+  package_indexes = [
+      binpkg.PackageIndexInfo.from_protobuf(x)
+      for x in input_proto.package_indexes
+  ]
   run_configs = sysroot.SetupBoardRunConfig(
-      force=replace_sysroot, upgrade_chroot=update_chroot)
+      force=replace_sysroot, upgrade_chroot=update_chroot,
+      package_indexes=package_indexes)
 
   try:
     created = sysroot.Create(build_target, run_configs,
@@ -158,6 +164,10 @@
       input_proto.sysroot.build_target)
   packages = [controller_util.PackageInfoToString(x)
               for x in input_proto.packages]
+  package_indexes = [
+      binpkg.PackageIndexInfo.from_protobuf(x)
+      for x in input_proto.package_indexes
+  ]
 
   if not target_sysroot.IsToolchainInstalled():
     cros_build_lib.Die('Toolchain must first be installed.')
@@ -169,6 +179,7 @@
       usepkg=not compile_source,
       install_debug_symbols=True,
       packages=packages,
+      package_indexes=package_indexes,
       use_flags=use_flags,
       use_goma=use_goma,
       incremental_build=False)
diff --git a/api/controller/sysroot_unittest.py b/api/controller/sysroot_unittest.py
index 5aad793..f852abc 100644
--- a/api/controller/sysroot_unittest.py
+++ b/api/controller/sysroot_unittest.py
@@ -121,7 +121,8 @@
     sysroot_controller.Create(in_proto, out_proto, self.api_config)
 
     # Default value checks.
-    rc_patch.assert_called_with(force=force, upgrade_chroot=upgrade_chroot)
+    rc_patch.assert_called_with(force=force, upgrade_chroot=upgrade_chroot,
+                                package_indexes=[])
     self.assertEqual(board, out_proto.sysroot.build_target.name)
     self.assertEqual(sysroot_path, out_proto.sysroot.path)
 
@@ -137,7 +138,8 @@
     sysroot_controller.Create(in_proto, out_proto, self.api_config)
 
     # Not default value checks.
-    rc_patch.assert_called_with(force=force, upgrade_chroot=upgrade_chroot)
+    rc_patch.assert_called_with(force=force, upgrade_chroot=upgrade_chroot,
+                                package_indexes=[])
     self.assertEqual(board, out_proto.sysroot.build_target.name)
     self.assertEqual(sysroot_path, out_proto.sysroot.path)
 
diff --git a/lib/binpkg.py b/lib/binpkg.py
index e48cf22..ffee91d 100644
--- a/lib/binpkg.py
+++ b/lib/binpkg.py
@@ -22,11 +22,14 @@
 
 from six.moves import urllib
 
+from chromite.api.gen.chromiumos import common_pb2
+from chromite.lib import build_target_lib
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
 from chromite.lib import gs
 from chromite.lib import osutils
 from chromite.lib import parallel
+from chromite.lib import sysroot_lib
 
 
 assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
@@ -319,6 +322,54 @@
       self.modified = False
 
 
+class PackageIndexInfo(object):
+  """A parser for PackageIndex metadata.
+
+  Attributes:
+    snapshot_sha (str): The git SHA of the manifest snapshot.
+    snapshot_number (int): The snapshot number.
+    build_target (build_target_lib.BuildTarget): The build_target.
+    profile (Profile): The build_target.
+    location (str): The GS path for the prebuilts directory.
+  """
+
+  def __init__(self, snapshot_sha='', snapshot_number=0,
+               build_target=None, profile=None, location=''):
+    self.snapshot_sha = snapshot_sha
+    self.snapshot_number = snapshot_number
+    self.build_target = build_target or build_target_lib.BuildTarget(name='')
+    self.profile = profile or sysroot_lib.Profile()
+    self.location = location
+
+  @property
+  def as_protobuf(self):
+    """Return a chromiumos.PackageIndexInfo protobuf."""
+    return common_pb2.PackageIndexInfo(
+        snapshot_sha=self.snapshot_sha,
+        snapshot_number=self.snapshot_number,
+        build_target=self.build_target.as_protobuf,
+        profile=self.profile.as_protobuf,
+        location=self.location)
+
+  @classmethod
+  def from_protobuf(cls, message):
+    """Return a PackageIndexInfo object for the given PackageIndexInfo protobuf.
+
+    Args:
+      message (chromiumos.PackageIndexInfo): The protobuf to parse
+
+    Returns:
+      (PackageIndexInfo) the parsed instance.
+    """
+    return cls(
+        snapshot_sha=message.snapshot_sha,
+        snapshot_number=message.snapshot_number,
+        build_target=build_target_lib.BuildTarget.from_protobuf(
+            message.build_target),
+        profile=sysroot_lib.Profile.from_protobuf(message.profile),
+        location=message.location)
+
+
 def _RetryUrlOpen(url, tries=3):
   """Open the specified url, retrying if we run into temporary errors.
 
diff --git a/lib/build_target_lib.py b/lib/build_target_lib.py
index d6bee95..828e3bd 100644
--- a/lib/build_target_lib.py
+++ b/lib/build_target_lib.py
@@ -10,6 +10,8 @@
 import os
 import re
 
+from chromite.api.gen.chromiumos import common_pb2
+
 
 class Error(Exception):
   """Base module error class."""
@@ -59,6 +61,18 @@
   def name(self):
     return self._name
 
+  @property
+  def as_protobuf(self):
+    return common_pb2.BuildTarget(name=self.name)
+
+  @classmethod
+  def from_protobuf(cls, message):
+    return cls(name=message.name)
+
+  @property
+  def profile_protobuf(self):
+    return common_pb2.Profile(name=self.profile)
+
   def full_path(self, *args):
     """Turn a sysroot-relative path into an absolute path."""
     return os.path.join(self.root, *[part.lstrip(os.sep) for part in args])
diff --git a/lib/sysroot_lib.py b/lib/sysroot_lib.py
index c9f2bfd..3ed8b52 100644
--- a/lib/sysroot_lib.py
+++ b/lib/sysroot_lib.py
@@ -12,6 +12,7 @@
 import os
 import sys
 
+from chromite.api.gen.chromiumos import common_pb2
 from chromite.lib import constants
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
@@ -239,6 +240,28 @@
   return '/%s' % _MAKE_CONF_USER
 
 
+class Profile(object):
+  """Class that encapsulates the profile name for a sysroot."""
+
+  def __init__(self, name=''):
+    self._name = name
+
+  @property
+  def name(self):
+    return self._name
+
+  def __eq__(self, other):
+    return self.name == other.name
+
+  @property
+  def as_protobuf(self):
+    return common_pb2.Profile(name=self._name)
+
+  @classmethod
+  def from_protobuf(cls, message):
+    return cls(name=message.name)
+
+
 class Sysroot(object):
   """Class that encapsulate the interaction with sysroots."""
 
@@ -412,12 +435,15 @@
     config_file = _GetMakeConfGenericPath()
     osutils.SafeSymlink(config_file, self._Path(_MAKE_CONF), sudo=True)
 
-  def InstallMakeConfBoard(self, accepted_licenses=None, local_only=False):
+  def InstallMakeConfBoard(self, accepted_licenses=None, local_only=False,
+                           package_indexes=None):
     """Make sure the make.conf.board file exists and is up to date.
 
     Args:
       accepted_licenses (str): Any additional accepted licenses.
       local_only (bool): Whether prebuilts can be fetched from remote sources.
+      package_indexes (list[PackageIndexInfo]): List of information about
+        available prebuilts, youngest first, or None.
     """
     board_conf = self.GenerateBoardMakeConf(accepted_licenses=accepted_licenses)
     make_conf_path = self._Path(_MAKE_CONF_BOARD)
@@ -426,7 +452,8 @@
     # Once make.conf.board has been generated, generate the binhost config.
     # We need to do this in two steps as the binhost generation step needs
     # portageq to be available.
-    binhost_conf = self.GenerateBinhostConf(local_only=local_only)
+    binhost_conf = self.GenerateBinhostConf(local_only=local_only,
+                                            package_indexes=package_indexes)
     osutils.WriteFile(make_conf_path, '%s\n%s\n' % (board_conf, binhost_conf),
                       sudo=True)
 
@@ -554,11 +581,13 @@
 
     return '\n'.join(config)
 
-  def GenerateBinhostConf(self, local_only=False):
+  def GenerateBinhostConf(self, local_only=False, package_indexes=None):
     """Returns the binhost configuration.
 
     Args:
       local_only (bool): If True, use binary packages from local boards only.
+      package_indexes (list[PackageIndexInfo]): List of information about
+        available prebuilts, youngest first, or None.
 
     Returns:
       str - The config contents.
@@ -577,6 +606,17 @@
                         'PORTAGE_BINHOST="$LOCAL_BINHOST"'])
 
     config = []
+    if package_indexes:
+      # TODO(crbug/1088059): Drop all use of overlay commits, once the solution
+      # is in place for non-snapshot checkouts.
+      # If present, this defines PORTAGE_BINHOST.  These are independent of the
+      # overlay commits.
+      config.append('# This is the list of binhosts provided by the API.')
+      config.append('PASSED_BINHOST="%s"' % ' '.join(
+          x.location for x in reversed(package_indexes)))
+      config.append('PORTAGE_BINHOST="$PASSED_BINHOST"')
+      return '\n'.join(config)
+
     postsubmit_binhost, postsubmit_binhost_internal = self._PostsubmitBinhosts(
         board)
 
diff --git a/service/sysroot.py b/service/sysroot.py
index 2e84ef9..8aaace8 100644
--- a/service/sysroot.py
+++ b/service/sysroot.py
@@ -41,7 +41,7 @@
   def __init__(self, set_default=False, force=False, usepkg=True, jobs=None,
                regen_configs=False, quiet=False, update_toolchain=True,
                upgrade_chroot=True, init_board_pkgs=True, local_build=False,
-               toolchain_changed=False):
+               toolchain_changed=False, package_indexes=None):
     """Initialize method.
 
     Args:
@@ -57,6 +57,8 @@
       local_build (bool): Bootstrap only from local packages?
       toolchain_changed (bool): Has a toolchain change occurred? Implies
         'force'.
+      package_indexes (list[PackageIndexInfo]): List of information about
+        available prebuilts, youngest first, or None.
     """
 
     self.set_default = set_default
@@ -69,6 +71,7 @@
     self.update_chroot = upgrade_chroot
     self.init_board_pkgs = init_board_pkgs
     self.local_build = local_build
+    self.package_indexes = package_indexes or []
 
   def GetUpdateChrootArgs(self):
     """Create a list containing the relevant update_chroot arguments.
@@ -96,7 +99,7 @@
 
   def __init__(self, usepkg=True, install_debug_symbols=False,
                packages=None, use_flags=None, use_goma=False,
-               incremental_build=True):
+               incremental_build=True, package_indexes=None):
     """Init method.
 
     Args:
@@ -112,6 +115,8 @@
         build or a fresh build. Always treating it as an incremental build is
         safe, but certain operations can be faster when we know we are doing
         a fresh build.
+      package_indexes (list[PackageIndexInfo]): List of information about
+        available prebuilts, youngest first, or None.
     """
     self.usepkg = usepkg
     self.install_debug_symbols = install_debug_symbols
@@ -119,6 +124,7 @@
     self.use_flags = use_flags
     self.use_goma = use_goma
     self.is_incremental = incremental_build
+    self.package_indexes = package_indexes or []
 
   def GetBuildPackagesArgs(self):
     """Get the build_packages script arguments."""
@@ -177,6 +183,10 @@
     if self.use_goma:
       env['USE_GOMA'] = 'true'
 
+    if self.package_indexes:
+      env['PORTAGE_BINHOST'] = ' '.join(
+          x.location for x in reversed(self.package_indexes))
+
     return env
 
 
@@ -269,7 +279,8 @@
   # Refresh the workon symlinks to compensate for crbug.com/679831.
   logging.info('Setting up portage in the sysroot.')
   _InstallPortageConfigs(sysroot, target, accept_licenses,
-                         run_configs.local_build)
+                         run_configs.local_build,
+                         package_indexes=run_configs.package_indexes)
 
   # Developer Experience Step: Set default board (if requested) to allow
   # running later commands without needing to pass the --board argument.
@@ -407,7 +418,8 @@
   sysroot.InstallMakeConfUser()
 
 
-def _InstallPortageConfigs(sysroot, target, accept_licenses, local_build):
+def _InstallPortageConfigs(sysroot, target, accept_licenses, local_build,
+                           package_indexes=None):
   """Install portage wrappers and configurations.
 
   Dependencies: make.conf.board_setup (InstallConfigs).
@@ -420,13 +432,16 @@
       in the sysroot.
     accept_licenses (str): Additional accepted licenses as a string.
     local_build (bool): If the build is a local only build.
+    package_indexes (list[PackageIndexInfo]): List of information about
+      available prebuilts, youngest first, or None.
   """
   sysroot.CreateAllWrappers(friendly_name=target.name)
   _ChooseProfile(target, sysroot)
   _RefreshWorkonSymlinks(target.name, sysroot)
   # Must be done after the profile is chosen or binhosts may be incomplete.
   sysroot.InstallMakeConfBoard(accepted_licenses=accept_licenses,
-                               local_only=local_build)
+                               local_only=local_build,
+                               package_indexes=package_indexes)
 
 
 def _InstallToolchain(sysroot, target, local_init=True):