blob: 148167d3ebb7a2e03d8dafb422b539945d33fe9e [file] [log] [blame]
Justin Yun0daf1862022-04-27 16:21:16 +09001#!/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()