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