• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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