• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Splits a manifest to the minimum set of projects needed to build the targets.
15
16Usage: manifest_split [options] targets
17
18targets: Space-separated list of targets that should be buildable
19         using the split manifest.
20
21options:
22  --manifest <path>
23      Path to the repo manifest to split. [Required]
24  --split-manifest <path>
25      Path to write the resulting split manifest. [Required]
26  --config <path>
27      Optional path(s) to a config XML file containing projects to add or
28      remove. See default_config.xml for an example. This flag can be passed
29      more than once to use multiple config files.
30        Sample file my_config.xml:
31          <config>
32            <add_project name="vendor/my/needed/project" />
33            <remove_project name="vendor/my/unused/project" />
34          </config>
35  --ignore-default-config
36      If provided, don't include default_config.xml.
37  --installed-prebuilt
38      Specify the directory containing an installed prebuilt Android.bp file.
39      Supply this option zero or more times, once for each installed prebuilt
40      directory.
41  --repo-list <path>
42      Optional path to the output of the 'repo list' command. Used if the
43      output of 'repo list' needs pre-processing before being used by
44      this tool.
45  --ninja-build <path>
46      Optional path to the combined-<target>.ninja file found in an out dir.
47      If not provided, the default file is used based on the lunch environment.
48  --ninja-binary <path>
49      Optional path to the ninja binary. Uses the standard binary by default.
50  --module-info <path>
51      Optional path to the module-info.json file found in an out dir.
52      If not provided, the default file is used based on the lunch environment.
53  --skip-module-info
54      If provided, skip parsing module-info.json for direct and adjacent
55      dependencies. Overrides --module-info option.
56  --kati-stamp <path>
57      Optional path to the .kati_stamp file found in an out dir.
58      If not provided, the default file is used based on the lunch environment.
59  --skip-kati
60      If provided, skip Kati makefiles projects. Overrides --kati-stamp option.
61  --overlay <path>
62      Optional path(s) to treat as overlays when parsing the kati stamp file
63      and scanning for makefiles. See the tools/treble/build/sandbox directory
64      for more info about overlays. This flag can be passed more than once.
65  --debug-file <path>
66      If provided, debug info will be written to a JSON file at this path.
67  -h  (--help)
68      Display this usage message and exit.
69"""
70
71from __future__ import print_function
72
73import getopt
74import json
75import logging
76import os
77import pkgutil
78import re
79import subprocess
80import sys
81import tempfile
82from typing import Dict, List, Pattern, Set, Tuple
83import xml.etree.ElementTree as ET
84
85import dataclasses
86
87
88logging.basicConfig(
89    stream=sys.stdout,
90    level=logging.INFO,
91    format="%(asctime)s - %(filename)s - %(levelname)-8s: %(message)s",
92    datefmt="%Y-%m-%d %H:%M:%S")
93logger = logging.getLogger(os.path.basename(__file__))
94
95# Projects determined to be needed despite the dependency not being visible
96# to ninja.
97DEFAULT_CONFIG_XML = "default_config.xml"
98
99# Pattern that matches a java dependency.
100_JAVA_LIB_PATTERN = re.compile(
101    # pylint: disable=line-too-long
102    '^out/target/common/obj/JAVA_LIBRARIES/(.+)_intermediates/classes-header.jar$'
103)
104
105
106@dataclasses.dataclass
107class PathMappingConfig:
108  pattern: Pattern[str]
109  sub: str
110
111
112@dataclasses.dataclass
113class ManifestSplitConfig:
114  """Holds the configuration for the split manifest tool.
115
116  Attributes:
117    remove_projects: A Dict of project name to the config file that specified
118      this project, for projects that should be removed from the resulting
119      manifest.
120    add_projects: A Dict of project name to the config file that specified
121      this project, for projects that should be added to the resulting manifest.
122    path_mappings: A list of PathMappingConfigs to modify a path in the build
123      sandbox to the path in the manifest.
124    ignore_paths: Set of paths to ignore when parsing module_info_file
125  """
126  remove_projects: Dict[str, str]
127  add_projects: Dict[str, str]
128  path_mappings: List[PathMappingConfig]
129  ignore_paths: Set[str]
130
131  @classmethod
132  def from_config_files(cls, config_files: List[str]):
133    """Reads from a list of config XML files.
134
135    Args:
136      config_files: A list of config XML filenames.
137
138    Returns:
139      A ManifestSplitConfig from the files.
140    """
141    remove_projects: Dict[str, str] = {}
142    add_projects: Dict[str, str] = {}
143    path_mappings = []
144    """ Always ignore paths in out/ directory. """
145    ignore_paths = set(["out/"])
146    for config_file in config_files:
147      root = ET.parse(config_file).getroot()
148
149      remove_projects.update({
150          c.attrib["name"]: config_file for c in root.findall("remove_project")
151      })
152
153      add_projects.update(
154          {c.attrib["name"]: config_file for c in root.findall("add_project")})
155
156      path_mappings.extend([
157          PathMappingConfig(
158              re.compile(child.attrib["pattern"]), child.attrib["sub"])
159          for child in root.findall("path_mapping")
160      ])
161
162      ignore_paths.update(
163          {c.attrib["name"]: config_file for c in root.findall("ignore_path")})
164
165    return cls(remove_projects, add_projects, path_mappings, ignore_paths)
166
167
168def get_repo_projects(repo_list_file, manifest, path_mappings):
169  """Returns a dict of { project path : project name } using the manifest.
170
171  The path_mappings stop on the first match mapping.  If the mapping results in
172  an empty string, that entry is removed.
173
174  Args:
175    repo_list_file: An optional filename to read instead of parsing the manifest.
176    manifest: The manifest object to scan for projects.
177    path_mappings: A list of PathMappingConfigs to modify a path in the build
178      sandbox to the path in the manifest.
179  """
180  repo_list = []
181
182  if repo_list_file:
183    with open(repo_list_file) as repo_list_lines:
184      repo_list = [line.strip().split(" : ") for line in repo_list_lines if line.strip()]
185  else:
186    root = manifest.getroot()
187    repo_list = [(p.get("path", p.get("name")), p.get("name")) for p in root.findall("project")]
188
189  repo_dict = {}
190  for entry in repo_list:
191    path, project = entry
192    for mapping in path_mappings:
193      if mapping.pattern.fullmatch(path):
194        path = mapping.pattern.sub(mapping.sub, path)
195        break
196    # If the resulting path mapping is empty, then don't add entry
197    if path:
198      repo_dict[path] = project
199  return repo_dict
200
201
202class ModuleInfo:
203  """Contains various mappings to/from module/project"""
204
205  def __init__(self, module_info_file, repo_projects, ignore_paths):
206    """Initialize a module info instance.
207
208    Builds various maps related to platform build system modules and how they
209    relate to each other and projects.
210
211    Args:
212      module_info_file: The path to a module-info.json file from a build.
213      repo_projects: The output of the get_repo_projects function.
214      ignore_paths: Set of paths to ignore from module_info_file data
215
216    Raises:
217      ValueError: A module from module-info.json belongs to a path not
218        known by the repo projects output.
219    """
220    # Maps a project to the set of modules it contains.
221    self.project_modules = {}
222    # Maps a module to the project that contains it.
223    self.module_project = {}
224    # Maps a module to its class.
225    self.module_class = {}
226    # Maps a module to modules it depends on.
227    self.module_deps = {}
228
229    with open(module_info_file) as module_info_file:
230      module_info = json.load(module_info_file)
231
232    # Check that module contains a path and the path is not in set of
233    # ignore paths
234    def module_has_valid_path(module):
235      paths = module.get("path")
236      if not paths:
237        return False
238      return all(not paths[0].startswith(p) for p in ignore_paths)
239
240    module_paths = {
241        module: module_info[module]["path"][0]
242        for module in module_info
243        if module_has_valid_path(module_info[module])
244    }
245    module_project_paths = {
246        module: scan_repo_projects(repo_projects, module_paths[module])
247        for module in module_paths
248    }
249
250    for module, project_path in module_project_paths.items():
251      if not project_path:
252        raise ValueError("Unknown module path for module %s: %s" %
253                         (module, module_info[module]))
254      repo_project = repo_projects[project_path]
255      self.project_modules.setdefault(repo_project, set()).add(module)
256      self.module_project[module] = repo_project
257
258    def dep_from_raw_dep(raw_dep):
259      match = re.search(_JAVA_LIB_PATTERN, raw_dep)
260      return match.group(1) if match else raw_dep
261
262    def deps_from_raw_deps(raw_deps):
263      return [dep_from_raw_dep(raw_dep) for raw_dep in raw_deps]
264
265    self.module_class = {
266        module: module_info[module]["class"][0]
267        for module in module_info
268    }
269    self.module_deps = {
270        module: deps_from_raw_deps(module_info[module]["dependencies"])
271        for module in module_info
272    }
273
274
275def get_ninja_inputs(ninja_binary, ninja_build_file, modules):
276  """Returns the set of input file path strings for the given modules.
277
278  Uses the `ninja -t inputs` tool.
279
280  Args:
281    ninja_binary: The path to a ninja binary.
282    ninja_build_file: The path to a .ninja file from a build.
283    modules: The list of modules to scan for inputs.
284  """
285  inputs = set()
286  NINJA_SHARD_LIMIT = 20000
287  for i in range(0, len(modules), NINJA_SHARD_LIMIT):
288    modules_shard = modules[i:i + NINJA_SHARD_LIMIT]
289    inputs = inputs.union(set(
290        subprocess.check_output([
291            ninja_binary,
292            "-f",
293            ninja_build_file,
294            "-t",
295            "inputs",
296            "-d",
297        ] + list(modules_shard)).decode().strip("\n").split("\n")))
298
299  def input_allowed(path):
300    path = path.strip()
301    if path.endswith("TEST_MAPPING") and "test_mapping" not in modules:
302      # Exclude projects that are only needed for TEST_MAPPING files, unless the
303      # user is asking to build 'test_mapping'.
304      return False
305    if path.endswith("MODULE_LICENSE_GPL"):
306      # Exclude projects that are included only due to having a
307      # MODULE_LICENSE_GPL file, if no other inputs from that project are used.
308      return False
309    return path
310
311  return {path.strip() for path in inputs if input_allowed(path)}
312
313
314def get_kati_makefiles(kati_stamp_file, overlays):
315  """Returns the set of makefile paths from the kati stamp file.
316
317  Uses the ckati prebuilt binary.
318  Also includes symlink sources in the resulting set for any
319  makefiles that are symlinks.
320
321  Args:
322    kati_stamp_file: The path to a .kati_stamp file from a build.
323    overlays: A list of paths to treat as overlays when parsing the kati stamp
324      file.
325  """
326  # Get a set of all makefiles that were parsed by Kati during the build.
327  makefiles = set(
328      subprocess.check_output([
329          "prebuilts/build-tools/linux-x86/bin/ckati",
330          "--dump_stamp_tool",
331          "--files",
332          kati_stamp_file,
333      ]).decode().strip("\n").split("\n"))
334
335  def is_product_makefile(makefile):
336    """Returns True if the makefile path meets certain criteria."""
337    banned_prefixes = [
338        "out/",
339        # Ignore product makefiles for sample AOSP boards.
340        "device/amlogic",
341        "device/generic",
342        "device/google",
343        "device/linaro",
344        "device/sample",
345    ]
346    banned_suffixes = [
347        # All Android.mk files in the source are always parsed by Kati,
348        # so including them here would bring in lots of unnecessary projects.
349        "Android.mk",
350        # The ckati stamp file always includes a line for the ckati bin at
351        # the beginnning.
352        "bin/ckati",
353    ]
354    return (all([not makefile.startswith(p) for p in banned_prefixes]) and
355            all([not makefile.endswith(s) for s in banned_suffixes]))
356
357  # Limit the makefiles to only product makefiles.
358  product_makefiles = {
359      os.path.normpath(path) for path in makefiles if is_product_makefile(path)
360  }
361
362  def strip_overlay(makefile):
363    """Remove any overlays from a makefile path."""
364    for overlay in overlays:
365      if makefile.startswith(overlay):
366        return makefile[len(overlay):]
367    return makefile
368
369  makefiles_and_symlinks = set()
370  for makefile in product_makefiles:
371    # Search for the makefile, possibly scanning overlays as well.
372    for overlay in [""] + overlays:
373      makefile_with_overlay = os.path.join(overlay, makefile)
374      if os.path.exists(makefile_with_overlay):
375        makefile = makefile_with_overlay
376        break
377
378    if not os.path.exists(makefile):
379      logger.warning("Unknown kati makefile: %s" % makefile)
380      continue
381
382    # Ensure the project that contains the makefile is included, as well as
383    # the project that any makefile symlinks point to.
384    makefiles_and_symlinks.add(strip_overlay(makefile))
385    if os.path.islink(makefile):
386      makefiles_and_symlinks.add(
387          strip_overlay(os.path.relpath(os.path.realpath(makefile))))
388
389  return makefiles_and_symlinks
390
391
392def scan_repo_projects(repo_projects, input_path):
393  """Returns the project path of the given input path if it exists.
394
395  Args:
396    repo_projects: The output of the get_repo_projects function.
397    input_path: The path of an input file used in the build, as given by the
398      ninja inputs tool.
399
400  Returns:
401    The path string, or None if not found.
402  """
403  parts = input_path.split("/")
404
405  for index in reversed(range(0, len(parts))):
406    project_path = os.path.join(*parts[:index + 1])
407    if project_path in repo_projects:
408      return project_path
409
410  return None
411
412
413def get_input_projects(repo_projects, inputs):
414  """Returns the collection of project names that contain the given input paths.
415
416  Args:
417    repo_projects: The output of the get_repo_projects function.
418    inputs: The paths of input files used in the build, as given by the ninja
419      inputs tool.
420  """
421  input_project_paths = {}
422  for input_path in inputs:
423    if not input_path.startswith("out/") and not input_path.startswith("/"):
424      input_project_paths.setdefault(
425          scan_repo_projects(repo_projects, input_path), []).append(input_path)
426
427  return {
428      repo_projects[project_path]: inputs
429      for project_path, inputs in input_project_paths.items()
430      if project_path is not None
431  }
432
433
434def update_manifest(manifest, input_projects, remove_projects):
435  """Modifies and returns a manifest ElementTree by modifying its projects.
436
437  Args:
438    manifest: The manifest object to modify.
439    input_projects: A set of projects that should stay in the manifest.
440    remove_projects: A set of projects that should be removed from the manifest.
441      Projects in this set override input_projects.
442
443  Returns:
444    The modified manifest object.
445  """
446  projects_to_keep = input_projects.difference(remove_projects)
447  root = manifest.getroot()
448  for child in root.findall("project"):
449    if child.attrib["name"] not in projects_to_keep:
450      root.remove(child)
451  return manifest
452
453
454@dataclasses.dataclass
455class DebugInfo:
456  """Simple class to store structured debug info for a project."""
457  direct_input: bool = False
458  adjacent_input: bool = False
459  deps_input: bool = False
460  kati_makefiles: List[str] = dataclasses.field(default_factory=list)
461  manual_add_config: str = ""
462  manual_remove_config: str = ""
463
464
465def create_split_manifest(targets, manifest_file, split_manifest_file,
466                          config_files, repo_list_file, ninja_build_file,
467                          ninja_binary, module_info_file, kati_stamp_file,
468                          overlays, installed_prebuilts, debug_file):
469  """Creates and writes a split manifest by inspecting build inputs.
470
471  Args:
472    targets: List of targets that should be buildable using the split manifest.
473    manifest_file: Path to the repo manifest to split.
474    split_manifest_file: Path to write the resulting split manifest.
475    config_files: Paths to a config XML file containing projects to add or
476      remove. See default_config.xml for an example. This flag can be passed
477      more than once to use multiple config files.
478    repo_list_file: Path to the output of the 'repo list' command.
479    ninja_build_file: Path to the combined-<target>.ninja file found in an out
480      dir.
481    ninja_binary: Path to the ninja binary.
482    module_info_file: Path to the module-info.json file found in an out dir.
483    kati_stamp_file: The path to a .kati_stamp file from a build.
484    overlays: A list of paths to treat as overlays when parsing the kati stamp
485      file.
486    installed_prebuilts: A list of paths for which to create "fake" repo
487      entries. These entries allow the tool to recognize modules that installed
488      rather than being sync'd via a manifest.
489    debug_file: If not None, the path to write JSON debug info.
490  """
491  debug_info = {}
492
493  config = ManifestSplitConfig.from_config_files(config_files)
494  original_manifest = ET.parse(manifest_file)
495
496
497  repo_projects = get_repo_projects(repo_list_file, original_manifest,
498                                    config.path_mappings)
499  repo_projects.update({ip: ip for ip in installed_prebuilts})
500
501  inputs = get_ninja_inputs(ninja_binary, ninja_build_file, targets)
502  input_projects = set(get_input_projects(repo_projects, inputs).keys())
503  for project in input_projects:
504    debug_info.setdefault(project, DebugInfo()).direct_input = True
505  logger.info(
506      "%s projects needed for Ninja-graph direct dependencies of targets \"%s\"",
507      len(input_projects), " ".join(targets))
508
509  if kati_stamp_file:
510    kati_makefiles = get_kati_makefiles(kati_stamp_file, overlays)
511    kati_makefiles_projects = get_input_projects(repo_projects, kati_makefiles)
512    for project, makefiles in kati_makefiles_projects.items():
513      debug_info.setdefault(project, DebugInfo()).kati_makefiles = makefiles
514    input_projects = input_projects.union(kati_makefiles_projects.keys())
515    logger.info("%s projects after including Kati makefiles projects.",
516                len(input_projects))
517  else:
518    logger.info("Kati makefiles projects skipped.")
519
520  for project, cfile in config.add_projects.items():
521    debug_info.setdefault(project, DebugInfo()).manual_add_config = cfile
522  for project, cfile in config.remove_projects.items():
523    debug_info.setdefault(project, DebugInfo()).manual_remove_config = cfile
524  input_projects = input_projects.union(config.add_projects.keys())
525  logger.info("%s projects after including manual additions.",
526              len(input_projects))
527
528  # Remove projects from our set of input projects before adding adjacent
529  # modules, so that no project is added only because of an adjacent
530  # dependency in a to-be-removed project.
531  input_projects = input_projects.difference(config.remove_projects.keys())
532
533  # While we still have projects whose modules we haven't checked yet,
534  if module_info_file:
535    module_info = ModuleInfo(module_info_file, repo_projects,
536                             config.ignore_paths)
537    checked_projects = set()
538    projects_to_check = input_projects.difference(checked_projects)
539    logger.info("Checking module-info dependencies for direct and adjacent modules...")
540  else:
541    logging.info("Direct and adjacent modules skipped.")
542    projects_to_check = None
543
544  iteration = 0
545
546  while projects_to_check:
547    iteration += 1
548    # check all modules in each project,
549    modules = []
550    deps_additions = set()
551
552    def process_deps(module):
553      for d in module_info.module_deps[module]:
554        if d in module_info.module_class:
555          if module_info.module_class[d] == "HEADER_LIBRARIES":
556            hla = module_info.module_project[d]
557            if hla not in input_projects:
558              deps_additions.add(hla)
559
560    for project in projects_to_check:
561      checked_projects.add(project)
562      if project not in module_info.project_modules:
563        continue
564      for module in module_info.project_modules[project]:
565        modules.append(module)
566        process_deps(module)
567
568    for project in deps_additions:
569      debug_info.setdefault(project, DebugInfo()).deps_input = True
570    input_projects = input_projects.union(deps_additions)
571    logger.info(
572        "pass %d - %d projects after including HEADER_LIBRARIES dependencies",
573        iteration, len(input_projects))
574
575    # adding those modules' input projects to our list of projects.
576    inputs = get_ninja_inputs(ninja_binary, ninja_build_file, modules)
577    adjacent_module_additions = set(
578        get_input_projects(repo_projects, inputs).keys())
579    for project in adjacent_module_additions:
580      debug_info.setdefault(project, DebugInfo()).adjacent_input = True
581    input_projects = input_projects.union(adjacent_module_additions)
582    logger.info(
583        "pass %d - %d projects after including adjacent-module Ninja-graph dependencies",
584        iteration, len(input_projects))
585
586    projects_to_check = input_projects.difference(checked_projects)
587
588  logger.info("%s projects - complete", len(input_projects))
589
590  split_manifest = update_manifest(original_manifest, input_projects,
591                                   config.remove_projects.keys())
592  split_manifest.write(split_manifest_file)
593
594  if debug_file:
595    with open(debug_file, "w") as debug_fp:
596      logger.info("Writing debug info to %s", debug_file)
597      json.dump(
598          debug_info,
599          fp=debug_fp,
600          sort_keys=True,
601          indent=2,
602          default=lambda info: info.__dict__)
603
604
605def main(argv):
606  try:
607    opts, args = getopt.getopt(argv, "h", [
608        "help",
609        "debug-file=",
610        "manifest=",
611        "split-manifest=",
612        "config=",
613        "ignore-default-config",
614        "repo-list=",
615        "ninja-build=",
616        "ninja-binary=",
617        "module-info=",
618        "skip-module-info",
619        "kati-stamp=",
620        "skip-kati",
621        "overlay=",
622        "installed-prebuilt=",
623    ])
624  except getopt.GetoptError as err:
625    print(__doc__, file=sys.stderr)
626    print("**%s**" % str(err), file=sys.stderr)
627    sys.exit(2)
628
629  debug_file = None
630  manifest_file = None
631  split_manifest_file = None
632  config_files = []
633  repo_list_file = None
634  ninja_build_file = None
635  module_info_file = None
636  ninja_binary = "prebuilts/build-tools/linux-x86/bin/ninja"
637  kati_stamp_file = None
638  overlays = []
639  installed_prebuilts = []
640  ignore_default_config = False
641  skip_kati = False
642  skip_module_info = False
643
644  for o, a in opts:
645    if o in ("-h", "--help"):
646      print(__doc__, file=sys.stderr)
647      sys.exit()
648    elif o in ("--debug-file"):
649      debug_file = a
650    elif o in ("--manifest"):
651      manifest_file = a
652    elif o in ("--split-manifest"):
653      split_manifest_file = a
654    elif o in ("--config"):
655      config_files.append(a)
656    elif o == "--ignore-default-config":
657      ignore_default_config = True
658    elif o in ("--repo-list"):
659      repo_list_file = a
660    elif o in ("--ninja-build"):
661      ninja_build_file = a
662    elif o in ("--ninja-binary"):
663      ninja_binary = a
664    elif o in ("--module-info"):
665      module_info_file = a
666    elif o == "--skip-module-info":
667      skip_module_info = True
668    elif o in ("--kati-stamp"):
669      kati_stamp_file = a
670    elif o == "--skip-kati":
671      skip_kati = True
672    elif o in ("--overlay"):
673      overlays.append(a)
674    elif o in ("--installed-prebuilt"):
675      installed_prebuilts.append(a)
676    else:
677      assert False, "unknown option \"%s\"" % o
678
679  if not args:
680    print(__doc__, file=sys.stderr)
681    print("**Missing targets**", file=sys.stderr)
682    sys.exit(2)
683  if not manifest_file:
684    print(__doc__, file=sys.stderr)
685    print("**Missing required flag --manifest**", file=sys.stderr)
686    sys.exit(2)
687  if not split_manifest_file:
688    print(__doc__, file=sys.stderr)
689    print("**Missing required flag --split-manifest**", file=sys.stderr)
690    sys.exit(2)
691
692  if skip_module_info:
693    if module_info_file:
694      logging.warning("User provided both --skip-module-info and --module-info args.  Arg --module-info ignored.")
695    module_info_file = None
696  elif not module_info_file:
697    module_info_file = os.path.join(os.environ["ANDROID_PRODUCT_OUT"],
698                                    "module-info.json")
699  if skip_kati:
700    if kati_stamp_file:
701      logging.warning("User provided both --skip-kati and --kati-stamp args.  Arg --kati-stamp ignored.")
702    kati_stamp_file = None
703  elif not kati_stamp_file:
704    kati_stamp_file = os.path.join(
705        os.environ["ANDROID_BUILD_TOP"], "out",
706        ".kati_stamp-%s" % os.environ["TARGET_PRODUCT"])
707
708  if not ninja_build_file:
709    ninja_build_file = os.path.join(
710        os.environ["ANDROID_BUILD_TOP"], "out",
711        "combined-%s.ninja" % os.environ["TARGET_PRODUCT"])
712
713  with tempfile.NamedTemporaryFile() as default_config_file:
714    if not ignore_default_config:
715      default_config_file.write(pkgutil.get_data(__name__, DEFAULT_CONFIG_XML))
716      default_config_file.flush()
717      config_files.insert(0, default_config_file.name)
718
719    create_split_manifest(
720        targets=args,
721        manifest_file=manifest_file,
722        split_manifest_file=split_manifest_file,
723        config_files=config_files,
724        repo_list_file=repo_list_file,
725        ninja_build_file=ninja_build_file,
726        ninja_binary=ninja_binary,
727        module_info_file=module_info_file,
728        kati_stamp_file=kati_stamp_file,
729        overlays=overlays,
730        installed_prebuilts=installed_prebuilts,
731        debug_file=debug_file)
732
733
734if __name__ == "__main__":
735  main(sys.argv[1:])
736