text: #!/usr/bin/env python3
  # Copyright 2013 The Flutter Authors. All rights reserved.
  # Use of this source code is governed by a BSD-style license that can be
  # found in the LICENSE file.
  
  #
  # Derivative work of https://chromium.googlesource.com/chromium/src/+/HEAD/build/config/fuchsia/prepare_package_inputs.py
  #
  """Creates a archive manifest used for Fuchsia package generation."""
  
  import argparse
  import json
  import os
  import shutil
  import subprocess
  import sys
  
  # File extension of a component manifest for each Component Framework version
  MANIFEST_VERSION_EXTENSIONS = {"v1": ".cmx", "v2": ".cm"}
  
  
  def make_package_path(file_path, roots):
      """Computes a path for |file_path| relative to one of the |roots|.
  
    Args:
      file_path: The file path to relativize.
      roots: A list of directory paths which may serve as a relative root for
        |file_path|.
  
      For example:
          * make_package_path('/foo/bar.txt', ['/foo/']) 'bar.txt'
          * make_package_path('/foo/dir/bar.txt', ['/foo/']) 'dir/bar.txt'
          * make_package_path('/foo/out/Debug/bar.exe', ['/foo/', '/foo/out/Debug/']) 'bar.exe'
    """
  
      # Prevents greedily matching against a shallow path when a deeper, better
      # matching path exists.
      roots.sort(key=len, reverse=True)
  
      for next_root in roots:
          if not next_root.endswith(os.sep):
              next_root += os.sep
  
          if file_path.startswith(next_root):
              relative_path = file_path[len(next_root):]
              return relative_path
  
      return file_path
  
  
  def _get_stripped_path(bin_path):
      """Finds the stripped version of |bin_path| in the build output directory.
  
          returns |bin_path| if no stripped path is found.
    """
      stripped_path = bin_path.replace('lib.unstripped/',
                                       'lib/').replace('exe.unstripped/', '')
      if os.path.exists(stripped_path):
          return stripped_path
      else:
          return bin_path
  
  
  def _is_binary(path):
      """Checks if the file at |path| is an ELF executable.
  
          This is done by inspecting its FourCC header.
    """
  
      with open(path, 'rb') as f:
          file_tag = f.read(4)
      return file_tag == b'\x7fELF'
  
  
  def _write_build_ids_txt(binary_paths, ids_txt_path):
      """Writes an index text file mapping build IDs to unstripped binaries."""
  
      READELF_FILE_PREFIX = 'File: '
      READELF_BUILD_ID_PREFIX = 'Build ID: '
  
      # List of binaries whose build IDs are awaiting processing by readelf.
      # Entries are removed as readelf's output is parsed.
      unprocessed_binary_paths = set(binary_paths)
      build_ids_map = {}
  
      # Sanity check that unstripped binaries do not also have their stripped
      # counterpart listed.
      for binary_path in binary_paths:
          stripped_binary_path = _get_stripped_path(binary_path)
          if stripped_binary_path != binary_path:
              unprocessed_binary_paths.discard(stripped_binary_path)
  
      with open(ids_txt_path, 'w') as ids_file:
        # TODO(richkadel): This script (originally from the Fuchsia GN SDK) was
        # changed, adding this `if unprocessed_binary_paths` check, because for
        # the Dart packages I tested (child-view and parent-view), this was
        # empty. Update the Fuchsia GN SDK? (Or figure out if the Dart packages
        # _should_ have at least one unprocessed_binary_path?)
        if unprocessed_binary_paths:
          # Create a set to dedupe stripped binary paths in case both the stripped and
          # unstripped versions of a binary are specified.
          readelf_stdout = subprocess.check_output(
              ['readelf', '-n'] + sorted(unprocessed_binary_paths)).decode('utf8')
  
          if len(binary_paths) == 1:
              # Readelf won't report a binary's path if only one was provided to the
              # tool.
              binary_path = binary_paths[0]
          else:
              binary_path = None
  
          for line in readelf_stdout.split('\n'):
              line = line.strip()
  
              if line.startswith(READELF_FILE_PREFIX):
                  binary_path = line[len(READELF_FILE_PREFIX):]
                  assert binary_path in unprocessed_binary_paths
  
              elif line.startswith(READELF_BUILD_ID_PREFIX):
                  # Paths to the unstripped executables listed in "ids.txt" are specified
                  # as relative paths to that file.
                  unstripped_rel_path = os.path.relpath(
                      os.path.abspath(binary_path),
                      os.path.dirname(os.path.abspath(ids_txt_path)))
  
                  build_id = line[len(READELF_BUILD_ID_PREFIX):]
                  build_ids_map[build_id] = unstripped_rel_path
                  unprocessed_binary_paths.remove(binary_path)
  
          for id_and_path in sorted(build_ids_map.items()):
              ids_file.write(id_and_path[0] + ' ' + id_and_path[1] + '\n')
  
      # Did readelf forget anything? Make sure that all binaries are accounted for.
      assert not unprocessed_binary_paths
  
  
  def _parse_component(component_info_file):
      component_info = json.load(open(component_info_file, 'r'))
      return component_info
  
  
  def _get_component_manifests(component_info):
      return [c for c in component_info if c.get('type') == 'manifest']
  
  
  # TODO(richkadel): Changed, from the Fuchsia GN SDK version to add this function
  # and related code, to include support for a file of resources that aren't known
  # until compile time.
  def _get_resource_items_from_json_items(component_info):
      nested_resources = []
      files = [c.get('source') for c in component_info if c.get('type') == 'json_of_resources']
      for json_file in files:
          for resource in _parse_component(json_file):
              nested_resources.append(resource)
      return nested_resources
  
  
  def _get_resource_items(component_info):
      return (
          [c for c in component_info if c.get('type') == 'resource'] +
           _get_resource_items_from_json_items(component_info)
      )
  
  
  def _get_expanded_files(runtime_deps_file):
      """ Process the runtime deps file for file paths, recursively walking
      directories as needed.
  
      Returns a set of expanded files referenced by the runtime deps file.
      """
  
      # runtime_deps may contain duplicate paths, so use a set for
      # de-duplication.
      expanded_files = set()
      for next_path in open(runtime_deps_file, 'r'):
          next_path = next_path.strip()
          if os.path.isdir(next_path):
              for root, _, files in os.walk(next_path):
                  for current_file in files:
                      if current_file.startswith('.'):
                          continue
                      expanded_files.add(
                          os.path.normpath(os.path.join(root, current_file)))
          else:
              expanded_files.add(os.path.normpath(next_path))
      return expanded_files
  
  
  def _write_gn_deps_file(
          depfile_path, package_manifest, component_manifests, out_dir,
          expanded_files):
      with open(depfile_path, 'w') as depfile:
          deps_list = [os.path.relpath(f, out_dir) for f in expanded_files]
          deps_list.extend(component_manifests)
  
          # The deps file is space-delimited, so filenames containing spaces
          # must have them escaped.
          deps_list = [f.replace(' ','\\ ') for f in deps_list]
  
          deps_string = ' '.join(sorted(deps_list))
          depfile.write('%s: %s' % (package_manifest, deps_string))
  
  
  def _write_meta_package_manifest(
          manifest_entries, manifest_path, app_name, out_dir, package_version):
      # Write meta/package manifest file and add to archive manifest.
      meta_package = os.path.join(os.path.dirname(manifest_path), 'package')
      with open(meta_package, 'w') as package_json:
          json_payload = {'version': package_version, 'name': app_name}
          json.dump(json_payload, package_json)
          package_json_filepath = os.path.relpath(package_json.name, out_dir)
          manifest_entries['meta/package'] = package_json_filepath
  
  
  def _write_component_manifest(
          manifest_entries, component_info, archive_manifest_path, out_dir):
      """Copy component manifest files and add to archive manifest.
  
      Raises an exception if a component uses a unknown manifest version.
      """
  
      for component_manifest in _get_component_manifests(component_info):
          manifest_version = component_manifest.get('manifest_version')
  
          if manifest_version not in MANIFEST_VERSION_EXTENSIONS:
              raise Exception(
                  'Unknown manifest_version: {}'.format(manifest_version))
  
          # TODO(richkadel): Changed, from the Fuchsia GN SDK version, to assume
          # the given `output_name` already includes its extension. This change
          # has not been fully validate, in particular, it has not been tested
          # with CF v2 `.cm` (from `.cml`) files. Original implementation was:
          #
          # extension = MANIFEST_VERSION_EXTENSIONS.get(manifest_version)
          # manifest_dest_file_path = os.path.join(
          #     os.path.dirname(archive_manifest_path),
          #     component_manifest.get('output_name') + extension)
          manifest_dest_file_path = os.path.join(
              os.path.dirname(archive_manifest_path),
              component_manifest.get('output_name'))
          # Add the 'meta/' subdir, for example, if `output_name` includes it
          os.makedirs(os.path.dirname(manifest_dest_file_path), exist_ok=True)
          shutil.copy(component_manifest.get('source'), manifest_dest_file_path)
  
          manifest_entries[
              'meta/%s' %
              os.path.basename(manifest_dest_file_path)] = os.path.relpath(
                  manifest_dest_file_path, out_dir)
      return manifest_dest_file_path
  
  
  def _write_package_manifest(
          manifest_entries, expanded_files, out_dir, exclude_file, root_dir,
          component_info):
      """Writes the package manifest for a Fuchsia package
  
      Returns a list of binaries in the package.
  
      Raises an exception if excluded files are not found."""
      gen_dir = os.path.normpath(os.path.join(out_dir, 'gen'))
      excluded_files_set = set(exclude_file)
      roots = [gen_dir, root_dir, out_dir]
  
      # Filter out component manifests. These are written out elsewhere.
      excluded_files_set.update(
          [
              make_package_path(
                  os.path.relpath(cf.get('source'), out_dir), roots)
              for cf in _get_component_manifests(component_info)
              if os.path.relpath(cf.get('source'), out_dir) in expanded_files
          ])
  
      # Filter out json_of_resources since only their contents are written, and we
      # don't know the contained resources until late in the build cycle
      excluded_files_set.update(
          [
              make_package_path(
                  os.path.relpath(cf.get('source'), out_dir), roots)
              for cf in component_info if cf.get('type') == 'json_of_resources' and
                  os.path.relpath(cf.get('source'), out_dir) in expanded_files
          ])
  
      # Write out resource files with specific package paths, and exclude them from
      # the list of expanded files so they are not listed twice in the manifest.
      for resource in _get_resource_items(component_info):
          relative_src_file = os.path.relpath(resource.get('source'), out_dir)
          resource_path = make_package_path(relative_src_file, roots)
          manifest_entries[resource.get('dest')] = relative_src_file
          if resource.get('type') == 'resource':
              excluded_files_set.add(resource_path)
  
      for current_file in expanded_files:
          current_file = _get_stripped_path(current_file)
          # make_package_path() may relativize to either the source root or
          # output directory.
          in_package_path = make_package_path(current_file, roots)
  
          if in_package_path in excluded_files_set:
              excluded_files_set.remove(in_package_path)
          else:
              manifest_entries[in_package_path] = current_file
  
      if excluded_files_set:
          raise Exception(
              'Some files were excluded with --exclude-file but '
              'not found in the deps list, or a resource (data) file '
              'was added and not filtered out. Excluded files and resources: '
              '%s' % ', '.join(excluded_files_set))
  
  
  def _build_manifest(args):
      # Use a sorted list to make sure the manifest order is deterministic.
      expanded_files = sorted(_get_expanded_files(args.runtime_deps_file))
      component_info = _parse_component(args.json_file)
      component_manifests = []
  
      # Collect the manifest entries in a map since duplication happens
      # because of runtime libraries.
      manifest_entries = {}
      _write_meta_package_manifest(
          manifest_entries, args.manifest_path, args.app_name, args.out_dir,
          args.package_version)
      for component_item in component_info:
          _write_package_manifest(
              manifest_entries, expanded_files, args.out_dir, args.exclude_file,
              args.root_dir, component_item)
          component_manifests.append(
              _write_component_manifest(
                  manifest_entries, component_item, args.manifest_path,
                  args.out_dir))
  
      with open(args.manifest_path, 'w') as manifest:
          for key in sorted(manifest_entries.keys()):
              manifest.write('%s=%s\n' % (key, manifest_entries[key]))
  
      binaries = [f for f in expanded_files if _is_binary(f)]
      _write_build_ids_txt(sorted(binaries), args.build_ids_file)
  
      # Omit any excluded_files from the expanded_files written to the depfile.
      gen_dir = os.path.normpath(os.path.join(args.out_dir, 'gen'))
      roots = [gen_dir, args.root_dir, args.out_dir]
      excluded_files_set = set(args.exclude_file)
      expanded_deps_files = [path for path in expanded_files
                             if make_package_path(path, roots)
                             not in excluded_files_set]
  
      _write_gn_deps_file(
          args.depfile_path, args.manifest_path, component_manifests,
          args.out_dir, expanded_deps_files)
      return 0
  
  
  def main():
      parser = argparse.ArgumentParser()
      parser.add_argument(
          '--root-dir', required=True, help='Build root directory')
      parser.add_argument(
          '--out-dir', required=True, help='Build output directory')
      parser.add_argument('--app-name', required=True, help='Package name')
      parser.add_argument(
          '--runtime-deps-file',
          required=True,
          help='File with the list of runtime dependencies.')
      parser.add_argument(
          '--depfile-path', required=True, help='Path to write GN deps file.')
      parser.add_argument(
          '--exclude-file',
          action='append',
          default=[],
          help='Package-relative file path to exclude from the package.')
      parser.add_argument(
          '--manifest-path', required=True, help='Manifest output path.')
      parser.add_argument(
          '--build-ids-file', required=True, help='Debug symbol index path.')
      parser.add_argument('--json-file', required=True)
      parser.add_argument(
          '--package-version', default='0', help='Version of the package')
  
      args = parser.parse_args()
  
      return _build_manifest(args)
  
  
  if __name__ == '__main__':
      sys.exit(main())
  