contrib: utility script to assist with unibuild conversions

Utility script to assist with converting pre-unibuild boards to
unibuild v2 (model.yaml format).

Run with --help for usage, you'll need a DUT over SSH that the script
can run commands on.

Note: this is a contrib-quality (unsupported) script, and it's meant
to be this way.  Essentially just submitting as is for now so that we
can iterate on it as we find more corner cases.

BUG=none
TEST=run for nocturne, build image and flash

Change-Id: I56abc90db0df6c8fe091add5181e8226f08a5b95
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2233191
Tested-by: Jack Rosenthal <jrosenth@chromium.org>
Commit-Queue: Jack Rosenthal <jrosenth@chromium.org>
Reviewed-by: C Shapiro <shapiroc@chromium.org>
Reviewed-by: Sean McAllister <smcallis@google.com>
diff --git a/contrib/cros_unibuild_convert.py b/contrib/cros_unibuild_convert.py
new file mode 100755
index 0000000..4b4d9ea
--- /dev/null
+++ b/contrib/cros_unibuild_convert.py
@@ -0,0 +1,719 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium OS 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 file uses 2-space indentations.
+# pylint: disable=bad-indentation
+
+# This is contrib-quality code: not all functions/classes are
+# documented.
+# pylint: disable=missing-function-docstring
+# pylint: disable=missing-class-docstring
+# pylint: disable=class-missing-docstring
+
+# Classes make heavy-use of setattr to dynamically set the attributes
+# on an object.  Disable this check which gets confused very
+# frequently.
+# pylint: disable=no-member
+
+"""Utility script to auto-convert a pre-unibuild board to unibuild."""
+
+import argparse
+import datetime
+import json
+import os
+import pathlib
+import re
+import shlex
+import subprocess
+import sys
+import tempfile
+# pylint: disable=import-error
+import yaml
+# pylint: enable=import-error
+
+
+make_defaults_search_and_destroy_re = re.compile(
+    r'(?:^\s*)*(?:^\s*#.*\s*)*^\s*USE="\s*\$\{?USE\}?\s*-unibuild\s*"\s*$',
+    re.MULTILINE)
+
+
+def log(message):
+  print('[{}] {}'.format(datetime.datetime.now(), message), file=sys.stderr)
+
+
+def prepend_all_lines(text, prepend):
+  return ''.join(
+      '{}{}\n'.format(prepend, line)
+      for line in text.splitlines())
+
+
+def gen_cros_copyright(line_comment='# '):
+  return prepend_all_lines(
+      """Copyright {} The Chromium OS Authors. All rights reserved.
+Use of this source code is coverned by a BSD-style license that can be
+found in the LICENSE file.""".format(datetime.datetime.now().strftime('%Y')),
+      line_comment)
+
+
+def yaml_str_representer(dumper, data):
+  style = None
+  tag = 'tag:yaml.org,2002:str'
+  if '\n' in data:
+    style = '|'
+  return dumper.represent_scalar(tag, data, style)
+
+
+yaml.add_representer(str, yaml_str_representer)
+
+
+def format_yaml(config):
+  conf_str = yaml.dump(config, indent=2, default_flow_style=False)
+  out = gen_cros_copyright()
+  out += """
+# This board only supports a single config, defined below, as it is a
+# migrated pre-unibuild device.
+device-config: &device_config\n"""
+  out += prepend_all_lines(conf_str, '  ')
+  out += """
+# Required dunder for chromeos-config to support a single device.
+chromeos:
+  devices:
+    - skus:
+        - config: *device_config\n"""
+  return out
+
+
+def generate_vpackage(depends):
+  return gen_cros_copyright() + """
+EAPI=7
+
+# cros_workon applies only to ebuild and files directory. Use the
+# canonical empty project.
+CROS_WORKON_PROJECT="chromiumos/infra/build/empty-project"
+CROS_WORKON_LOCALNAME="../platform/empty-project"
+
+inherit cros-workon
+
+DESCRIPTION="ChromeOS Unibuild Config virtual package"
+HOMEPAGE="https://chromium.googlesource.com/chromiumos/platform2/+/master/chromeos-config/README.md"
+
+LICENSE="BSD-Google"
+SLOT="0"
+KEYWORDS="~*"
+
+DEPEND="%(depends)s"
+RDEPEND="${DEPEND}"
+""" % {
+    'depends':
+    (''.join('\n\t{}'.format(d) for d in depends) + '\n')
+    if len(depends) > 1 else
+    ''.join(depends)}
+
+
+def generate_bsp_ebuild(private=False):
+  return gen_cros_copyright() + """
+EAPI=7
+
+# cros_workon applies only to ebuild and files directory. Use the
+# canonical empty project.
+CROS_WORKON_PROJECT="chromiumos/infra/build/empty-project"
+CROS_WORKON_LOCALNAME="../platform/empty-project"
+
+inherit cros-workon cros-unibuild
+
+DESCRIPTION="ChromeOS model configuration"
+HOMEPAGE="https://chromium.googlesource.com/chromiumos/platform2/+/master/chromeos-config/README.md"
+
+LICENSE="BSD-Google"
+SLOT="0"
+KEYWORDS="~*"
+
+src_install() {
+\tinstall%(maybe_private)s_model_files
+}
+""" % {'maybe_private': '_private' if private else ''}
+
+
+def generate_firmware_ebuild(board_name):
+  return gen_cros_copyright() + """
+# Change this version number when any change is made to model.yaml
+# in order to trigger an auto-revbump is required.
+# VERSION=REVBUMP-0.0.1
+
+EAPI=7
+CROS_WORKON_COMMIT=""
+CROS_WORKON_TREE=""
+CROS_WORKON_LOCALNAME="platform/firmware"
+CROS_WORKON_PROJECT="chromiumos/platform/firmware"
+CROS_BOARDS=( %(board_name)s )
+
+inherit cros-workon cros-firmware cros-unibuild
+
+DESCRIPTION="Chrome OS Firmware (%(board_name)s)"
+HOMEPAGE="http://src.chromium.org"
+LICENSE="BSD-Google"
+SLOT="0"
+KEYWORDS="~*"
+
+RDEPEND=""
+
+# Unified Builds firmware URL's are read from:
+#   chromeos-base/chromeos-config-bsp-private/files/model.yaml
+# in this repository. Those config files output the SRC_URI's used by Portage.
+#
+# Update the model.yaml, then run this command from the
+# src/platform/dev/contrib directory:
+#
+#   ./cros_update_firmware --board=%(board_name)s
+#
+# Verify the changes by running:
+#   /build/%(board_name)s/usr/sbin/chromeos-firmwareupdate --manifest
+#
+# If this works then you can create a CL with your changes, which should include
+# the files:
+# chromeos-base/chromeos-config-bsp-private/files/model.yaml
+# chromeos-base/chromeos-firmware-%(board_name)s/Manifest
+# chromeos-base/chromeos-firmware-%(board_name)s/files/srcuris
+# chromeos-base/chromeos-firmware-%(board_name)s/chromeos-firmware-%(board_name)s-9999.ebuild
+cros-firmware_setup_source
+""" % {'board_name': board_name}
+
+
+def find_file(searchdir, name):
+  results = []
+  for root, _, files in os.walk(searchdir):
+    if name in files:
+      results.append(pathlib.Path(root) / name)
+  return results
+
+
+def find_one_file(searchdir, name):
+  results = find_file(searchdir, name)
+  assert len(results) == 1
+  return results.pop()
+
+
+def sh_getvar(script, varname):
+  script = script + ('\necho "${%s}"\n' % varname)
+  with tempfile.NamedTemporaryFile('w') as f:
+    f.write(script)
+    f.flush()
+    res = subprocess.run(['sh', f.name], stdout=subprocess.PIPE,
+                         check=True, encoding='utf-8')
+  return res.stdout.strip() or None
+
+
+def write_file(fullpath, file_contents):
+  os.makedirs(fullpath.parent, exist_ok=True)
+  log('Writing {}...'.format(fullpath))
+  with open(fullpath, 'w') as f:
+    f.write(file_contents)
+
+
+def generate_make_defaults(contents):
+  contents = make_defaults_search_and_destroy_re.sub('', contents)
+  contents += """
+# Enable chromeos-config.
+USE="${USE} unibuild"
+"""
+  return contents
+
+
+class CrosConfig:
+  def __init__(self, public_yaml_raw, private_yaml_raw):
+    with tempfile.NamedTemporaryFile(mode='w', delete=False) as merged_tempfile, \
+         tempfile.NamedTemporaryFile(mode='w') as public_yaml_tempfile, \
+         tempfile.NamedTemporaryFile(mode='w') as private_yaml_tempfile:
+      public_yaml_tempfile.write(public_yaml_raw)
+      public_yaml_tempfile.flush()
+
+      private_yaml_tempfile.write(private_yaml_raw)
+      private_yaml_tempfile.flush()
+
+      log('Merging and validating config schema...')
+      subprocess.run(['cros_config_schema', '-o', merged_tempfile.name,
+                      '-m', public_yaml_tempfile.name,
+                      private_yaml_tempfile.name], check=True)
+      self.merged_yaml = merged_tempfile.name
+
+  def run_host_command(self, *args):
+    return subprocess.run(['cros_config_host', '-c', self.merged_yaml]
+                          + list(args),
+                          check=True, encoding='utf-8',
+                          stdout=subprocess.PIPE).stdout
+
+
+class BoardOverlays:
+  FIRMWARE_ATTRS = [
+      ('CROS_FIRMWARE_MAIN_IMAGE', 'bcs_main_ro'),
+      ('CROS_FIRMWARE_MAIN_RW_IMAGE', 'bcs_main_rw'),
+      ('CROS_FIRMWARE_EC_IMAGE', 'bcs_ec'),
+      ('CROS_FIRMWARE_PD_IMAGE', 'bcs_pd'),
+  ]
+
+  MAKE_DEFAULTS_ATTRS = [
+      ('EC_FIRMWARE', 'ec_firmwares'),
+      ('PD_FIRMWARE', 'pd_firmwares'),
+      ('EC_FIRMWARE_EXTRA', 'ec_firmware_extras'),
+      ('FPMCU_FIRMWARE', 'fpmcu_firmware'),
+      ('USE', 'use_flags'),
+  ]
+
+  def __init__(self, board_name, checkout, mosys_platform):
+    self.board_name = board_name
+    self.mosys_platform = mosys_platform
+    self.public_overlay = (checkout / 'src' / 'overlays'
+                           / f'overlay-{board_name}')
+    log('Public overlay path: {}'.format(self.public_overlay))
+    self.private_overlay = (checkout / 'src' / 'private-overlays'
+                            / f'overlay-{board_name}-private')
+    log('Private overlay path: {}'.format(self.private_overlay))
+
+    assert self.public_overlay.is_dir()
+    assert self.private_overlay.is_dir()
+
+    # Find the firmware ebuild
+    self.firmware_ebuild_path = find_one_file(
+        self.private_overlay, f'chromeos-firmware-{board_name}-9999.ebuild')
+    log('Firmware ebuild path: {}'.format(self.firmware_ebuild_path))
+
+    # Read the firmware attrs from it
+    for _, attr in self.FIRMWARE_ATTRS:
+      setattr(self, attr, None)
+
+    with open(self.firmware_ebuild_path) as f:
+      for line in f:
+        if '#' in line:
+          line, _, _ = line.partition('#')
+        line = line.strip()
+
+        for var, attr in self.FIRMWARE_ATTRS:
+          if line.startswith('{}='.format(var)):
+            _, _, value = line.partition('=')
+            value = value.replace('"', '').replace("'", '')
+            setattr(self, attr, value)
+
+    # Find make.defaults files
+    self.public_make_defaults_file = (
+        self.public_overlay / 'profiles' / 'base' / 'make.defaults')
+    self.private_make_defaults_file = (
+        self.private_overlay / 'profiles' / 'base' / 'make.defaults')
+
+    with open(self.public_make_defaults_file) as f:
+      self.public_make_defaults = f.read()
+    with open(self.private_make_defaults_file) as f:
+      self.private_make_defaults = f.read()
+
+    for var, attr in self.MAKE_DEFAULTS_ATTRS:
+      setattr(self, attr, set())
+      for script in (self.public_make_defaults, self.private_make_defaults):
+        value = sh_getvar(script, var)
+        if value:
+          for v in value.split():
+            getattr(self, attr).add(v)
+
+    if 'whiskers' in self.ec_firmware_extras:
+      self.ec_firmware_extras.remove('whiskers')
+      self.detachable_base_build_target = 'whiskers'
+    else:
+      self.detachable_base_build_target = None
+
+    self.ec_build_target = ' '.join(self.ec_firmwares) or None
+    self.ec_extras_build_target = sorted(list(self.ec_firmware_extras
+                                              | self.pd_firmwares)) or None
+
+  def write_file(self, overlay_flags, path, file_contents):
+    dirs = []
+    if overlay_flags & M_PUBLIC:
+      dirs += [self.public_overlay]
+    if overlay_flags & M_PRIVATE:
+      dirs += [self.private_overlay]
+    for d in dirs:
+      write_file(d / path, file_contents)
+
+
+class Dut:
+  def __init__(self, hostname, checkout, port=22):
+    self.ssh_hostname = hostname
+
+    id_source = checkout / 'chromite' / 'ssh_keys' / 'testing_rsa'
+    with open(id_source, 'rb') as f:
+      id_contents = f.read()
+
+    with tempfile.NamedTemporaryFile(mode='wb', delete=False) as tmpfile:
+      tmpfile.write(id_contents)
+      self.ssh_identity = tmpfile.name
+
+    with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
+      self.ssh_known_hosts_file = tmpfile.name
+
+    self.ssh_port = port
+
+    # Check connectivity.
+    log('Checking SSH connectivity to DUT...')
+    self.run_command(['/bin/true'])
+
+  # Linter is unaware that we set check=True in kwargs.
+  # pylint: disable=subprocess-run-check
+  def run_command(self, argv, *args, **kwargs):
+    kwargs.setdefault('check', True)
+    kwargs.setdefault('stdout', subprocess.PIPE)
+    kwargs.setdefault('encoding', 'utf-8')
+    quoted_argv = [shlex.quote(arg) for arg in argv]
+    return subprocess.run(['ssh',
+                           '-p', '{}'.format(self.ssh_port),
+                           '-i', self.ssh_identity,
+                           '-o', 'UserKnownHostsFile={}'.format(
+                               self.ssh_known_hosts_file),
+                           '-o', 'StrictHostKeyChecking=no',
+                           '-o', 'CheckHostIP=no',
+                           '-o', 'ConnectTimeout=10',
+                           'root@{}'.format(self.ssh_hostname)] + quoted_argv,
+                          *args, **kwargs)
+  # pylint: enable=subprocess-run-check
+
+
+class DeviceConfig:
+  ATTRS = {
+      'brand_code': ['mosys', 'platform', 'brand'],
+      'model': ['mosys', 'platform', 'model'],
+      'lsb_release': ['cat', '/etc/lsb-release'],
+      'smbios_name': ['cat', '/sys/class/dmi/id/product_name'],
+      'fdt_compatible_raw': ['cat', '/proc/device-tree/compatible'],
+      'arc_build_props': ['cat', '/usr/share/arc/properties/build.prop'],
+      'mosys_psu_type': ['mosys', 'psu', 'type'],
+      'whitelabel_tag': ['vpd_get_value', 'whitelabel_tag'],
+      'customization_id': ['vpd_get_value', 'customization_id'],
+      'cras_config_dir': ['sh', '/etc/cras/get_device_config_dir'],
+      'internal_ucm_suffix': ['sh', '/etc/cras/get_internal_ucm_suffix'],
+      # disgusting, but whatever...
+      'powerd_raw':
+      ['python3', '-c',
+       'import os;'
+       'import json;'
+       'print(json.dumps('
+       '{f.replace("_", "-"): open("/usr/share/power_manager/board_specific/"+f).read().rstrip()'
+       ' for f in os.listdir("/usr/share/power_manager/board_specific")}))'],
+  }
+
+  @classmethod
+  def from_dut(cls, dut):
+    slf = cls()
+    for attr, cmd in cls.ATTRS.items():
+      try:
+        log('Running {!r} on DUT...'.format(cmd))
+        res = dut.run_command(cmd)
+      except subprocess.CalledProcessError:
+        setattr(slf, attr, None)
+      else:
+        setattr(slf, attr, res.stdout.strip())
+    return slf
+
+  def __str__(self):
+    return 'DeviceConfig({})'.format(
+        ', '.join('{}={!r}'.format(attr, getattr(self, attr))
+                  for attr in self.ATTRS))
+
+  def lsb_val(self, name, default=None):
+    for item in self.lsb_release.splitlines():
+      k, _, v = item.partition('=')
+      if k == name:
+        return v
+    return default
+
+  def arc_build_prop(self, name, default=None):
+    for line in self.arc_build_props.splitlines():
+      if '#' in line:
+        line, _, _ = line.partition('#')
+      line = line.strip()
+      if line.startswith('{}='.format(name)):
+        _, _, val = line.partition('=')
+        return val
+    return default
+
+
+def genconf_first_api_level(_, overlay):
+  if overlay.board_name in ('atlas', 'nocturne'):
+    return '28'
+  return '25'
+
+
+def genconf_dt_compatible_match(device, overlay):
+  if not device.fdt_compatible_raw:
+    return None
+  compatible_strings = device.fdt_compatible_raw.strip('\x00').split('\x00')
+  compatible_strings.sort(key=lambda s: (s.startswith('google'),
+                                         'rev' not in s,
+                                         'sku' not in s,
+                                         overlay.board_name in s,
+                                         -len(s)))
+  return compatible_strings[-1]
+
+
+def genconf_psu_type(device, _):
+  if device.mosys_psu_type:
+    return device.mosys_psu_type
+  devicetype = device.lsb_val('DEVICETYPE')
+  if devicetype == 'CHROMEBOOK':
+    return 'battery'
+  if devicetype in ('CHROMEBIT', 'CHROMEBASE', 'CHROMEBOX'):
+    return 'AC_only'
+  return None
+
+
+def genconf_fp_board(_, overlay):
+  if overlay.fpmcu_firmware:
+    return ' '.join(overlay.fpmcu_firmware)
+  return None
+
+
+def genconf_fp_type(_, overlay):
+  if 'fp_on_power_button' in overlay.use_flags:
+    return 'on-power-button'
+  if overlay.fpmcu_firmware:
+    return 'stand-alone'
+  return None
+
+
+def genconf_fp_location(_, overlay):
+  if overlay.board_name == 'nocturne':
+    return 'power-button-top-left'
+  return None
+
+
+def genconf_signature_id(device, _):
+  if device.whitelabel_tag:
+    return device.whitelabel_tag.upper()
+  if device.customization_id:
+    return device.customization_id.upper().partition('-')[0]
+  return device.model
+
+
+def genconf_cras_config_dir(device, _):
+  prefix = '/etc/cras/'
+  if device.cras_config_dir and device.cras_config_dir.startswith(prefix):
+    return device.cras_config_dir[len(prefix):]
+  if device.cras_config_dir:
+    return '../../{}'.format(device.cras_config_dir)
+  return None
+
+
+def genconf_powerd_settings(device, overlay):
+  if not device.powerd_raw:
+    d = {}
+  else:
+    d = json.loads(device.powerd_raw)
+  if 'mosys_eventlog' in overlay.use_flags:
+    d['mosys-eventlog'] = '1'
+  return d
+
+
+M_PUBLIC = (1 << 0)
+M_PRIVATE = (1 << 1)
+
+
+genconf_schema = {
+    'name': (M_PUBLIC | M_PRIVATE, lambda d, _: d.model),
+    'brand-code': (M_PUBLIC, lambda d, _: d.brand_code),
+    'arc': {
+        'build-properties': {
+            'device': (M_PRIVATE, lambda d, _:
+                       d.arc_build_prop('ro.product.device')),
+            'marketing-name': (M_PRIVATE, lambda d, _:
+                               d.arc_build_prop('ro.product.model')),
+            'oem': (M_PRIVATE,
+                    lambda d, _: d.arc_build_prop('ro.product.brand')),
+            'first-api-level': (M_PRIVATE, genconf_first_api_level),
+            'metrics-tag': (M_PRIVATE,
+                            lambda d, _: d.arc_build_prop('ro.product.board')),
+            'product': (M_PRIVATE, lambda d, _:
+                        d.arc_build_prop('ro.product.name')),
+        },
+    },
+    'audio': {
+        'main': {
+            'cras-config-dir': (M_PUBLIC, genconf_cras_config_dir),
+            'ucm-suffix': (M_PUBLIC, lambda d, _: d.internal_ucm_suffix),
+        },
+    },
+    'fingerprint': {
+        'board': (M_PUBLIC, genconf_fp_board),
+        'fingerprint-sensor-type': (M_PUBLIC, genconf_fp_type),
+        'sensor-location': (M_PUBLIC, genconf_fp_location),
+    },
+    'firmware': {
+        'image-name': (M_PUBLIC, lambda d, _: d.model),
+        'name': (M_PRIVATE, lambda d, _: d.model),
+        'bcs-overlay': (M_PRIVATE, lambda _, b:
+                        f'overlay-{b.board_name}-private'),
+        'ec-ro-image': (M_PRIVATE, lambda _, b: b.bcs_ec),
+        'pd-ro-image': (M_PRIVATE, lambda _, b: b.bcs_pd),
+        'main-ro-image': (M_PRIVATE, lambda _, b: b.bcs_main_ro),
+        'main-rw-image': (M_PRIVATE, lambda _, b: b.bcs_main_rw),
+        'build-targets': {
+            'base': (M_PUBLIC, lambda _, b: b.detachable_base_build_target),
+            'coreboot': (M_PUBLIC, lambda _, b: b.board_name),
+            'depthcharge': (M_PUBLIC, lambda _, b: b.board_name),
+            'ec': (M_PUBLIC, lambda _, b: b.ec_build_target),
+            'ec_extras': (M_PUBLIC, lambda _, b: b.ec_extras_build_target),
+        },
+    },
+    'firmware-signing': {
+        'key-id': (M_PRIVATE, lambda d, _: d.model.upper()),
+        'signature-id': (M_PRIVATE, genconf_signature_id),
+    },
+    'hardware-properties': {
+        'psu-type': (M_PUBLIC, genconf_psu_type),
+    },
+    'identity': {
+        'platform-name': (M_PUBLIC, lambda _, b: b.mosys_platform),
+        'smbios-name-match': (M_PUBLIC | M_PRIVATE, lambda d, _: d.smbios_name),
+        'device-tree-compatible-match': (M_PUBLIC | M_PRIVATE,
+                                         genconf_dt_compatible_match),
+    },
+    'power': (M_PUBLIC, genconf_powerd_settings),
+}
+
+
+def genconf(schema, device_conf, overlay_conf):
+
+  def qualifies_as_value(v):
+    return v is not None and v != {}
+
+  if isinstance(schema, dict):
+    pub, priv = {}, {}
+    for k, v in schema.items():
+      pub_r, priv_r = genconf(v, device_conf, overlay_conf)
+      if qualifies_as_value(pub_r):
+        pub[k] = pub_r
+      if qualifies_as_value(priv_r):
+        priv[k] = priv_r
+    return pub, priv
+
+  if isinstance(schema, tuple):
+    pub, priv = None, None
+    flags, func = schema
+    value = func(device_conf, overlay_conf)
+    if flags & M_PUBLIC:
+      pub = value
+    if flags & M_PRIVATE:
+      priv = value
+    return pub, priv
+
+
+def validate_gs_uri(uri):
+  log('Validating {}...'.format(uri))
+  subprocess.run(['gsutil', 'stat', uri], check=True, stdout=subprocess.DEVNULL)
+
+
+def parse_opts(argv):
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--cros-checkout',
+                      type=pathlib.Path,
+                      default=pathlib.Path(os.getenv('HOME')) / 'trunk',
+                      help='Location of the ChromeOS checkout')
+  parser.add_argument('--dut', '-d',
+                      type=str,
+                      required=True,
+                      help='Hostname of DUT to use for querying and testing.')
+  parser.add_argument('--dut-ssh-port', type=int, default=22,
+                      help='SSH port to use on the dut.')
+  parser.add_argument('--board', '-b',
+                      type=str,
+                      required=True,
+                      help='Board name to convert.')
+  parser.add_argument('--mosys-platform', type=str, required=True)
+  parser.add_argument('--dry-run',
+                      action='store_true',
+                      default=False,
+                      help='Dry run')
+  return parser.parse_args(argv)
+
+
+def main(argv):
+  opts = parse_opts(argv)
+
+  overlays = BoardOverlays(opts.board, opts.cros_checkout, opts.mosys_platform)
+  dut = Dut(opts.dut, opts.cros_checkout, port=opts.dut_ssh_port)
+
+  log('Loading configuration from DUT...')
+  dut_config = DeviceConfig.from_dut(dut)
+  log('Got configuration: {}'.format(dut_config))
+
+  assert dut_config.lsb_val('CHROMEOS_RELEASE_BOARD') == opts.board
+  assert dut_config.lsb_val('CHROMEOS_RELEASE_UNIBUILD', '0') != '1'
+
+  log('Generating chromeos-config values...')
+  public_config, private_config = genconf(genconf_schema, dut_config, overlays)
+
+  public_config_yaml = format_yaml(public_config)
+  private_config_yaml = format_yaml(private_config)
+  log('Got public config: \n{}'.format(public_config_yaml))
+  log('Got private config: \n{}'.format(private_config_yaml))
+
+  log('Generating ebuilds...')
+
+  public_vpackage = generate_vpackage(('chromeos-base/chromeos-config-bsp', ))
+  private_vpackage = generate_vpackage(
+      ('chromeos-base/chromeos-config-bsp',
+       'chromeos-base/chromeos-config-bsp-private'))
+  log('Got public vpackage: \n{}'.format(public_vpackage))
+  log('Got private vpackage: \n{}'.format(private_vpackage))
+
+  public_bsp_ebuild = generate_bsp_ebuild()
+  private_bsp_ebuild = generate_bsp_ebuild(private=True)
+  log('Got public bsp_ebuild: \n{}'.format(public_bsp_ebuild))
+  log('Got private bsp_ebuild: \n{}'.format(private_bsp_ebuild))
+
+  firmware_ebuild = generate_firmware_ebuild(opts.board)
+  log('Got firmware ebuild: \n{}'.format(firmware_ebuild))
+
+  public_make_defaults = generate_make_defaults(overlays.public_make_defaults)
+  log('Got public make defaults: \n{}'.format(public_make_defaults))
+  private_make_defaults = generate_make_defaults(overlays.private_make_defaults)
+  log('Got private make defaults: \n{}'.format(private_make_defaults))
+
+  cros_config = CrosConfig(public_config_yaml, private_config_yaml)
+  firmware_srcuris = cros_config.run_host_command('get-firmware-uris')
+  log('Got firmware URIs: {}'.format(firmware_srcuris))
+
+  log('Validating firmware srcuris...')
+  for uri in firmware_srcuris.split():
+    validate_gs_uri(uri)
+
+  firmware_srcuris_path = (overlays.firmware_ebuild_path.parent
+                           / 'files' / 'srcuris')
+
+  if opts.dry_run:
+    return
+
+  overlays.write_file(
+      M_PUBLIC, 'chromeos-base/chromeos-config-bsp/files/model.yaml',
+      public_config_yaml)
+  overlays.write_file(
+      M_PRIVATE, 'chromeos-base/chromeos-config-bsp-private/files/model.yaml',
+      private_config_yaml)
+  overlays.write_file(
+      M_PUBLIC, 'virtual/chromeos-config-bsp/chromeos-config-bsp-9999.ebuild',
+      public_vpackage)
+  overlays.write_file(
+      M_PRIVATE, 'virtual/chromeos-config-bsp/chromeos-config-bsp-9999.ebuild',
+      private_vpackage)
+  overlays.write_file(
+      M_PUBLIC,
+      'chromeos-base/chromeos-config-bsp/chromeos-config-bsp-9999.ebuild',
+      public_bsp_ebuild)
+  overlays.write_file(
+      M_PRIVATE,
+      'chromeos-base/chromeos-config-bsp-private/chromeos-config-bsp-private-9999.ebuild',
+      private_bsp_ebuild)
+  write_file(overlays.firmware_ebuild_path, firmware_ebuild)
+  write_file(firmware_srcuris_path, firmware_srcuris)
+  write_file(overlays.public_make_defaults_file, public_make_defaults)
+  write_file(overlays.private_make_defaults_file, private_make_defaults)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])