• 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_stamp_dump 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_stamp_dump",
330          "--files",
331          kati_stamp_file,
332      ]).decode().strip("\n").split("\n"))
333
334  def is_product_makefile(makefile):
335    """Returns True if the makefile path meets certain criteria."""
336    banned_prefixes = [
337        "out/",
338        # Ignore product makefiles for sample AOSP boards.
339        "device/amlogic",
340        "device/generic",
341        "device/google",
342        "device/linaro",
343        "device/sample",
344    ]
345    banned_suffixes = [
346        # All Android.mk files in the source are always parsed by Kati,
347        # so including them here would bring in lots of unnecessary projects.
348        "Android.mk",
349        # The ckati stamp file always includes a line for the ckati bin at
350        # the beginnning.
351        "bin/ckati",
352    ]
353    return (all([not makefile.startswith(p) for p in banned_prefixes]) and
354            all([not makefile.endswith(s) for s in banned_suffixes]))
355
356  # Limit the makefiles to only product makefiles.
357  product_makefiles = {
358      os.path.normpath(path) for path in makefiles if is_product_makefile(path)
359  }
360
361  def strip_overlay(makefile):
362    """Remove any overlays from a makefile path."""
363    for overlay in overlays:
364      if makefile.startswith(overlay):
365        return makefile[len(overlay):]
366    return makefile
367
368  makefiles_and_symlinks = set()
369  for makefile in product_makefiles:
370    # Search for the makefile, possibly scanning overlays as well.
371    for overlay in [""] + overlays:
372      makefile_with_overlay = os.path.join(overlay, makefile)
373      if os.path.exists(makefile_with_overlay):
374        makefile = makefile_with_overlay
375        break
376
377    if not os.path.exists(makefile):
378      logger.warning("Unknown kati makefile: %s" % makefile)
379      continue
380
381    # Ensure the project that contains the makefile is included, as well as
382    # the project that any makefile symlinks point to.
383    makefiles_and_symlinks.add(strip_overlay(makefile))
384    if os.path.islink(makefile):
385      makefiles_and_symlinks.add(
386          strip_overlay(os.path.relpath(os.path.realpath(makefile))))
387
388  return makefiles_and_symlinks
389
390
391def scan_repo_projects(repo_projects, input_path):
392  """Returns the project path of the given input path if it exists.
393
394  Args:
395    repo_projects: The output of the get_repo_projects function.
396    input_path: The path of an input file used in the build, as given by the
397      ninja inputs tool.
398
399  Returns:
400    The path string, or None if not found.
401  """
402  parts = input_path.split("/")
403
404  for index in reversed(range(0, len(parts))):
405    project_path = os.path.join(*parts[:index + 1])
406    if project_path in repo_projects:
407      return project_path
408
409  return None
410
411
412def get_input_projects(repo_projects, inputs):
413  """Returns the collection of project names that contain the given input paths.
414
415  Args:
416    repo_projects: The output of the get_repo_projects function.
417    inputs: The paths of input files used in the build, as given by the ninja
418      inputs tool.
419  """
420  input_project_paths = {}
421  for input_path in inputs:
422    if not input_path.startswith("out/") and not input_path.startswith("/"):
423      input_project_paths.setdefault(
424          scan_repo_projects(repo_projects, input_path), []).append(input_path)
425
426  return {
427      repo_projects[project_path]: inputs
428      for project_path, inputs in input_project_paths.items()
429      if project_path is not None
430  }
431
432
433def update_manifest(manifest, input_projects, remove_projects):
434  """Modifies and returns a manifest ElementTree by modifying its projects.
435
436  Args:
437    manifest: The manifest object to modify.
438    input_projects: A set of projects that should stay in the manifest.
439    remove_projects: A set of projects that should be removed from the manifest.
440      Projects in this set override input_projects.
441
442  Returns:
443    The modified manifest object.
444  """
445  projects_to_keep = input_projects.difference(remove_projects)
446  root = manifest.getroot()
447  for child in root.findall("project"):
448    if child.attrib["name"] not in projects_to_keep:
449      root.remove(child)
450  return manifest
451
452
453@dataclasses.dataclass
454class DebugInfo:
455  """Simple class to store structured debug info for a project."""
456  direct_input: bool = False
457  adjacent_input: bool = False
458  deps_input: bool = False
459  kati_makefiles: List[str] = dataclasses.field(default_factory=list)
460  manual_add_config: str = ""
461  manual_remove_config: str = ""
462
463
464def create_split_manifest(targets, manifest_file, split_manifest_file,
465                          config_files, repo_list_file, ninja_build_file,
466                          ninja_binary, module_info_file, kati_stamp_file,
467                          overlays, installed_prebuilts, debug_file):
468  """Creates and writes a split manifest by inspecting build inputs.
469
470  Args:
471    targets: List of targets that should be buildable using the split manifest.
472    manifest_file: Path to the repo manifest to split.
473    split_manifest_file: Path to write the resulting split manifest.
474    config_files: Paths to a config XML file containing projects to add or
475      remove. See default_config.xml for an example. This flag can be passed
476      more than once to use multiple config files.
477    repo_list_file: Path to the output of the 'repo list' command.
478    ninja_build_file: Path to the combined-<target>.ninja file found in an out
479      dir.
480    ninja_binary: Path to the ninja binary.
481    module_info_file: Path to the module-info.json file found in an out dir.
482    kati_stamp_file: The path to a .kati_stamp file from a build.
483    overlays: A list of paths to treat as overlays when parsing the kati stamp
484      file.
485    installed_prebuilts: A list of paths for which to create "fake" repo
486      entries. These entries allow the tool to recognize modules that installed
487      rather than being sync'd via a manifest.
488    debug_file: If not None, the path to write JSON debug info.
489  """
490  debug_info = {}
491
492  config = ManifestSplitConfig.from_config_files(config_files)
493  original_manifest = ET.parse(manifest_file)
494
495
496  repo_projects = get_repo_projects(repo_list_file, original_manifest,
497                                    config.path_mappings)
498  repo_projects.update({ip: ip for ip in installed_prebuilts})
499
500  inputs = get_ninja_inputs(ninja_binary, ninja_build_file, targets)
501  input_projects = set(get_input_projects(repo_projects, inputs).keys())
502  for project in input_projects:
503    debug_info.setdefault(project, DebugInfo()).direct_input = True
504  logger.info(
505      "%s projects needed for Ninja-graph direct dependencies of targets \"%s\"",
506      len(input_projects), " ".join(targets))
507
508  if kati_stamp_file:
509    kati_makefiles = get_kati_makefiles(kati_stamp_file, overlays)
510    kati_makefiles_projects = get_input_projects(repo_projects, kati_makefiles)
511    for project, makefiles in kati_makefiles_projects.items():
512      debug_info.setdefault(project, DebugInfo()).kati_makefiles = makefiles
513    input_projects = input_projects.union(kati_makefiles_projects.keys())
514    logger.info("%s projects after including Kati makefiles projects.",
515                len(input_projects))
516  else:
517    logger.info("Kati makefiles projects skipped.")
518
519  for project, cfile in config.add_projects.items():
520    debug_info.setdefault(project, DebugInfo()).manual_add_config = cfile
521  for project, cfile in config.remove_projects.items():
522    debug_info.setdefault(project, DebugInfo()).manual_remove_config = cfile
523  input_projects = input_projects.union(config.add_projects.keys())
524  logger.info("%s projects after including manual additions.",
525              len(input_projects))
526
527  # Remove projects from our set of input projects before adding adjacent
528  # modules, so that no project is added only because of an adjacent
529  # dependency in a to-be-removed project.
530  input_projects = input_projects.difference(config.remove_projects.keys())
531
532  # While we still have projects whose modules we haven't checked yet,
533  if module_info_file:
534    module_info = ModuleInfo(module_info_file, repo_projects,
535                             config.ignore_paths)
536    checked_projects = set()
537    projects_to_check = input_projects.difference(checked_projects)
538    logger.info("Checking module-info dependencies for direct and adjacent modules...")
539  else:
540    logging.info("Direct and adjacent modules skipped.")
541    projects_to_check = None
542
543  iteration = 0
544
545  while projects_to_check:
546    iteration += 1
547    # check all modules in each project,
548    modules = []
549    deps_additions = set()
550
551    def process_deps(module):
552      for d in module_info.module_deps[module]:
553        if d in module_info.module_class:
554          if module_info.module_class[d] == "HEADER_LIBRARIES":
555            hla = module_info.module_project[d]
556            if hla not in input_projects:
557              deps_additions.add(hla)
558
559    for project in projects_to_check:
560      checked_projects.add(project)
561      if project not in module_info.project_modules:
562        continue
563      for module in module_info.project_modules[project]:
564        modules.append(module)
565        process_deps(module)
566
567    for project in deps_additions:
568      debug_info.setdefault(project, DebugInfo()).deps_input = True
569    input_projects = input_projects.union(deps_additions)
570    logger.info(
571        "pass %d - %d projects after including HEADER_LIBRARIES dependencies",
572        iteration, len(input_projects))
573
574    # adding those modules' input projects to our list of projects.
575    inputs = get_ninja_inputs(ninja_binary, ninja_build_file, modules)
576    adjacent_module_additions = set(
577        get_input_projects(repo_projects, inputs).keys())
578    for project in adjacent_module_additions:
579      debug_info.setdefault(project, DebugInfo()).adjacent_input = True
580    input_projects = input_projects.union(adjacent_module_additions)
581    logger.info(
582        "pass %d - %d projects after including adjacent-module Ninja-graph dependencies",
583        iteration, len(input_projects))
584
585    projects_to_check = input_projects.difference(checked_projects)
586
587  logger.info("%s projects - complete", len(input_projects))
588
589  split_manifest = update_manifest(original_manifest, input_projects,
590                                   config.remove_projects.keys())
591  split_manifest.write(split_manifest_file)
592
593  if debug_file:
594    with open(debug_file, "w") as debug_fp:
595      logger.info("Writing debug info to %s", debug_file)
596      json.dump(
597          debug_info,
598          fp=debug_fp,
599          sort_keys=True,
600          indent=2,
601          default=lambda info: info.__dict__)
602
603
604def main(argv):
605  try:
606    opts, args = getopt.getopt(argv, "h", [
607        "help",
608        "debug-file=",
609        "manifest=",
610        "split-manifest=",
611        "config=",
612        "ignore-default-config",
613        "repo-list=",
614        "ninja-build=",
615        "ninja-binary=",
616        "module-info=",
617        "skip-module-info",
618        "kati-stamp=",
619        "skip-kati",
620        "overlay=",
621        "installed-prebuilt=",
622    ])
623  except getopt.GetoptError as err:
624    print(__doc__, file=sys.stderr)
625    print("**%s**" % str(err), file=sys.stderr)
626    sys.exit(2)
627
628  debug_file = None
629  manifest_file = None
630  split_manifest_file = None
631  config_files = []
632  repo_list_file = None
633  ninja_build_file = None
634  module_info_file = None
635  ninja_binary = "prebuilts/build-tools/linux-x86/bin/ninja"
636  kati_stamp_file = None
637  overlays = []
638  installed_prebuilts = []
639  ignore_default_config = False
640  skip_kati = False
641  skip_module_info = False
642
643  for o, a in opts:
644    if o in ("-h", "--help"):
645      print(__doc__, file=sys.stderr)
646      sys.exit()
647    elif o in ("--debug-file"):
648      debug_file = a
649    elif o in ("--manifest"):
650      manifest_file = a
651    elif o in ("--split-manifest"):
652      split_manifest_file = a
653    elif o in ("--config"):
654      config_files.append(a)
655    elif o == "--ignore-default-config":
656      ignore_default_config = True
657    elif o in ("--repo-list"):
658      repo_list_file = a
659    elif o in ("--ninja-build"):
660      ninja_build_file = a
661    elif o in ("--ninja-binary"):
662      ninja_binary = a
663    elif o in ("--module-info"):
664      module_info_file = a
665    elif o == "--skip-module-info":
666      skip_module_info = True
667    elif o in ("--kati-stamp"):
668      kati_stamp_file = a
669    elif o == "--skip-kati":
670      skip_kati = True
671    elif o in ("--overlay"):
672      overlays.append(a)
673    elif o in ("--installed-prebuilt"):
674      installed_prebuilts.append(a)
675    else:
676      assert False, "unknown option \"%s\"" % o
677
678  if not args:
679    print(__doc__, file=sys.stderr)
680    print("**Missing targets**", file=sys.stderr)
681    sys.exit(2)
682  if not manifest_file:
683    print(__doc__, file=sys.stderr)
684    print("**Missing required flag --manifest**", file=sys.stderr)
685    sys.exit(2)
686  if not split_manifest_file:
687    print(__doc__, file=sys.stderr)
688    print("**Missing required flag --split-manifest**", file=sys.stderr)
689    sys.exit(2)
690
691  if skip_module_info:
692    if module_info_file:
693      logging.warning("User provided both --skip-module-info and --module-info args.  Arg --module-info ignored.")
694    module_info_file = None
695  elif not module_info_file:
696    module_info_file = os.path.join(os.environ["ANDROID_PRODUCT_OUT"],
697                                    "module-info.json")
698  if skip_kati:
699    if kati_stamp_file:
700      logging.warning("User provided both --skip-kati and --kati-stamp args.  Arg --kati-stamp ignored.")
701    kati_stamp_file = None
702  elif not kati_stamp_file:
703    kati_stamp_file = os.path.join(
704        os.environ["ANDROID_BUILD_TOP"], "out",
705        ".kati_stamp-%s" % os.environ["TARGET_PRODUCT"])
706
707  if not ninja_build_file:
708    ninja_build_file = os.path.join(
709        os.environ["ANDROID_BUILD_TOP"], "out",
710        "combined-%s.ninja" % os.environ["TARGET_PRODUCT"])
711
712  with tempfile.NamedTemporaryFile() as default_config_file:
713    if not ignore_default_config:
714      default_config_file.write(pkgutil.get_data(__name__, DEFAULT_CONFIG_XML))
715      default_config_file.flush()
716      config_files.insert(0, default_config_file.name)
717
718    create_split_manifest(
719        targets=args,
720        manifest_file=manifest_file,
721        split_manifest_file=split_manifest_file,
722        config_files=config_files,
723        repo_list_file=repo_list_file,
724        ninja_build_file=ninja_build_file,
725        ninja_binary=ninja_binary,
726        module_info_file=module_info_file,
727        kati_stamp_file=kati_stamp_file,
728        overlays=overlays,
729        installed_prebuilts=installed_prebuilts,
730        debug_file=debug_file)
731
732
733if __name__ == "__main__":
734  main(sys.argv[1:])
735