#!/usr/bin/env python3 # Copyright (C) 2022 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 collections import copy import hierarchy import json import logging import filecmp import os import shutil import subprocess import sys import tempfile import collect_metadata import utils BUILD_CMD_TO_ALL = ( 'clean', 'installclean', 'update-meta', ) BUILD_ALL_EXEMPTION = ( 'art', ) def get_supported_product(ctx, supported_products): hierarchy_map = hierarchy.parse_hierarchy(ctx.build_top()) target = ctx.target_product() while target not in supported_products: if target not in hierarchy_map: return None target = hierarchy_map[target] return target def parse_goals(ctx, metadata, goals): """Parse goals and returns a map from each component to goals. e.g. "m main art timezone:foo timezone:bar" will return the following dict: { "main": {"all"}, "art": {"all"}, "timezone": {"foo", "bar"}, } """ # for now, goal should look like: # {component} or {component}:{subgoal} ret = collections.defaultdict(set) for goal in goals: # check if the command is for all components if goal in BUILD_CMD_TO_ALL: ret['all'].add(goal) continue # should be {component} or {component}:{subgoal} try: component, subgoal = goal.split(':') if ':' in goal else (goal, 'all') except ValueError: raise RuntimeError( 'unknown goal: %s: should be {component} or {component}:{subgoal}' % goal) if component not in metadata: raise RuntimeError('unknown goal: %s: component %s not found' % (goal, component)) if not get_supported_product(ctx, metadata[component]['lunch_targets']): raise RuntimeError("can't find matching target. Supported targets are: " + str(metadata[component]['lunch_targets'])) ret[component].add(subgoal) return ret def find_cycle(metadata): """ Finds a cyclic dependency among components. This is for debugging. """ visited = set() parent_node = dict() in_stack = set() # Returns a cycle if one is found def dfs(node): # visit_order[visit_time[node] - 1] == node nonlocal visited, parent_node, in_stack visited.add(node) in_stack.add(node) if 'deps' not in metadata[node]: in_stack.remove(node) return None for next in metadata[node]['deps']: # We found a cycle (next ~ node) if next is still in the stack if next in in_stack: cycle = [node] while cycle[-1] != next: cycle.append(parent_node[cycle[-1]]) return cycle # Else, continue searching if next in visited: continue parent_node[next] = node result = dfs(next) if result: return result in_stack.remove(node) return None for component in metadata: if component in visited: continue result = dfs(component) if result: return result return None def topological_sort_components(metadata): """ Performs topological sort on components. If A depends on B, B appears first. """ # If A depends on B, we want B to appear before A. But the graph in metadata # is represented as A -> B (B in metadata[A]['deps']). So we sort in the # reverse order, and then reverse the result again to get the desired order. indegree = collections.defaultdict(int) for component in metadata: if 'deps' not in metadata[component]: continue for dep in metadata[component]['deps']: indegree[dep] += 1 component_queue = collections.deque() for component in metadata: if indegree[component] == 0: component_queue.append(component) result = [] while component_queue: component = component_queue.popleft() result.append(component) if 'deps' not in metadata[component]: continue for dep in metadata[component]['deps']: indegree[dep] -= 1 if indegree[dep] == 0: component_queue.append(dep) # If topological sort fails, there must be a cycle. if len(result) != len(metadata): cycle = find_cycle(metadata) raise RuntimeError('circular dependency found among metadata: %s' % cycle) return result[::-1] def add_dependency_goals(ctx, metadata, component, goals): """ Adds goals that given component depends on.""" # For now, let's just add "all" # TODO: add detailed goals (e.g. API build rules, library build rules, etc.) if 'deps' not in metadata[component]: return for dep in metadata[component]['deps']: goals[dep].add('all') def sorted_goals_with_dependencies(ctx, metadata, parsed_goals): """ Analyzes the dependency graph among components, adds build commands for dependencies, and then sorts the goals. Returns a list of tuples: (component_name, set of subgoals). Builds should be run in the list's order. """ # TODO(inseob@): after topological sort, some components may be built in # parallel. topological_order = topological_sort_components(metadata) combined_goals = copy.deepcopy(parsed_goals) # Add build rules for each component's dependencies # We do this in reverse order, so it can be transitive. # e.g. if A depends on B and B depends on C, and we build A, # C should also be built, in addition to B. for component in topological_order[::-1]: if component in combined_goals: add_dependency_goals(ctx, metadata, component, combined_goals) ret = [] for component in ['all'] + topological_order: if component in combined_goals: ret.append((component, combined_goals[component])) return ret def run_build(ctx, metadata, component, subgoals): build_cmd = metadata[component]['build_cmd'] out_dir = metadata[component]['out_dir'] default_goals = '' if 'default_goals' in metadata[component]: default_goals = metadata[component]['default_goals'] if 'all' in subgoals: goal = default_goals else: goal = ' '.join(subgoals) build_vars = '' if 'update-meta' in subgoals: build_vars = 'TARGET_MULTITREE_UPDATE_META=true' # TODO(inseob@): shell escape cmd = [ '/bin/bash', '-c', 'source build/envsetup.sh && lunch %s-%s && %s %s %s' % (get_supported_product(ctx, metadata[component]['lunch_targets']), ctx.target_build_variant(), build_vars, build_cmd, goal) ] logging.debug('cwd: ' + metadata[component]['path']) logging.debug('running build: ' + str(cmd)) subprocess.run(cmd, cwd=metadata[component]['path'], check=True) def run_build_all(ctx, metadata, subgoals): for component in metadata: if component in BUILD_ALL_EXEMPTION: continue run_build(ctx, metadata, component, subgoals) def find_components(metadata, predicate): for component in metadata: if predicate(component): yield component def import_filegroups(metadata, component, exporting_component, target_file_pairs): imported_filegroup_dir = os.path.join(metadata[component]['path'], 'imported', exporting_component) bp_content = '' for name, outpaths in target_file_pairs: bp_content += ('filegroup {{\n' ' name: "{fname}",\n' ' srcs: [\n'.format(fname=name)) for outpath in outpaths: bp_content += ' "{outfile}",\n'.format(outfile=os.path.basename(outpath)) bp_content += (' ],\n' '}\n') with tempfile.TemporaryDirectory() as tmp_dir: with open(os.path.join(tmp_dir, 'Android.bp'), 'w') as fout: fout.write(bp_content) for _, outpaths in target_file_pairs: for outpath in outpaths: os.symlink(os.path.join(metadata[exporting_component]['path'], outpath), os.path.join(tmp_dir, os.path.basename(outpath))) cmp_result = filecmp.dircmp(tmp_dir, imported_filegroup_dir) if os.path.exists(imported_filegroup_dir) and len( cmp_result.left_only) + len(cmp_result.right_only) + len( cmp_result.diff_files) == 0: # Files are identical, it doesn't need to be written logging.info( 'imported files exists and the contents are identical: {} -> {}' .format(component, exporting_component)) continue logging.info('creating symlinks for imported files: {} -> {}'.format( component, exporting_component)) os.makedirs(imported_filegroup_dir, exist_ok=True) shutil.rmtree(imported_filegroup_dir, ignore_errors=True) shutil.move(tmp_dir, imported_filegroup_dir) def prepare_build(metadata, component): imported_dir = os.path.join(metadata[component]['path'], 'imported') if utils.META_DEPS not in metadata[component]: if os.path.exists(imported_dir): logging.debug('remove {}'.format(imported_dir)) shutil.rmtree(imported_dir) return imported_components = set() for exp_comp in metadata[component][utils.META_DEPS]: if utils.META_FILEGROUP in metadata[component][utils.META_DEPS][exp_comp]: filegroups = metadata[component][utils.META_DEPS][exp_comp][utils.META_FILEGROUP] target_file_pairs = [] for name in filegroups: target_file_pairs.append((name, filegroups[name])) import_filegroups(metadata, component, exp_comp, target_file_pairs) imported_components.add(exp_comp) # Remove directories that are not generated this time. if os.path.exists(imported_dir): if len(imported_components) == 0: shutil.rmtree(imported_dir) else: for remove_target in set(os.listdir(imported_dir)) - imported_components: logging.info('remove unnecessary imported dir: {}'.format(remove_target)) shutil.rmtree(os.path.join(imported_dir, remove_target)) def main(): utils.set_logging_config(logging.DEBUG) ctx = utils.get_build_context() logging.info('collecting metadata') utils.set_logging_config(True) goals = sys.argv[1:] if not goals: logging.debug('empty goals. defaults to main') goals = ['main'] logging.debug('goals: ' + str(goals)) # Force update the metadata for the 'update-meta' build metadata_collector = collect_metadata.MetadataCollector( ctx.components_top(), ctx.out_dir(), collect_metadata.COMPONENT_METADATA_DIR, collect_metadata.COMPONENT_METADATA_FILE, force_update='update-meta' in goals) metadata_collector.collect() metadata = metadata_collector.get_metadata() logging.debug('metadata: ' + str(metadata)) parsed_goals = parse_goals(ctx, metadata, goals) logging.debug('parsed goals: ' + str(parsed_goals)) sorted_goals = sorted_goals_with_dependencies(ctx, metadata, parsed_goals) logging.debug('sorted goals with deps: ' + str(sorted_goals)) for component, subgoals in sorted_goals: if component == 'all': run_build_all(ctx, metadata, subgoals) continue prepare_build(metadata, component) run_build(ctx, metadata, component, subgoals) if __name__ == '__main__': main()