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