chromium/third_party/fuchsia-gn-sdk/src/populate_build_id_dir.py

#!/usr/bin/env python3.8
#
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Populates a .build-id directory of unstripped binaries cataloged by a list
of ids.txt files."""

import argparse
import os
import shutil
import subprocess
import sys


def populate_build_id_dir(readelf_exec, ids_txt_path, output_dir, build_id_dirs, filenames):
    """
  Processes an ids.txt file, populating the output .build-id directory.

  Each unstripped ELF binary is placed in a hierarchy keyed by the GNU build
  ID. Each binary resides in a directory whose name is the first two characters
  of the build ID, with the binary file itself named after the remaining
  characters of the build ID. So, a binary file with the build ID "deadbeef"
  would be located at the path 'output_dir/de/adbeef.debug'.

  See documentation at https://fedoraproject.org/wiki/Releases/FeatureBuildId.

  Args:
    ids_txt_path (str): Path to ids.txt file to process.
    output_dir (str): Path to output .build-id directory.
    build_id_dirs (list of str): Paths to directory containing symbols for
      prebuilts.
    filenames (set): Set to store binary filenames which were hardlinked to
      the output .build-id directory.
  """
    for line in open(ids_txt_path, 'r'):
        build_id, binary_path = line.strip().split(' ')
        output_filename = os.path.join(build_id[:2], build_id[2:] + '.debug')

        # Check first if we have the debug binary under any |build_id_dirs|.
        symbol_source_path = _find_binary_file(output_filename, build_id_dirs)

        # Otherwise, check in the ids.txt directory itself, assuming
        # relative paths in ids.txt.
        if not symbol_source_path:
            symbol_source_path = os.path.abspath(
                os.path.join(os.path.dirname(ids_txt_path), binary_path))

        # Don't check zero length files, they exist as placeholders for prebuilts.
        if os.path.getsize(symbol_source_path) == 0:
            continue

        # Exclude stripped binaries (indicated by their lack of symbol tables).
        readelf_args = [readelf_exec, '-S', symbol_source_path]
        readelf_output = subprocess.check_output(
            readelf_args, universal_newlines=True, text=True)
        if '.symtab' not in readelf_output:
            continue

        output_path = os.path.join(output_dir, output_filename)
        if not os.path.exists(os.path.dirname(output_path)):
            os.makedirs(os.path.dirname(output_path))
        if not os.path.exists(output_path):
            os.link(symbol_source_path, output_path)
        filenames.add(output_path)


def _find_binary_file(binary_file, build_id_dirs):
    """Look for the binary_file in the list of build_id_dirs."""
    for dir in build_id_dirs:
        filepath = os.path.join(dir, binary_file)
        if os.path.exists(filepath):
            return filepath
    return None


def main(args):
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'ids_txt_paths',
        type=str,
        help='Path to a file, which is a newline-separated list '
        'of paths to ids.txt files.')
    parser.add_argument(
        '--output_dir',
        type=str,
        required=True,
        help='Path to output .build-id dir.')
    parser.add_argument(
        '--build-id-dir',
        type=str,
        required=True,
        action='append',
        help='Directory containing symbols. Can be specified multiple times')
    parser.add_argument(
        '--depfile', type=str, required=True, help='Path to the depfile.')
    parser.add_argument(
        '--stamp', type=str, required=True, help='Path to stamp file.')
    parser.add_argument(
        '--readelf-exec', default='readelf', help='readelf executable to use.')
    args = parser.parse_args(args)

    # If the output directory already exists, wipe it, so the directory does
    # not accumulate over multiple invocations. This is important since the
    # same executable will have a different name when its content changes.
    if os.path.exists(args.output_dir):
        shutil.rmtree(args.output_dir)

    with open(args.ids_txt_paths, 'r') as f:
        ids_txt_paths = f.read().splitlines()

    filenames = set()
    for ids_txt_path in ids_txt_paths:
        populate_build_id_dir(args.readelf_exec, ids_txt_path, args.output_dir, args.build_id_dir, filenames)

    with open(args.depfile, 'w') as f:
        f.writelines('%s: %s\n' % (args.stamp, ' '.join(sorted(filenames))))
    with open(args.stamp, 'w') as f:
        os.utime(f.name, None)

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))