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