blob: 8823580fee9308a82771aaba243cf48ff5708c91 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2022 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.
"""A command-line utility to merge from two ConfigBundle protos. One
ConfigBundle is generated from Starlark source files, another is generated from
external data sources such as HWID and model.yaml (i.e. "backfilled").
For each repeated field in ConfigBundle, all elements in
the backfilled ConfigBundle that are not in the original ConfigBundle are added
to the original ConfigBundle. The id field of each message is used to determine
equality.
For example, in the components repeated field, each Component's id.value
field is used to determine whether to add it to config_bundle.
"""
import argparse
import pathlib
from typing import Callable, Sequence, TypeVar
from chromiumos.config.payload import config_bundle_pb2
from google.protobuf import json_format
def _parse_config_bundle(path: pathlib.Path) -> config_bundle_pb2.ConfigBundle:
cb = config_bundle_pb2.ConfigBundle()
json_format.Parse(path.read_text(), cb)
return cb
M = TypeVar('M')
def _merge_sequences_by_key(seq1: Sequence[M], seq2: Sequence[M],
keyFn: Callable[[M], str]):
"""Merges two sequences containing the same type of objects.
For each element in seq2, if seq1 does not contain the element, as
determined by keyFn, the element is added to seq1. If both sequences contain
an element, it is not added to seq1.
Args:
seq1: The sequence to be modified.
seq2: The sequence whose elements will be added to seq1.
keyFn: A function to map from the object type to strings.
"""
seq1Keys = set()
for elem in seq1:
seq1Keys.add(keyFn(elem))
for elem in seq2:
if keyFn(elem) not in seq1Keys:
seq1.append(elem)
def merge_config_bundles(
config_bundle: config_bundle_pb2.ConfigBundle,
backfilled_config_bundle: config_bundle_pb2.ConfigBundle,
):
"""Merges backfilled_config_bundle into config_bundle.
For each repeated field in ConfigBundle, all elements in
backfilled_config_bundle that are not in config_bundle are added to
config_bundle. The id field of each message is used to determine equality.
For example, in the components repeated field, each Component's id.value
field is used to determine whether to add it to config_bundle.
Args:
config_bundle: Original ConfigBundle to merge into.
backfilled_config_bundle: ConfigBundle generated by the backfiller to
merge elements from.
"""
_merge_sequences_by_key(config_bundle.partner_list,
backfilled_config_bundle.partner_list,
lambda p: p.id.value)
_merge_sequences_by_key(
config_bundle.components,
backfilled_config_bundle.components,
lambda c: c.id.value,
)
_merge_sequences_by_key(
config_bundle.program_list,
backfilled_config_bundle.program_list,
lambda p: p.id.value,
)
_merge_sequences_by_key(
config_bundle.design_list,
backfilled_config_bundle.design_list,
lambda d: d.id.value,
)
_merge_sequences_by_key(
config_bundle.device_brand_list,
backfilled_config_bundle.device_brand_list,
lambda db: db.id.value,
)
_merge_sequences_by_key(
config_bundle.software_configs,
backfilled_config_bundle.software_configs,
lambda s: s.design_config_id.value,
)
_merge_sequences_by_key(
config_bundle.brand_configs,
backfilled_config_bundle.brand_configs,
lambda b: b.id.value,
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--config-bundle',
type=pathlib.Path,
required=True,
help=('Path to a ConfigBundle generated from Starlark source files, '
'in jsonpb format.'),
)
parser.add_argument(
'--backfilled-config-bundle',
type=pathlib.Path,
required=True,
help=('Path to a ConfigBundle generated from external data sources, '
'i.e. the "backfilled" ConfigBundle, in jsonpb format.'),
)
parser.add_argument('--output',
type=pathlib.Path,
required=True,
help='Path to write the merged ConfigBundle.')
args = parser.parse_args()
config_bundle = _parse_config_bundle(args.config_bundle)
backfilled_config_bundle = _parse_config_bundle(
args.backfilled_config_bundle)
merge_config_bundles(config_bundle, backfilled_config_bundle)
args.output.write_text(json_format.MessageToJson(config_bundle, sort_keys=True))
if __name__ == '__main__':
main()