1#!/usr/bin/env python3 2# Copyright (C) 2022 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 collections 17import copy 18import hierarchy 19import json 20import logging 21import filecmp 22import os 23import shutil 24import subprocess 25import sys 26import tempfile 27import collect_metadata 28import utils 29 30BUILD_CMD_TO_ALL = ( 31 'clean', 32 'installclean', 33 'update-meta', 34) 35BUILD_ALL_EXEMPTION = ( 36 'art', 37) 38 39def get_supported_product(ctx, supported_products): 40 hierarchy_map = hierarchy.parse_hierarchy(ctx.build_top()) 41 target = ctx.target_product() 42 43 while target not in supported_products: 44 if target not in hierarchy_map: 45 return None 46 target = hierarchy_map[target] 47 return target 48 49 50def parse_goals(ctx, metadata, goals): 51 """Parse goals and returns a map from each component to goals. 52 53 e.g. 54 55 "m main art timezone:foo timezone:bar" will return the following dict: { 56 "main": {"all"}, 57 "art": {"all"}, 58 "timezone": {"foo", "bar"}, 59 } 60 """ 61 # for now, goal should look like: 62 # {component} or {component}:{subgoal} 63 64 ret = collections.defaultdict(set) 65 66 for goal in goals: 67 # check if the command is for all components 68 if goal in BUILD_CMD_TO_ALL: 69 ret['all'].add(goal) 70 continue 71 72 # should be {component} or {component}:{subgoal} 73 try: 74 component, subgoal = goal.split(':') if ':' in goal else (goal, 'all') 75 except ValueError: 76 raise RuntimeError( 77 'unknown goal: %s: should be {component} or {component}:{subgoal}' % 78 goal) 79 if component not in metadata: 80 raise RuntimeError('unknown goal: %s: component %s not found' % 81 (goal, component)) 82 if not get_supported_product(ctx, metadata[component]['lunch_targets']): 83 raise RuntimeError("can't find matching target. Supported targets are: " + 84 str(metadata[component]['lunch_targets'])) 85 86 ret[component].add(subgoal) 87 88 return ret 89 90 91def find_cycle(metadata): 92 """ Finds a cyclic dependency among components. 93 94 This is for debugging. 95 """ 96 visited = set() 97 parent_node = dict() 98 in_stack = set() 99 100 # Returns a cycle if one is found 101 def dfs(node): 102 # visit_order[visit_time[node] - 1] == node 103 nonlocal visited, parent_node, in_stack 104 105 visited.add(node) 106 in_stack.add(node) 107 if 'deps' not in metadata[node]: 108 in_stack.remove(node) 109 return None 110 for next in metadata[node]['deps']: 111 # We found a cycle (next ~ node) if next is still in the stack 112 if next in in_stack: 113 cycle = [node] 114 while cycle[-1] != next: 115 cycle.append(parent_node[cycle[-1]]) 116 return cycle 117 118 # Else, continue searching 119 if next in visited: 120 continue 121 122 parent_node[next] = node 123 result = dfs(next) 124 if result: 125 return result 126 127 in_stack.remove(node) 128 return None 129 130 for component in metadata: 131 if component in visited: 132 continue 133 134 result = dfs(component) 135 if result: 136 return result 137 138 return None 139 140 141def topological_sort_components(metadata): 142 """ Performs topological sort on components. 143 144 If A depends on B, B appears first. 145 """ 146 # If A depends on B, we want B to appear before A. But the graph in metadata 147 # is represented as A -> B (B in metadata[A]['deps']). So we sort in the 148 # reverse order, and then reverse the result again to get the desired order. 149 indegree = collections.defaultdict(int) 150 for component in metadata: 151 if 'deps' not in metadata[component]: 152 continue 153 for dep in metadata[component]['deps']: 154 indegree[dep] += 1 155 156 component_queue = collections.deque() 157 for component in metadata: 158 if indegree[component] == 0: 159 component_queue.append(component) 160 161 result = [] 162 while component_queue: 163 component = component_queue.popleft() 164 result.append(component) 165 if 'deps' not in metadata[component]: 166 continue 167 for dep in metadata[component]['deps']: 168 indegree[dep] -= 1 169 if indegree[dep] == 0: 170 component_queue.append(dep) 171 172 # If topological sort fails, there must be a cycle. 173 if len(result) != len(metadata): 174 cycle = find_cycle(metadata) 175 raise RuntimeError('circular dependency found among metadata: %s' % cycle) 176 177 return result[::-1] 178 179 180def add_dependency_goals(ctx, metadata, component, goals): 181 """ Adds goals that given component depends on.""" 182 # For now, let's just add "all" 183 # TODO: add detailed goals (e.g. API build rules, library build rules, etc.) 184 if 'deps' not in metadata[component]: 185 return 186 187 for dep in metadata[component]['deps']: 188 goals[dep].add('all') 189 190 191def sorted_goals_with_dependencies(ctx, metadata, parsed_goals): 192 """ Analyzes the dependency graph among components, adds build commands for 193 194 dependencies, and then sorts the goals. 195 196 Returns a list of tuples: (component_name, set of subgoals). 197 Builds should be run in the list's order. 198 """ 199 # TODO(inseob@): after topological sort, some components may be built in 200 # parallel. 201 202 topological_order = topological_sort_components(metadata) 203 combined_goals = copy.deepcopy(parsed_goals) 204 205 # Add build rules for each component's dependencies 206 # We do this in reverse order, so it can be transitive. 207 # e.g. if A depends on B and B depends on C, and we build A, 208 # C should also be built, in addition to B. 209 for component in topological_order[::-1]: 210 if component in combined_goals: 211 add_dependency_goals(ctx, metadata, component, combined_goals) 212 213 ret = [] 214 for component in ['all'] + topological_order: 215 if component in combined_goals: 216 ret.append((component, combined_goals[component])) 217 218 return ret 219 220 221def run_build(ctx, metadata, component, subgoals): 222 build_cmd = metadata[component]['build_cmd'] 223 out_dir = metadata[component]['out_dir'] 224 default_goals = '' 225 if 'default_goals' in metadata[component]: 226 default_goals = metadata[component]['default_goals'] 227 228 if 'all' in subgoals: 229 goal = default_goals 230 else: 231 goal = ' '.join(subgoals) 232 233 build_vars = '' 234 if 'update-meta' in subgoals: 235 build_vars = 'TARGET_MULTITREE_UPDATE_META=true' 236 # TODO(inseob@): shell escape 237 cmd = [ 238 '/bin/bash', '-c', 239 'source build/envsetup.sh && lunch %s-%s && %s %s %s' % 240 (get_supported_product(ctx, metadata[component]['lunch_targets']), 241 ctx.target_build_variant(), build_vars, build_cmd, goal) 242 ] 243 logging.debug('cwd: ' + metadata[component]['path']) 244 logging.debug('running build: ' + str(cmd)) 245 246 subprocess.run(cmd, cwd=metadata[component]['path'], check=True) 247 248 249def run_build_all(ctx, metadata, subgoals): 250 for component in metadata: 251 if component in BUILD_ALL_EXEMPTION: 252 continue 253 run_build(ctx, metadata, component, subgoals) 254 255 256def find_components(metadata, predicate): 257 for component in metadata: 258 if predicate(component): 259 yield component 260 261 262def import_filegroups(metadata, component, exporting_component, target_file_pairs): 263 imported_filegroup_dir = os.path.join(metadata[component]['path'], 'imported', exporting_component) 264 265 bp_content = '' 266 for name, outpaths in target_file_pairs: 267 bp_content += ('filegroup {{\n' 268 ' name: "{fname}",\n' 269 ' srcs: [\n'.format(fname=name)) 270 for outpath in outpaths: 271 bp_content += ' "{outfile}",\n'.format(outfile=os.path.basename(outpath)) 272 bp_content += (' ],\n' 273 '}\n') 274 275 with tempfile.TemporaryDirectory() as tmp_dir: 276 with open(os.path.join(tmp_dir, 'Android.bp'), 'w') as fout: 277 fout.write(bp_content) 278 for _, outpaths in target_file_pairs: 279 for outpath in outpaths: 280 os.symlink(os.path.join(metadata[exporting_component]['path'], outpath), 281 os.path.join(tmp_dir, os.path.basename(outpath))) 282 cmp_result = filecmp.dircmp(tmp_dir, imported_filegroup_dir) 283 if os.path.exists(imported_filegroup_dir) and len( 284 cmp_result.left_only) + len(cmp_result.right_only) + len( 285 cmp_result.diff_files) == 0: 286 # Files are identical, it doesn't need to be written 287 logging.info( 288 'imported files exists and the contents are identical: {} -> {}' 289 .format(component, exporting_component)) 290 continue 291 logging.info('creating symlinks for imported files: {} -> {}'.format( 292 component, exporting_component)) 293 os.makedirs(imported_filegroup_dir, exist_ok=True) 294 shutil.rmtree(imported_filegroup_dir, ignore_errors=True) 295 shutil.move(tmp_dir, imported_filegroup_dir) 296 297 298def prepare_build(metadata, component): 299 imported_dir = os.path.join(metadata[component]['path'], 'imported') 300 if utils.META_DEPS not in metadata[component]: 301 if os.path.exists(imported_dir): 302 logging.debug('remove {}'.format(imported_dir)) 303 shutil.rmtree(imported_dir) 304 return 305 306 imported_components = set() 307 for exp_comp in metadata[component][utils.META_DEPS]: 308 if utils.META_FILEGROUP in metadata[component][utils.META_DEPS][exp_comp]: 309 filegroups = metadata[component][utils.META_DEPS][exp_comp][utils.META_FILEGROUP] 310 target_file_pairs = [] 311 for name in filegroups: 312 target_file_pairs.append((name, filegroups[name])) 313 import_filegroups(metadata, component, exp_comp, target_file_pairs) 314 imported_components.add(exp_comp) 315 316 # Remove directories that are not generated this time. 317 if os.path.exists(imported_dir): 318 if len(imported_components) == 0: 319 shutil.rmtree(imported_dir) 320 else: 321 for remove_target in set(os.listdir(imported_dir)) - imported_components: 322 logging.info('remove unnecessary imported dir: {}'.format(remove_target)) 323 shutil.rmtree(os.path.join(imported_dir, remove_target)) 324 325 326def main(): 327 utils.set_logging_config(logging.DEBUG) 328 ctx = utils.get_build_context() 329 330 logging.info('collecting metadata') 331 332 utils.set_logging_config(True) 333 334 goals = sys.argv[1:] 335 if not goals: 336 logging.debug('empty goals. defaults to main') 337 goals = ['main'] 338 339 logging.debug('goals: ' + str(goals)) 340 341 # Force update the metadata for the 'update-meta' build 342 metadata_collector = collect_metadata.MetadataCollector( 343 ctx.components_top(), ctx.out_dir(), 344 collect_metadata.COMPONENT_METADATA_DIR, 345 collect_metadata.COMPONENT_METADATA_FILE, 346 force_update='update-meta' in goals) 347 metadata_collector.collect() 348 349 metadata = metadata_collector.get_metadata() 350 logging.debug('metadata: ' + str(metadata)) 351 352 parsed_goals = parse_goals(ctx, metadata, goals) 353 logging.debug('parsed goals: ' + str(parsed_goals)) 354 355 sorted_goals = sorted_goals_with_dependencies(ctx, metadata, parsed_goals) 356 logging.debug('sorted goals with deps: ' + str(sorted_goals)) 357 358 for component, subgoals in sorted_goals: 359 if component == 'all': 360 run_build_all(ctx, metadata, subgoals) 361 continue 362 prepare_build(metadata, component) 363 run_build(ctx, metadata, component, subgoals) 364 365 366if __name__ == '__main__': 367 main() 368