|  | #!/usr/bin/env python3.8 | 
|  | # Copyright 2020 The Fuchsia Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  | """Validate the sysmgr config for the product being built. | 
|  |  | 
|  | Sysmgr's configuration is provided through the config-data package as a set of | 
|  | JSON files, which are all individually read by sysmgr at runtime and merged to | 
|  | form its overall configuration. This tool validates that there are no conflicts | 
|  | between files, e.g. multiple files providing different component URLs for the | 
|  | same "services" key. For example, it catches invalid configuration like this:: | 
|  |  | 
|  |  | 
|  | file1.config: | 
|  | { | 
|  | "services": { | 
|  | "fuchsia.my.Service": "fuchsia-pkg://fuchsia.com/package_a#meta/foo.cmx" | 
|  | } | 
|  | } | 
|  |  | 
|  | file2.config: | 
|  | { | 
|  | "services": { | 
|  | "fuchsia.my.Service": "fuchsia-pkg://fuchsia.com/package_b#meta/bar.cmx" | 
|  | } | 
|  | } | 
|  |  | 
|  | The input provided to this tool is expected to be the config-data package | 
|  | manifest, formatted like this:: | 
|  |  | 
|  | meta/data/some_package/foo=../../src/somewhere/foo | 
|  | meta/data/sysmgr/services.config=../../src/sys/sysmgr/config/services.config | 
|  | meta/data/sysmgr/file1.config=../../src/bar/file1.config | 
|  | meta/data/other_package/baz=../../some/other/path/baz | 
|  |  | 
|  | where the path before the '=' is the destination path in the package, and the | 
|  | path after the '=' is the source file (rebased to the root build directory). | 
|  | """ | 
|  |  | 
|  | from collections import defaultdict | 
|  | import argparse | 
|  | import json | 
|  | import os | 
|  | import sys | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument( | 
|  | '--depfile', | 
|  | type=argparse.FileType('w'), | 
|  | help='Write a GN depfile to the given path', | 
|  | required=True) | 
|  | parser.add_argument( | 
|  | '--merged', | 
|  | type=argparse.FileType('w'), | 
|  | help='Write a snapshot of this sysmgr config to the given path', | 
|  | required=True) | 
|  | parser.add_argument( | 
|  | '--config-entries', | 
|  | type=argparse.FileType('r'), | 
|  | help='config_data entries', | 
|  | required=True) | 
|  | args = parser.parse_args() | 
|  |  | 
|  | # Build a list of all the source paths that contribute to sysmgr's config-data | 
|  | config_entries = json.load(args.config_entries) | 
|  | sysmgr_config_files = [ | 
|  | e['source'] | 
|  | for e in config_entries | 
|  | if e['destination'].startswith('meta/data/sysmgr/') | 
|  | ] | 
|  |  | 
|  | # Parse all config files. | 
|  | # | 
|  | # Build a list of all conflicts, rather than | 
|  | # failing immediately on the first conflict. This allows us to print a more | 
|  | # useful build failure so that developers don't need to play whack-a-mole with | 
|  | # multiple conflicts. | 
|  | configs = [] | 
|  | files_by_service = defaultdict(list) | 
|  | for config_file in sysmgr_config_files: | 
|  | with open(config_file, 'r') as f: | 
|  | config = json.load(f) | 
|  | configs.append(config) | 
|  |  | 
|  | services = config.get('services') | 
|  | if services: | 
|  | for service in services.keys(): | 
|  | files_by_service[service].append(config_file) | 
|  |  | 
|  | # If any conflicts were detected, print a useful error message and then | 
|  | # exit. | 
|  | service_conflicts = False | 
|  | for service, config_files in files_by_service.items(): | 
|  | if len(config_files) > 1: | 
|  | print( | 
|  | 'Duplicate sysmgr configuration for service {} in files: {}'. | 
|  | format(service, ', '.join(config_files))) | 
|  | service_conflicts = True | 
|  |  | 
|  | if service_conflicts: | 
|  | return 1 | 
|  |  | 
|  | # Create a single merged configuration analogous to sysmgr's init itself. | 
|  | merged_config = {} | 
|  | for config in configs: | 
|  | for category, values in config.items(): | 
|  | existing = merged_config.get(category) | 
|  | if type(existing) is dict: | 
|  | merged_config[category].update(values) | 
|  | elif type(existing) is list: | 
|  | merged_config[category] += values | 
|  | else: | 
|  | merged_config[category] = values | 
|  |  | 
|  | json.dump( | 
|  | merged_config, | 
|  | args.merged, | 
|  | indent=2, | 
|  | separators=(',', ': '), | 
|  | sort_keys=True) | 
|  |  | 
|  | # Write the depfile, which is a Makefile format file that has a single output | 
|  | # (the merged file) and lists all input files as dependencies. | 
|  | args.depfile.write( | 
|  | '{}: {}\n'.format(args.merged, ' '.join(sysmgr_config_files))) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main()) |