1#!/usr/bin/env python3 2# Copyright (C) 2021 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import argparse 17import copy 18import json 19import logging 20import os 21import sys 22import yaml 23from collections import defaultdict 24from typing import ( 25 List, 26 Set, 27) 28 29import utils 30 31# SKIP_COMPONENT_SEARCH = ( 32# 'tools', 33# ) 34COMPONENT_METADATA_DIR = '.repo' 35COMPONENT_METADATA_FILE = 'treeinfo.yaml' 36GENERATED_METADATA_FILE = 'metadata.json' 37COMBINED_METADATA_FILENAME = 'multitree_meta.json' 38 39 40class Dep(object): 41 def __init__(self, name, component, deps_type): 42 self.name = name 43 self.component = component 44 self.type = deps_type 45 self.out_paths = list() 46 47 48class ExportedDep(Dep): 49 def __init__(self, name, component, deps_type): 50 super().__init__(name, component, deps_type) 51 52 def setOutputPaths(self, output_paths: list): 53 self.out_paths = output_paths 54 55 56class ImportedDep(Dep): 57 required_type_map = { 58 # import type: (required type, get imported module list) 59 utils.META_FILEGROUP: (utils.META_MODULES, True), 60 } 61 62 def __init__(self, name, component, deps_type, import_map): 63 super().__init__(name, component, deps_type) 64 self.exported_deps: Set[ExportedDep] = set() 65 self.imported_modules: List[str] = list() 66 self.required_type = deps_type 67 get_imported_module = False 68 if deps_type in ImportedDep.required_type_map: 69 self.required_type, get_imported_module = ImportedDep.required_type_map[deps_type] 70 if get_imported_module: 71 self.imported_modules = import_map[name] 72 else: 73 self.imported_modules.append(name) 74 75 def verify_and_add(self, exported: ExportedDep): 76 if self.required_type != exported.type: 77 raise RuntimeError( 78 '{comp} components imports {module} for {imp_type} but it is exported as {exp_type}.' 79 .format(comp=self.component, module=exported.name, imp_type=self.required_type, exp_type=exported.type)) 80 self.exported_deps.add(exported) 81 self.out_paths.extend(exported.out_paths) 82 # Remove duplicates. We may not use set() which is not JSON serializable 83 self.out_paths = list(dict.fromkeys(self.out_paths)) 84 85 86class MetadataCollector(object): 87 """Visit all component directories and collect the metadata from them. 88 89Example of metadata: 90========== 91build_cmd: m # build command for this component. 'm' if omitted 92out_dir: out # out dir of this component. 'out' if omitted 93exports: 94 libraries: 95 - name: libopenjdkjvm 96 - name: libopenjdkjvmd 97 build_cmd: mma # build command for libopenjdkjvmd if specified 98 out_dir: out/soong # out dir for libopenjdkjvmd if specified 99 - name: libctstiagent 100 APIs: 101 - api1 102 - api2 103imports: 104 libraries: 105 - lib1 106 - lib2 107 APIs: 108 - import_api1 109 - import_api2 110lunch_targets: 111 - arm64 112 - x86_64 113""" 114 115 def __init__(self, component_top, out_dir, meta_dir, meta_file, force_update=False): 116 if not os.path.exists(out_dir): 117 os.makedirs(out_dir) 118 119 self.__component_top = component_top 120 self.__out_dir = out_dir 121 self.__metadata_path = os.path.join(meta_dir, meta_file) 122 self.__combined_metadata_path = os.path.join(self.__out_dir, 123 COMBINED_METADATA_FILENAME) 124 self.__force_update = force_update 125 126 self.__metadata = dict() 127 self.__map_exports = dict() 128 self.__component_set = set() 129 130 def collect(self): 131 """ Read precomputed combined metadata from the json file. 132 133 If any components have updated their metadata, update the metadata 134 information and the json file. 135 """ 136 timestamp = self.__restore_metadata() 137 if timestamp and os.path.getmtime(__file__) > timestamp: 138 logging.info('Update the metadata as the orchestrator has been changed') 139 self.__force_update = True 140 self.__collect_from_components(timestamp) 141 142 def get_metadata(self): 143 """ Returns collected metadata from all components""" 144 if not self.__metadata: 145 logging.warning('Metadata is empty') 146 return copy.deepcopy(self.__metadata) 147 148 def __collect_from_components(self, timestamp): 149 """ Read metadata from all components 150 151 If any components have newer metadata files or are removed, update the 152 combined metadata. 153 """ 154 metadata_updated = False 155 for component in os.listdir(self.__component_top): 156 # if component in SKIP_COMPONENT_SEARCH: 157 # continue 158 if self.__read_component_metadata(timestamp, component): 159 metadata_updated = True 160 if self.__read_generated_metadata(timestamp, component): 161 metadata_updated = True 162 163 deleted_components = set() 164 for meta in self.__metadata: 165 if meta not in self.__component_set: 166 logging.info('Component {} is removed'.format(meta)) 167 deleted_components.add(meta) 168 metadata_updated = True 169 for meta in deleted_components: 170 del self.__metadata[meta] 171 172 if metadata_updated: 173 self.__update_dependencies() 174 self.__store_metadata() 175 logging.info('Metadata updated') 176 177 def __read_component_metadata(self, timestamp, component): 178 """ Search for the metadata file from a component. 179 180 If the metadata is modified, read the file and update the metadata. 181 """ 182 component_path = os.path.join(self.__component_top, component) 183 metadata_file = os.path.join(component_path, self.__metadata_path) 184 logging.info( 185 'Reading a metadata file from {} component ...'.format(component)) 186 if not os.path.isfile(metadata_file): 187 logging.warning('Metadata file {} not found!'.format(metadata_file)) 188 return False 189 190 self.__component_set.add(component) 191 if not self.__force_update and timestamp and timestamp > os.path.getmtime(metadata_file): 192 logging.info('... yaml not changed. Skip') 193 return False 194 195 with open(metadata_file) as f: 196 meta = yaml.load(f, Loader=yaml.SafeLoader) 197 198 meta['path'] = component_path 199 if utils.META_BUILDCMD not in meta: 200 meta[utils.META_BUILDCMD] = utils.DEFAULT_BUILDCMD 201 if utils.META_OUTDIR not in meta: 202 meta[utils.META_OUTDIR] = utils.DEFAULT_OUTDIR 203 204 if utils.META_IMPORTS not in meta: 205 meta[utils.META_IMPORTS] = defaultdict(dict) 206 if utils.META_EXPORTS not in meta: 207 meta[utils.META_EXPORTS] = defaultdict(dict) 208 209 self.__metadata[component] = meta 210 return True 211 212 def __read_generated_metadata(self, timestamp, component): 213 """ Read a metadata gerated by 'update-meta' build command from the soong build system 214 215 Soong generate the metadata that has the information of import/export module/files. 216 Build orchestrator read the generated metadata to collect the dependency information. 217 218 Generated metadata has the following format: 219 { 220 "Imported": { 221 "FileGroups": { 222 "<name_of_filegroup>": [ 223 "<exported_module_name>", 224 ... 225 ], 226 ... 227 } 228 } 229 "Exported": { 230 "<exported_module_name>": [ 231 "<output_file_path>", 232 ... 233 ], 234 ... 235 } 236 } 237 """ 238 if component not in self.__component_set: 239 # skip reading generated metadata if the component metadata file was missing 240 return False 241 component_out = os.path.join(self.__component_top, component, self.__metadata[component][utils.META_OUTDIR]) 242 generated_metadata_file = os.path.join(component_out, 'soong', 'multitree', GENERATED_METADATA_FILE) 243 if not os.path.isfile(generated_metadata_file): 244 logging.info('... Soong did not generated the metadata file. Skip') 245 return False 246 if not self.__force_update and timestamp and timestamp > os.path.getmtime(generated_metadata_file): 247 logging.info('... Soong generated metadata not changed. Skip') 248 return False 249 250 with open(generated_metadata_file, 'r') as gen_meta_json: 251 try: 252 gen_metadata = json.load(gen_meta_json) 253 except json.decoder.JSONDecodeError: 254 logging.warning('JSONDecodeError!!!: skip reading the {} file'.format( 255 generated_metadata_file)) 256 return False 257 258 if utils.SOONG_IMPORTED in gen_metadata: 259 imported = gen_metadata[utils.SOONG_IMPORTED] 260 if utils.SOONG_IMPORTED_FILEGROUPS in imported: 261 self.__metadata[component][utils.META_IMPORTS][utils.META_FILEGROUP] = imported[utils.SOONG_IMPORTED_FILEGROUPS] 262 if utils.SOONG_EXPORTED in gen_metadata: 263 self.__metadata[component][utils.META_EXPORTS][utils.META_MODULES] = gen_metadata[utils.SOONG_EXPORTED] 264 265 return True 266 267 def __update_export_map(self): 268 """ Read metadata of all components and update the export map 269 270 'libraries' and 'APIs' are special exproted types that are provided manually 271 from the .yaml metadata files. These need to be replaced with the implementation 272 in soong gerated metadata. 273 The export type 'module' is generated from the soong build system from the modules 274 with 'export: true' property. This export type includes a dictionary with module 275 names as keys and their output files as values. These output files will be used as 276 prebuilt sources when generating the imported modules. 277 """ 278 self.__map_exports = dict() 279 for comp in self.__metadata: 280 if utils.META_EXPORTS not in self.__metadata[comp]: 281 continue 282 exports = self.__metadata[comp][utils.META_EXPORTS] 283 284 for export_type in exports: 285 for module in exports[export_type]: 286 if export_type == utils.META_LIBS: 287 name = module[utils.META_LIB_NAME] 288 else: 289 name = module 290 291 if name in self.__map_exports: 292 raise RuntimeError( 293 'Exported libs conflict!!!: "{name}" in the {comp} component is already exported by the {prev} component.' 294 .format(name=name, comp=comp, prev=self.__map_exports[name][utils.EXP_COMPONENT])) 295 exported_deps = ExportedDep(name, comp, export_type) 296 if export_type == utils.META_MODULES: 297 exported_deps.setOutputPaths(exports[export_type][module]) 298 self.__map_exports[name] = exported_deps 299 300 def __verify_and_add_dependencies(self, component): 301 """ Search all imported items from the export_map. 302 303 If any imported items are not provided by the other components, report 304 an error. 305 Otherwise, add the component dependency and update the exported information to the 306 import maps. 307 """ 308 def verify_and_add_dependencies(imported_dep: ImportedDep): 309 for module in imported_dep.imported_modules: 310 if module not in self.__map_exports: 311 raise RuntimeError( 312 'Imported item not found!!!: Imported module "{module}" in the {comp} component is not exported from any other components.' 313 .format(module=module, comp=imported_dep.component)) 314 imported_dep.verify_and_add(self.__map_exports[module]) 315 316 deps = self.__metadata[component][utils.META_DEPS] 317 exp_comp = self.__map_exports[module].component 318 if exp_comp not in deps: 319 deps[exp_comp] = defaultdict(defaultdict) 320 deps[exp_comp][imported_dep.type][imported_dep.name] = imported_dep.out_paths 321 322 self.__metadata[component][utils.META_DEPS] = defaultdict() 323 imports = self.__metadata[component][utils.META_IMPORTS] 324 for import_type in imports: 325 for module in imports[import_type]: 326 verify_and_add_dependencies(ImportedDep(module, component, import_type, imports[import_type])) 327 328 def __check_imports(self): 329 """ Search the export map to find the component to import libraries or APIs. 330 331 Update the 'deps' field that includes the dependent components. 332 """ 333 for component in self.__metadata: 334 self.__verify_and_add_dependencies(component) 335 if utils.META_DEPS in self.__metadata[component]: 336 logging.debug('{comp} depends on {list} components'.format( 337 comp=component, list=self.__metadata[component][utils.META_DEPS])) 338 339 def __update_dependencies(self): 340 """ Generate a dependency graph for the components 341 342 Update __map_exports and the dependency graph with the maps. 343 """ 344 self.__update_export_map() 345 self.__check_imports() 346 347 def __store_metadata(self): 348 """ Store the __metadata dictionary as json format""" 349 with open(self.__combined_metadata_path, 'w') as json_file: 350 json.dump(self.__metadata, json_file, indent=2) 351 352 def __restore_metadata(self): 353 """ Read the stored json file and return the time stamps of the 354 355 metadata file. 356 """ 357 if not os.path.exists(self.__combined_metadata_path): 358 return None 359 360 with open(self.__combined_metadata_path, 'r') as json_file: 361 try: 362 self.__metadata = json.load(json_file) 363 except json.decoder.JSONDecodeError: 364 logging.warning('JSONDecodeError!!!: skip reading the {} file'.format( 365 self.__combined_metadata_path)) 366 return None 367 368 logging.info('Metadata restored from {}'.format( 369 self.__combined_metadata_path)) 370 self.__update_export_map() 371 return os.path.getmtime(self.__combined_metadata_path) 372 373 374def get_args(): 375 376 def check_dir(path): 377 if os.path.exists(path) and os.path.isdir(path): 378 return os.path.normpath(path) 379 else: 380 raise argparse.ArgumentTypeError('\"{}\" is not a directory'.format(path)) 381 382 parser = argparse.ArgumentParser() 383 parser.add_argument( 384 '--component-top', 385 help='Scan all components under this directory.', 386 default=os.path.join(os.path.dirname(__file__), '../../../components'), 387 type=check_dir) 388 parser.add_argument( 389 '--meta-file', 390 help='Name of the metadata file.', 391 default=COMPONENT_METADATA_FILE, 392 type=str) 393 parser.add_argument( 394 '--meta-dir', 395 help='Each component has the metadata in this directory.', 396 default=COMPONENT_METADATA_DIR, 397 type=str) 398 parser.add_argument( 399 '--out-dir', 400 help='Out dir for the outer tree. The orchestrator stores the collected metadata in this directory.', 401 default=os.path.join(os.path.dirname(__file__), '../../../out'), 402 type=os.path.normpath) 403 parser.add_argument( 404 '--force', 405 '-f', 406 action='store_true', 407 help='Force to collect metadata', 408 ) 409 parser.add_argument( 410 '--verbose', 411 '-v', 412 help='Increase output verbosity, e.g. "-v", "-vv".', 413 action='count', 414 default=0) 415 return parser.parse_args() 416 417 418def main(): 419 args = get_args() 420 utils.set_logging_config(args.verbose) 421 422 metadata_collector = MetadataCollector(args.component_top, args.out_dir, 423 args.meta_dir, args.meta_file, args.force) 424 metadata_collector.collect() 425 426 427if __name__ == '__main__': 428 main() 429