| #!/usr/bin/env python3 |
| # Copyright (C) 2021 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import copy |
| import json |
| import logging |
| import os |
| import sys |
| import yaml |
| from collections import defaultdict |
| from typing import ( |
| List, |
| Set, |
| ) |
| |
| import utils |
| |
| # SKIP_COMPONENT_SEARCH = ( |
| # 'tools', |
| # ) |
| COMPONENT_METADATA_DIR = '.repo' |
| COMPONENT_METADATA_FILE = 'treeinfo.yaml' |
| GENERATED_METADATA_FILE = 'metadata.json' |
| COMBINED_METADATA_FILENAME = 'multitree_meta.json' |
| |
| |
| class Dep(object): |
| def __init__(self, name, component, deps_type): |
| self.name = name |
| self.component = component |
| self.type = deps_type |
| self.out_paths = list() |
| |
| |
| class ExportedDep(Dep): |
| def __init__(self, name, component, deps_type): |
| super().__init__(name, component, deps_type) |
| |
| def setOutputPaths(self, output_paths: list): |
| self.out_paths = output_paths |
| |
| |
| class ImportedDep(Dep): |
| required_type_map = { |
| # import type: (required type, get imported module list) |
| utils.META_FILEGROUP: (utils.META_MODULES, True), |
| } |
| |
| def __init__(self, name, component, deps_type, import_map): |
| super().__init__(name, component, deps_type) |
| self.exported_deps: Set[ExportedDep] = set() |
| self.imported_modules: List[str] = list() |
| self.required_type = deps_type |
| get_imported_module = False |
| if deps_type in ImportedDep.required_type_map: |
| self.required_type, get_imported_module = ImportedDep.required_type_map[deps_type] |
| if get_imported_module: |
| self.imported_modules = import_map[name] |
| else: |
| self.imported_modules.append(name) |
| |
| def verify_and_add(self, exported: ExportedDep): |
| if self.required_type != exported.type: |
| raise RuntimeError( |
| '{comp} components imports {module} for {imp_type} but it is exported as {exp_type}.' |
| .format(comp=self.component, module=exported.name, imp_type=self.required_type, exp_type=exported.type)) |
| self.exported_deps.add(exported) |
| self.out_paths.extend(exported.out_paths) |
| # Remove duplicates. We may not use set() which is not JSON serializable |
| self.out_paths = list(dict.fromkeys(self.out_paths)) |
| |
| |
| class MetadataCollector(object): |
| """Visit all component directories and collect the metadata from them. |
| |
| Example of metadata: |
| ========== |
| build_cmd: m # build command for this component. 'm' if omitted |
| out_dir: out # out dir of this component. 'out' if omitted |
| exports: |
| libraries: |
| - name: libopenjdkjvm |
| - name: libopenjdkjvmd |
| build_cmd: mma # build command for libopenjdkjvmd if specified |
| out_dir: out/soong # out dir for libopenjdkjvmd if specified |
| - name: libctstiagent |
| APIs: |
| - api1 |
| - api2 |
| imports: |
| libraries: |
| - lib1 |
| - lib2 |
| APIs: |
| - import_api1 |
| - import_api2 |
| lunch_targets: |
| - arm64 |
| - x86_64 |
| """ |
| |
| def __init__(self, component_top, out_dir, meta_dir, meta_file, force_update=False): |
| if not os.path.exists(out_dir): |
| os.makedirs(out_dir) |
| |
| self.__component_top = component_top |
| self.__out_dir = out_dir |
| self.__metadata_path = os.path.join(meta_dir, meta_file) |
| self.__combined_metadata_path = os.path.join(self.__out_dir, |
| COMBINED_METADATA_FILENAME) |
| self.__force_update = force_update |
| |
| self.__metadata = dict() |
| self.__map_exports = dict() |
| self.__component_set = set() |
| |
| def collect(self): |
| """ Read precomputed combined metadata from the json file. |
| |
| If any components have updated their metadata, update the metadata |
| information and the json file. |
| """ |
| timestamp = self.__restore_metadata() |
| if timestamp and os.path.getmtime(__file__) > timestamp: |
| logging.info('Update the metadata as the orchestrator has been changed') |
| self.__force_update = True |
| self.__collect_from_components(timestamp) |
| |
| def get_metadata(self): |
| """ Returns collected metadata from all components""" |
| if not self.__metadata: |
| logging.warning('Metadata is empty') |
| return copy.deepcopy(self.__metadata) |
| |
| def __collect_from_components(self, timestamp): |
| """ Read metadata from all components |
| |
| If any components have newer metadata files or are removed, update the |
| combined metadata. |
| """ |
| metadata_updated = False |
| for component in os.listdir(self.__component_top): |
| # if component in SKIP_COMPONENT_SEARCH: |
| # continue |
| if self.__read_component_metadata(timestamp, component): |
| metadata_updated = True |
| if self.__read_generated_metadata(timestamp, component): |
| metadata_updated = True |
| |
| deleted_components = set() |
| for meta in self.__metadata: |
| if meta not in self.__component_set: |
| logging.info('Component {} is removed'.format(meta)) |
| deleted_components.add(meta) |
| metadata_updated = True |
| for meta in deleted_components: |
| del self.__metadata[meta] |
| |
| if metadata_updated: |
| self.__update_dependencies() |
| self.__store_metadata() |
| logging.info('Metadata updated') |
| |
| def __read_component_metadata(self, timestamp, component): |
| """ Search for the metadata file from a component. |
| |
| If the metadata is modified, read the file and update the metadata. |
| """ |
| component_path = os.path.join(self.__component_top, component) |
| metadata_file = os.path.join(component_path, self.__metadata_path) |
| logging.info( |
| 'Reading a metadata file from {} component ...'.format(component)) |
| if not os.path.isfile(metadata_file): |
| logging.warning('Metadata file {} not found!'.format(metadata_file)) |
| return False |
| |
| self.__component_set.add(component) |
| if not self.__force_update and timestamp and timestamp > os.path.getmtime(metadata_file): |
| logging.info('... yaml not changed. Skip') |
| return False |
| |
| with open(metadata_file) as f: |
| meta = yaml.load(f, Loader=yaml.SafeLoader) |
| |
| meta['path'] = component_path |
| if utils.META_BUILDCMD not in meta: |
| meta[utils.META_BUILDCMD] = utils.DEFAULT_BUILDCMD |
| if utils.META_OUTDIR not in meta: |
| meta[utils.META_OUTDIR] = utils.DEFAULT_OUTDIR |
| |
| if utils.META_IMPORTS not in meta: |
| meta[utils.META_IMPORTS] = defaultdict(dict) |
| if utils.META_EXPORTS not in meta: |
| meta[utils.META_EXPORTS] = defaultdict(dict) |
| |
| self.__metadata[component] = meta |
| return True |
| |
| def __read_generated_metadata(self, timestamp, component): |
| """ Read a metadata gerated by 'update-meta' build command from the soong build system |
| |
| Soong generate the metadata that has the information of import/export module/files. |
| Build orchestrator read the generated metadata to collect the dependency information. |
| |
| Generated metadata has the following format: |
| { |
| "Imported": { |
| "FileGroups": { |
| "<name_of_filegroup>": [ |
| "<exported_module_name>", |
| ... |
| ], |
| ... |
| } |
| } |
| "Exported": { |
| "<exported_module_name>": [ |
| "<output_file_path>", |
| ... |
| ], |
| ... |
| } |
| } |
| """ |
| if component not in self.__component_set: |
| # skip reading generated metadata if the component metadata file was missing |
| return False |
| component_out = os.path.join(self.__component_top, component, self.__metadata[component][utils.META_OUTDIR]) |
| generated_metadata_file = os.path.join(component_out, 'soong', 'multitree', GENERATED_METADATA_FILE) |
| if not os.path.isfile(generated_metadata_file): |
| logging.info('... Soong did not generated the metadata file. Skip') |
| return False |
| if not self.__force_update and timestamp and timestamp > os.path.getmtime(generated_metadata_file): |
| logging.info('... Soong generated metadata not changed. Skip') |
| return False |
| |
| with open(generated_metadata_file, 'r') as gen_meta_json: |
| try: |
| gen_metadata = json.load(gen_meta_json) |
| except json.decoder.JSONDecodeError: |
| logging.warning('JSONDecodeError!!!: skip reading the {} file'.format( |
| generated_metadata_file)) |
| return False |
| |
| if utils.SOONG_IMPORTED in gen_metadata: |
| imported = gen_metadata[utils.SOONG_IMPORTED] |
| if utils.SOONG_IMPORTED_FILEGROUPS in imported: |
| self.__metadata[component][utils.META_IMPORTS][utils.META_FILEGROUP] = imported[utils.SOONG_IMPORTED_FILEGROUPS] |
| if utils.SOONG_EXPORTED in gen_metadata: |
| self.__metadata[component][utils.META_EXPORTS][utils.META_MODULES] = gen_metadata[utils.SOONG_EXPORTED] |
| |
| return True |
| |
| def __update_export_map(self): |
| """ Read metadata of all components and update the export map |
| |
| 'libraries' and 'APIs' are special exproted types that are provided manually |
| from the .yaml metadata files. These need to be replaced with the implementation |
| in soong gerated metadata. |
| The export type 'module' is generated from the soong build system from the modules |
| with 'export: true' property. This export type includes a dictionary with module |
| names as keys and their output files as values. These output files will be used as |
| prebuilt sources when generating the imported modules. |
| """ |
| self.__map_exports = dict() |
| for comp in self.__metadata: |
| if utils.META_EXPORTS not in self.__metadata[comp]: |
| continue |
| exports = self.__metadata[comp][utils.META_EXPORTS] |
| |
| for export_type in exports: |
| for module in exports[export_type]: |
| if export_type == utils.META_LIBS: |
| name = module[utils.META_LIB_NAME] |
| else: |
| name = module |
| |
| if name in self.__map_exports: |
| raise RuntimeError( |
| 'Exported libs conflict!!!: "{name}" in the {comp} component is already exported by the {prev} component.' |
| .format(name=name, comp=comp, prev=self.__map_exports[name][utils.EXP_COMPONENT])) |
| exported_deps = ExportedDep(name, comp, export_type) |
| if export_type == utils.META_MODULES: |
| exported_deps.setOutputPaths(exports[export_type][module]) |
| self.__map_exports[name] = exported_deps |
| |
| def __verify_and_add_dependencies(self, component): |
| """ Search all imported items from the export_map. |
| |
| If any imported items are not provided by the other components, report |
| an error. |
| Otherwise, add the component dependency and update the exported information to the |
| import maps. |
| """ |
| def verify_and_add_dependencies(imported_dep: ImportedDep): |
| for module in imported_dep.imported_modules: |
| if module not in self.__map_exports: |
| raise RuntimeError( |
| 'Imported item not found!!!: Imported module "{module}" in the {comp} component is not exported from any other components.' |
| .format(module=module, comp=imported_dep.component)) |
| imported_dep.verify_and_add(self.__map_exports[module]) |
| |
| deps = self.__metadata[component][utils.META_DEPS] |
| exp_comp = self.__map_exports[module].component |
| if exp_comp not in deps: |
| deps[exp_comp] = defaultdict(defaultdict) |
| deps[exp_comp][imported_dep.type][imported_dep.name] = imported_dep.out_paths |
| |
| self.__metadata[component][utils.META_DEPS] = defaultdict() |
| imports = self.__metadata[component][utils.META_IMPORTS] |
| for import_type in imports: |
| for module in imports[import_type]: |
| verify_and_add_dependencies(ImportedDep(module, component, import_type, imports[import_type])) |
| |
| def __check_imports(self): |
| """ Search the export map to find the component to import libraries or APIs. |
| |
| Update the 'deps' field that includes the dependent components. |
| """ |
| for component in self.__metadata: |
| self.__verify_and_add_dependencies(component) |
| if utils.META_DEPS in self.__metadata[component]: |
| logging.debug('{comp} depends on {list} components'.format( |
| comp=component, list=self.__metadata[component][utils.META_DEPS])) |
| |
| def __update_dependencies(self): |
| """ Generate a dependency graph for the components |
| |
| Update __map_exports and the dependency graph with the maps. |
| """ |
| self.__update_export_map() |
| self.__check_imports() |
| |
| def __store_metadata(self): |
| """ Store the __metadata dictionary as json format""" |
| with open(self.__combined_metadata_path, 'w') as json_file: |
| json.dump(self.__metadata, json_file, indent=2) |
| |
| def __restore_metadata(self): |
| """ Read the stored json file and return the time stamps of the |
| |
| metadata file. |
| """ |
| if not os.path.exists(self.__combined_metadata_path): |
| return None |
| |
| with open(self.__combined_metadata_path, 'r') as json_file: |
| try: |
| self.__metadata = json.load(json_file) |
| except json.decoder.JSONDecodeError: |
| logging.warning('JSONDecodeError!!!: skip reading the {} file'.format( |
| self.__combined_metadata_path)) |
| return None |
| |
| logging.info('Metadata restored from {}'.format( |
| self.__combined_metadata_path)) |
| self.__update_export_map() |
| return os.path.getmtime(self.__combined_metadata_path) |
| |
| |
| def get_args(): |
| |
| def check_dir(path): |
| if os.path.exists(path) and os.path.isdir(path): |
| return os.path.normpath(path) |
| else: |
| raise argparse.ArgumentTypeError('\"{}\" is not a directory'.format(path)) |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--component-top', |
| help='Scan all components under this directory.', |
| default=os.path.join(os.path.dirname(__file__), '../../../components'), |
| type=check_dir) |
| parser.add_argument( |
| '--meta-file', |
| help='Name of the metadata file.', |
| default=COMPONENT_METADATA_FILE, |
| type=str) |
| parser.add_argument( |
| '--meta-dir', |
| help='Each component has the metadata in this directory.', |
| default=COMPONENT_METADATA_DIR, |
| type=str) |
| parser.add_argument( |
| '--out-dir', |
| help='Out dir for the outer tree. The orchestrator stores the collected metadata in this directory.', |
| default=os.path.join(os.path.dirname(__file__), '../../../out'), |
| type=os.path.normpath) |
| parser.add_argument( |
| '--force', |
| '-f', |
| action='store_true', |
| help='Force to collect metadata', |
| ) |
| parser.add_argument( |
| '--verbose', |
| '-v', |
| help='Increase output verbosity, e.g. "-v", "-vv".', |
| action='count', |
| default=0) |
| return parser.parse_args() |
| |
| |
| def main(): |
| args = get_args() |
| utils.set_logging_config(args.verbose) |
| |
| metadata_collector = MetadataCollector(args.component_top, args.out_dir, |
| args.meta_dir, args.meta_file, args.force) |
| metadata_collector.collect() |
| |
| |
| if __name__ == '__main__': |
| main() |