• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env vpython3
2# Copyright 2020 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Join information from config_bundle, model.yaml and HWID.
6
7Takes a generated config_bundle payload, optional public and private model.yaml
8 files, and an optional HWID database and merges the data together into a new
9set of generated joined data.
10
11Can optionally generate a new ConfigBundle from just the model.yaml and HWID
12files.  Simple specify a project name with --project-name/-p and omit
13--config-bundle/-c.  At least one of these two options must be specified.
14"""
15
16# pylint: disable=too-many-lines
17
18import argparse
19import json
20import logging
21import os
22import sys
23import tempfile
24import yaml
25
26from google.cloud import bigquery
27
28from merge_plugins.merge_hwid import MergeHwid
29
30from common import config_bundle_utils
31
32from checker import io_utils
33from chromiumos.build.api import firmware_config_pb2
34from chromiumos.config.api import topology_pb2
35from chromiumos.config.payload import config_bundle_pb2
36
37# HWID databases use some custom tags, which are mostly legacy as far as I can
38# tell, so we'll ignore them explicitly to allow the parser to succeed.
39yaml.add_constructor(
40    '!re',
41    lambda loader, node: loader.construct_scalar(node),
42    Loader=yaml.SafeLoader,
43)
44yaml.add_constructor(
45    '!region_field', lambda loader, node: None, Loader=yaml.SafeLoader)
46yaml.add_constructor(
47    '!region_component', lambda loader, node: None, Loader=yaml.SafeLoader)
48
49# git repo locations
50CROS_PLATFORM_REPO = 'https://chromium.googlesource.com/chromiumos/platform2'
51CROS_CONFIG_INTERNAL_REPO = 'https://chrome-internal.googlesource.com/chromeos/config-internal'
52
53# DLM/AVL table configurations
54DLM_PRODUCTS_TABLE = 'cros-device-lifecycle-manager.prod.products'
55DLM_DEVICES_TABLE = 'cros-device-lifecycle-manager.prod.devices'
56
57
58def load_models(public_path, private_path):
59  """Load model.yaml from a public and/or private path."""
60
61  # Have to import this here since we need repos cloned and sys.path set up
62  # pylint: disable=import-outside-toplevel, import-error
63  from cros_config_host import cros_config_schema
64  from libcros_config_host import CrosConfig
65  # pylint: enable=import-outside-toplevel, import-error
66
67  if not (public_path or private_path):
68    return None
69
70  configs = [config for config in [public_path, private_path] if config]
71  with tempfile.TemporaryDirectory() as temp_dir:
72    # Convert the model.yaml files into a payload JSON
73    config_file = os.path.join(temp_dir, 'config.json')
74    cros_config_schema.Main(
75        schema=None, config=None, output=config_file, configs=configs)
76
77    # And load the payload json into a CrosConfigJson object
78    return CrosConfig(config_file)
79
80
81def non_null_values(items):
82  """Unwrap a HWID item block into a dictionary of key => values for non-null values.
83
84  a HWID item block looks like:
85    items:
86      storage_device:
87        status: unsupported
88        values:
89          class: '0x010101'
90          device: '0xff00'
91          sectors: '5000000'
92          vendor: '0xbeef'
93      some_hardware:
94         values:
95      FAKE_RAM_CHIP:
96        values:
97          class: '0x010101'
98          device: '0xff00'
99          sectors: '250000000'
100          vendor: '0xabcd'
101
102  We'll iterate over this and break out the 'values' block, make sure it's not
103  None, and check whether we should exclude it based on the 'status' field if
104  present.
105  """
106
107  def _include(val):
108    if not val['values']:
109      return False
110
111    if 'status' in val and val['status'].lower() == 'unsupported':
112      return False
113    return True
114
115  return [(key, val['values']) for key, val in items.items() if _include(val)]
116
117
118def merge_avl_dlm(config_bundle):
119  """Merge in additional information from AVL/DLM.
120
121  Args:
122    config_bundle: ConfigBundle instance to update
123
124  Returns:
125    reference to updated ConfigBundle
126  """
127
128  def canonical_name(name):
129    """Canonicalize code name for a project."""
130
131    # These rules are from empirical runs with the real project data
132    name = name.lower()
133    if '_' in name:
134      name = name[0:name.find('_')]
135    return name
136
137  client = bigquery.Client(project='chromeos-bot')
138
139  def merge_form_factor(design, name, device_form_factor):
140    """Map from form factor information in DLM to our proto definitions."""
141    if not device_form_factor:
142      logging.warning("Null form factor for '%s', skipping", design)
143      return
144
145    try:
146      form_factor_enum = topology_pb2.HardwareFeatures.FormFactor
147      form_factor = getattr(form_factor_enum, device_form_factor)
148
149      for design_config in design.configs:
150        design_config.hardware_features.form_factor.form_factor = form_factor
151    except AttributeError:
152      logging.warning(
153          "invalid form factor '%s' for '%s'",
154          device_form_factor,
155          name,
156      )
157
158  ##############################################################################
159  ## start of function body
160
161  # canonicalize design names to be compatible with the DLM database
162  project_names = [
163      canonical_name(design.name) for design in config_bundle.design_list
164  ]
165
166  if not project_names:
167    logging.info('no designs to populate from DLM, aborting')
168    return config_bundle
169
170  # query all projects at once, we'll filter them on our side.
171  query = """
172    SELECT googleCodeName, deviceFormFactor
173      FROM {device_table} devices
174      WHERE googleCodeName IN ({projects})
175  """.format(
176      device_table=DLM_DEVICES_TABLE,
177      projects=','.join(["'%s'" % name for name in project_names]),
178  )
179  logging.info(query)
180
181  # sort through DLM information and update config bundle
182  rows = list(client.query(query))
183  for design, name in zip(config_bundle.design_list, project_names):
184    rows = [row for row in rows if row.get('googleCodeName') == name]
185
186    if len(rows) == 0:
187      logging.warning("no results returned for '%s', bad project name?", name)
188      continue
189
190    if len(rows) > 1:
191      logging.warning(
192          "multiple results returned for '%s', cowardly refusing to merge DLM data",
193          name,
194      )
195      continue
196
197    merge_form_factor(design, name, rows[0].get('deviceFormFactor'))
198
199  return config_bundle
200
201
202def merge_audio_config(sw_config, model):
203  """Merge audio configuration from model.yaml into the given sw_config.
204
205  Args:
206    sw_config (SoftwareConfig): software config to update
207    model (CrosConfig): parsed model.yaml information
208
209  Returns:
210    None
211  """
212  audio_props = model.GetProperties('/audio/main')
213  audio_config = sw_config.audio_configs.add()
214  audio_config.ucm_suffix = audio_props.get('ucm-suffix', '')
215
216
217def merge_power_config(sw_config, model):
218  """Merge power configuration from model.yaml into the given sw_config.
219
220  Args:
221    sw_config (SoftwareConfig): software config to update
222    model (CrosConfig): parsed model.yaml information
223
224  Returns:
225    None
226  """
227  power_props = model.GetProperties('/power')
228  power_config = sw_config.power_config
229
230  for key, val in power_props.items():
231    # We don't support autobrightness yet
232    if key == 'autobrightness':
233      continue
234    power_config.preferences[key] = val
235
236
237def merge_bluetooth_config(sw_config, model):
238  """Merge bluetooth configuration from model.yaml into the given sw_config.
239
240  Args:
241    sw_config (SoftwareConfig): software config to update
242    model (CrosConfig): parsed model.yaml information
243
244  Returns:
245    None
246  """
247  bt_props = model.GetProperties('/bluetooth')
248  bt_config = sw_config.bluetooth_config
249
250  for key, val in bt_props.get('flags', {}).items():
251    bt_config.flags[key] = val
252
253
254def merge_firmware_config(sw_config, model):
255  """Merge firmware configuration from model.yaml into the given sw_config.
256
257  Args:
258    sw_config (SoftwareConfig): software config to update
259    model (CrosConfig): parsed model.yaml information
260
261  Returns:
262    None
263  """
264  fw_props = model.GetProperties('/firmware')
265
266  # Populate firmware config
267  fw_config = sw_config.firmware
268  fw_config.main_ro_payload.type = firmware_config_pb2.FirmwareType.MAIN
269  fw_config.main_ro_payload.firmware_image_name = \
270      fw_props.get('main-ro-image', '')
271
272  fw_config.main_rw_payload.type = firmware_config_pb2.FirmwareType.MAIN
273  fw_config.main_rw_payload.firmware_image_name = \
274      fw_props.get('main-rw-image', '')
275
276  fw_config.ec_ro_payload.type = firmware_config_pb2.FirmwareType.EC
277  fw_config.ec_ro_payload.firmware_image_name = \
278      fw_props.get('ec-ro-image', '')
279
280  fw_config.ec_rw_payload.type = firmware_config_pb2.FirmwareType.EC
281  fw_config.ec_rw_payload.firmware_image_name = \
282      fw_props.get('ec-rw-image', '')
283
284  fw_config.pd_ro_payload.type = firmware_config_pb2.FirmwareType.PD
285  fw_config.pd_ro_payload.firmware_image_name = \
286      fw_props.get('pd-ro-image', '')
287
288  # Populate build config
289  build_props = model.GetProperties('/firmware/build-targets')
290
291  build_config = sw_config.firmware_build_config
292  build_config.build_targets.bmpblk = build_props.get('bmpblk', '')
293  build_config.build_targets.coreboot = build_props.get('coreboot', '')
294  build_config.build_targets.depthcharge = build_props.get('depthcharge', '')
295  build_config.build_targets.ec = build_props.get('ec', '')
296  build_config.build_targets.ish = build_props.get('ish', '')
297  build_config.build_targets.libpayload = build_props.get('libpayload', '')
298  build_config.build_targets.zephyr_ec = build_props.get('zephyr-ec', '')
299  build_config.build_targets.zephyr_detachable_base = \
300      build_props.get('zephyr-detachable-base', '')
301
302  for extra in build_props.get('ec-extras', []):
303    build_config.build_targets.ec_extras.append(extra)
304
305
306def merge_camera_config(hw_feat, model):
307  """Merge camera config from model.yaml into the given hardware features.
308
309  Args:
310    hw_feat (HardwareFeatures): hardware features to update
311    model (CrosConfig): parsed model.yaml information
312
313  Returns:
314    None
315  """
316  camera_props = model.GetProperties('/camera')
317  del hw_feat, camera_props
318  # TODO: Merge camera configuration when it's available
319
320
321def merge_buttons(hw_feat, model):
322  """Merge power/volume button information from model.yaml into hardware features.
323
324  Args:
325    hw_feat (HardwareFeatures): hardware features to update
326    model (CrosConfig): parsed model.yaml information
327
328  Returns:
329    None
330  """
331  ui_props = model.GetProperties('/ui')
332  button = topology_pb2.HardwareFeatures.Button
333
334  if 'power-button' in ui_props:
335    edge = ui_props['power-button']['edge']
336    hw_feat.power_button.edge = button.Edge.Value(edge.upper())
337    hw_feat.power_button.position = float(ui_props['power-button']['position'])
338
339  if 'side-volume-button' in ui_props:
340    region = ui_props['side-volume-button']['region']
341    hw_feat.volume_button.region = button.Region.Value(region.upper())
342    side = ui_props['side-volume-button']['side']
343    hw_feat.volume_button.edge = button.Edge.Value(side.upper())
344
345
346def merge_hardware_props(hw_feat, model):
347  """Merge hardware properties from model.yaml into the given hardware features.
348
349  Args:
350    hw_feat (HardwareFeatures): hardware features to update
351    model (CrosConfig): parsed model.yaml information
352
353  Returns:
354    None
355  """
356  form_factor = topology_pb2.HardwareFeatures.FormFactor
357  stylus = topology_pb2.HardwareFeatures.Stylus
358
359  def kw_to_present(config, key):
360    if not key in config:
361      return topology_pb2.HardwareFeatures.PRESENT_UNKNOWN
362    if config[key]:
363      return topology_pb2.HardwareFeatures.PRESENT
364    return topology_pb2.HardwareFeatures.NOT_PRESENT
365
366  hw_props = model.GetProperties('/hardware-properties')
367
368  hw_feat.accelerometer.base_accelerometer = \
369      kw_to_present(hw_props, 'has-base-accelerometer')
370  hw_feat.accelerometer.lid_accelerometer = \
371      kw_to_present(hw_props, 'has-lid-accelerometer')
372  hw_feat.gyroscope.base_gyroscope = \
373      kw_to_present(hw_props, 'has-base-gyroscope')
374  hw_feat.gyroscope.lid_gyroscope = \
375      kw_to_present(hw_props, 'has-lid-gyroscope')
376  hw_feat.light_sensor.base_lightsensor = \
377      kw_to_present(hw_props, 'has-base-light-sensor')
378  hw_feat.light_sensor.lid_lightsensor = \
379      kw_to_present(hw_props, 'has-lid-light-sensor')
380  hw_feat.light_sensor.camera_lightsensor = \
381      kw_to_present(hw_props, 'has-camera-light-sensor')
382  hw_feat.magnetometer.base_magnetometer = \
383      kw_to_present(hw_props, 'has-base-magnetometer')
384  hw_feat.magnetometer.lid_magnetometer = \
385      kw_to_present(hw_props, 'has-lid-magnetometer')
386  hw_feat.screen.touch_support = \
387      kw_to_present(hw_props, 'has-touchscreen')
388
389  hw_feat.form_factor.form_factor = form_factor.FORM_FACTOR_UNKNOWN
390  if hw_props.get('is-lid-convertible', False):
391    hw_feat.form_factor.form_factor = form_factor.CONVERTIBLE
392
393  stylus_val = hw_props.get('stylus-category', '')
394  if not stylus_val:
395    hw_feat.stylus.stylus = stylus.STYLUS_UNKNOWN
396  if stylus_val == 'none':
397    hw_feat.stylus.stylus = stylus.NONE
398  if stylus_val == 'internal':
399    hw_feat.stylus.stylus = stylus.INTERNAL
400  if stylus_val == 'external':
401    hw_feat.stylus.stylus = stylus.EXTERNAL
402
403
404def merge_fingerprint_config(hw_feat, model):
405  """Merge fingerprint config from model.yaml into the given hardware features.
406
407  Args:
408    hw_feat (HardwareFeatures): hardware features to update
409    model (CrosConfig): parsed model.yaml information
410
411  Returns:
412    None
413  """
414  location = topology_pb2.HardwareFeatures.Fingerprint.Location
415
416  fing_prop = model.GetProperties('/fingerprint')
417  hw_feat.fingerprint.board = fing_prop.get('board', '')
418
419  sensor_location = fing_prop.get('sensor-location', 'none')
420  if sensor_location == 'none':
421    hw_feat.fingerprint.present = False
422  else:
423    hw_feat.fingerprint.present = True
424    hw_feat.fingerprint.location = location.Value(
425        sensor_location.upper().replace('-', '_'))
426
427
428def merge_device_brand(config_bundle, design, model, project_name):
429  """Merge brand information from model.yaml into specific Design instance.
430
431  The ConfigBundle and Design protos are updated in place with the information
432  from model.yaml.
433
434  In general we'll have a 1:1 mapping with Design to Brand information so we
435  create a new DeviceBrand and link it to a new Brand_Config value.
436
437  Args:
438    config_bundle (ConfigBundle): top level ConfigBundle to update
439    design (Design): design in the config bundle to update
440    model (CrosConfig): parsed model.yaml information
441    project_name (str): name of the device (eg: phaser)
442
443  Returns:
444    A reference to the input ConfigBundle updated with data from model
445  """
446
447  # pylint: disable=too-many-locals
448
449  whitelabel = model.GetProperties('/identity/whitelabel-tag')
450  whitelabel = whitelabel or ''
451
452  # find/create new brand entry for the design
453  brand_name = ''
454  brand_code = model.GetProperties('/brand-code')
455  brand_id = '{}_{}'.format(project_name, brand_code)
456
457  # find/create device brand
458  device_brand = None
459  for brand in config_bundle.device_brand_list:
460    if (brand.design_id == design.id and brand.brand_name == brand_name and
461        brand.brand_code == brand_code):
462      device_brand = brand
463      break
464
465  if not device_brand:
466    device_brand = config_bundle.device_brand_list.add()
467    device_brand.id.value = brand_id
468    device_brand.design_id.MergeFrom(design.id)
469    device_brand.brand_name = ''
470    device_brand.brand_code = brand_code
471
472  # find/create brand config
473  brand_config = None
474  for config in config_bundle.brand_configs:
475    if (config.brand_id == device_brand.id and
476        config.scan_config.whitelabel_tag == whitelabel):
477      brand_config = config
478      break
479
480  if not brand_config:
481    brand_config = config_bundle.brand_configs.add()
482    brand_config.brand_id.MergeFrom(device_brand.id)
483    brand_config.scan_config.whitelabel_tag = whitelabel
484
485  wallpaper = model.GetWallpaperFiles()
486  if wallpaper:
487    brand_config.wallpaper = wallpaper.pop()
488
489  regulatory_label = model.GetProperties('/regulatory-label')
490  if regulatory_label:
491    brand_config.regulatory_label = regulatory_label
492
493  help_tags = [
494      '/ui/help-content-id',
495      '/identity/customization-id',
496      '/name',
497  ]
498
499  for tag in help_tags:
500    help_content = model.GetProperties(tag)
501    if help_content:
502      brand_config.help_content_id = help_content
503      break
504
505  return config_bundle
506
507
508def merge_model(config_bundle, design_config, model):
509  """Merge model from model.yaml into a specific Design.Config instance.
510
511  The ConfigBundle, and Design.Config are updated in place with
512  model.yaml information.
513
514  Args:
515    config_bundle (ConfigBundle): top level ConfigBundle to update
516    design_config (Design.Config): design config in the config bundle to update
517    model (CrosConfig): parsed model.yaml information
518
519  Returns:
520    A reference to the input config_bundle updated with data from model
521  """
522
523  identity = model.GetProperties('/identity')
524
525  # Merge hardware configuration
526  hw_feat = design_config.hardware_features
527  merge_fingerprint_config(hw_feat, model)
528  merge_hardware_props(hw_feat, model)
529  merge_camera_config(hw_feat, model)
530  merge_buttons(hw_feat, model)
531
532  # Merge software configuration
533  sw_config = None
534  for config in config_bundle.software_configs:
535    if config.design_config_id == design_config.id:
536      sw_config = config
537      break
538
539  # Already have software config for this design_config, so don't re-populate
540  if sw_config:
541    return config_bundle
542
543  sw_config = config_bundle.software_configs.add()
544  sw_config.design_config_id.MergeFrom(design_config.id)
545
546  sw_config.id_scan_config.firmware_sku = identity.get('sku-id', 0xFFFFFFFF)
547
548  if 'frid' in identity:
549    sw_config.id_scan_config.frid = identity['frid']
550
551  merge_firmware_config(sw_config, model)
552  merge_bluetooth_config(sw_config, model)
553  merge_power_config(sw_config, model)
554  merge_audio_config(sw_config, model)
555
556  return config_bundle
557
558
559def merge_configs(options):
560  # pylint: disable=too-many-locals
561  # pylint: disable=too-many-branches
562  # pylint: disable=too-many-statements
563  """Read and merge configs together, generating new config_bundle output."""
564
565  config_bundle_path = options.config_bundle
566  program_name = options.program_name
567  project_name = options.project_name
568  public_path = options.public_model
569  private_path = options.private_model
570  hwid_path = options.hwid
571
572  def safe_equal(stra, strb):
573    """return True if inputs are equal ignoring case and edge whitespace"""
574    return stra.lower().strip() == strb.lower().strip()
575
576  # Set of models to ensure exist in the output.
577  ensure_models = set([project_name])
578
579  # generate canonical program ID
580  program_id = program_name.lower()
581
582  config_bundle = config_bundle_pb2.ConfigBundle()
583  if config_bundle_path:
584    # if we're only importing, then save designs to ensure exist
585    input_bundle = io_utils.read_config(config_bundle_path)
586    if options.import_only:
587      for design in input_bundle.design_list:
588        if program_name and \
589           not safe_equal(design.program_id.value, program_id):
590          continue
591        ensure_models.add(design.name.lower())
592
593      # Sort to ensure ordering is consistent
594      ensure_models = sorted(ensure_models)
595      logging.debug('ensuring models: %s', ensure_models)
596    else:
597      # we're joining payloads so expose full config bundle for merging
598      config_bundle = input_bundle
599
600  # ensure that a program entry is added for the manually specified program name
601  config_bundle_utils.find_program(config_bundle, program_id, create=True)
602
603  models = load_models(public_path, private_path)
604
605  def find_design(name_program, name_project):
606    """Searches config_bundle for a matching design_config.
607
608    Args:
609      name_program (str): program name
610      name_project (str): project name
611
612    Returns:
613      Either found Design for input parameters or new one created and placed
614      in the config_bundle.
615    """
616
617    # find program
618    program = config_bundle_utils.find_program(
619        config_bundle,
620        name_program.lower(),
621    )
622
623    for design in config_bundle.design_list:
624      # skip other program designs (shouldn't happen)
625      if not safe_equal(program.id.value, design.program_id.value):
626        continue
627
628      if safe_equal(name_project, design.name):
629        return design
630
631    # no design found, create one
632    design = config_bundle.design_list.add()
633    design.id.value = name_project.lower()
634    design.name = name_project.lower()
635    design.program_id.MergeFrom(program.id)
636    return design
637
638  def find_design_config(name_program, name_project, sku):
639    """Searches config_bundle for a matching design_config.
640
641    Args:
642      name_program (str): program name
643      name_project (str): project name
644      sku (str): specific sku
645
646    Returns:
647      Either found Design and Design.Config for input parameters or new ones
648      create and placed in the config_bundle.
649    """
650
651    design = find_design(name_program, name_project)
652
653    for config in design.configs:
654      design_sku = config.id.value.lower().split(':')[-1]
655      if safe_equal(design_sku, sku):
656        return design, config
657
658    # Create new Design.Config, the board id is encoded according to CBI:
659    #   https://chromium.googlesource.com/chromiumos/docs/+/HEAD/design_docs/cros_board_info.md
660    config = design.configs.add()
661    config.id.value = '{}:{}'.format(name_project.lower(), sku)
662    return design, config
663
664  ### start of function body
665
666  # GetDeviceConfigs() will return an entry for all combinations of:
667  #     (program, project, sku, whitelabel)
668  # so we need to be careful not to create duplicate entries.
669  for model in models.GetDeviceConfigs() if models else []:
670    identity = model.GetProperties('/identity')
671    project = model.GetName()
672    assert project, 'project name is undefined'
673
674    sku = identity.get('sku-id')
675    if not sku:
676      # sku not defined, SKUs are by definition < 0x7FFFFFF so we'll use
677      # 0x8000000 for the SKU-less case to keep it an integer
678      sku = '0x80000000'
679      logging.info('found wildcard sku in %s, setting sku-id to "%s"', project,
680                   sku)
681    sku = str(sku)
682
683    if sku == '255':
684      logging.info('skipping unprovisioned sku %s', sku)
685      continue
686
687    # ignore other projects
688    if not safe_equal(project_name, project):
689      continue
690
691    # Lookup design config for this specific device
692    design, design_config = find_design_config(program_name, project, sku)
693
694    merge_device_brand(config_bundle, design, model, project_name)
695    merge_model(config_bundle, design_config, model)
696
697  # ensure that all our required model names exist.
698  for project in ensure_models:
699    design = find_design(program_name, project)
700
701  # Merge information from HWID into config bundle
702  if hwid_path:
703    merger = MergeHwid(hwid_path)
704    merger.merge(config_bundle)
705
706    if options.hwid_residual:
707      with open(options.hwid_residual, 'w', encoding='utf-8') as outfile:
708        json.dump(merger.residual(), outfile, indent=2)
709
710  return config_bundle
711
712
713def main(options):
714  """Runs the script."""
715
716  def clone_repo(repo, path):
717    """Clone a given repo to the given path in the file system."""
718    cmd = 'git clone -q --depth=1 --shallow-submodules {repo} {path}'.format(
719        repo=repo, path=path)
720    print('Creating shallow clone of {repo} ({cmd})'.format(repo=repo, cmd=cmd))
721    os.system(cmd)
722
723  def clone_or_use_dep(repo, clone_path, dep_path, sub_path=''):
724    """Either use a dependency in-place or clone it and use that.
725
726    If the dependency doesn't exist at dep_path, then clone it into clone_path.
727    Either way, add the path/{sub_path} to sys.path.
728
729    Args:
730      repo (str): url of the git repo for the dependency
731      clone_path (str): where to clone repo if neeeded
732      dep_path (str): location on disk the path might be located
733      sub_path (str): path inside of repo to add to sys.path
734
735    Returns:
736      nothing
737    """
738    root_path = dep_path
739    if not os.path.exists(root_path):
740      clone_repo(repo, clone_path)
741      root_path = clone_path
742    sys.path.append(os.path.join(root_path, sub_path))
743
744  if not (options.config_bundle or options.project_name):
745    raise RuntimeError(
746        'At least one of {config_opt} or {project_opt} must be specified.'
747        .format(
748            config_opt='--config-bundle/-c', project_opt='--project-name/-p'))
749
750  with tempfile.TemporaryDirectory(prefix='join_proto_') as temppath:
751    this_dir = os.path.realpath(os.path.dirname(__file__))
752
753    clone_or_use_dep(
754        CROS_PLATFORM_REPO,
755        os.path.join(temppath, 'platform2'),
756        os.path.realpath(os.path.join(this_dir, '../../platform2')),
757        'chromeos-config',
758    )
759
760    clone_or_use_dep(
761        CROS_CONFIG_INTERNAL_REPO,
762        os.path.join(temppath, 'config-internal'),
763        os.path.realpath(os.path.join(this_dir, '../../config-internal')),
764    )
765
766    io_utils.write_message_json(
767        merge_avl_dlm(merge_configs(options)),
768        options.output,
769    )
770
771
772if __name__ == '__main__':
773  parser = argparse.ArgumentParser(description=__doc__)
774  parser.add_argument(
775      '-o',
776      '--output',
777      type=str,
778      required=True,
779      help='output file to write joined ConfigBundle jsonproto to')
780
781  parser.add_argument(
782      '-p',
783      '--project-name',
784      type=str,
785      required=True,
786      help="""When specified without --config-bundle/-c, this species the project name to
787generate ConfigBundle information for from the model.yaml/HWID files.  When
788specified with --config-bundle/-c, then only projects with this name will be
789updated.""")
790
791  parser.add_argument(
792      '--program-name',
793      type=str,
794      required=True,
795      help="""Program name to add to the output ConfigBundle.  This program will be
796added to the program_list even if there are no designs present.""")
797
798  parser.add_argument(
799      '-c',
800      '--config-bundle',
801      type=str,
802      help="""generated config_bundle payload in jsonpb format
803(eg: generated/config.jsonproto).  If not specified, an empty ConfigBundle
804instance is used instead.""")
805
806  parser.add_argument(
807      '--import-only',
808      action='store_true',
809      help="""When specified, don't use values from --config-bundle directly.  Instead,
810only use the config bundle to propagate models to imported payload.""")
811
812  parser.add_argument(
813      '--hwid-residual',
814      type=str,
815      help='when given, write remaining unparsed HWID information to this file',
816  )
817
818  parser.add_argument(
819      '--public-model', type=str, help='public model.yaml file to merge')
820  parser.add_argument(
821      '--private-model', type=str, help='private model.yaml file to merge')
822  parser.add_argument('--hwid', type=str, help='HWID database to merge')
823  parser.add_argument(
824      '-v', '--verbose', help='increase output verbosity', action='store_true')
825  parser.add_argument('-l', '--log', type=str, help='set logging level')
826
827  args = parser.parse_args()
828  # pylint: disable=invalid-name
829  loglevel = logging.INFO if args.verbose else logging.WARNING
830  if args.log:
831    loglevel = {
832        'critical': logging.CRITICAL,
833        'error': logging.ERROR,
834        'warning': logging.WARNING,
835        'info': logging.INFO,
836        'debug': logging.DEBUG,
837    }.get(args.log.lower())
838
839    if not loglevel:
840      logging.error("invalid value for -l/--log '%s'", args.log)
841
842  logging.basicConfig(level=loglevel)
843  main(args)
844