1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211

build / toolchain / gcc_solink_wrapper.py [blame]

#!/usr/bin/env python3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Runs 'ld -shared' and generates a .TOC file that's untouched when unchanged.

This script exists to avoid using complex shell commands in
gcc_toolchain.gni's tool("solink"), in case the host running the compiler
does not have a POSIX-like shell (e.g. Windows).
"""

import argparse
import os
import shlex
import subprocess
import sys

import wrapper_utils


def CollectSONAME(args):
  """Replaces: readelf -d $sofile | grep SONAME"""
  # TODO(crbug.com/40797404): Come up with a way to get this info without having
  # to bundle readelf in the toolchain package.
  toc = ''
  readelf = subprocess.Popen(wrapper_utils.CommandToRun(
      [args.readelf, '-d', args.sofile]),
                             stdout=subprocess.PIPE,
                             bufsize=-1,
                             universal_newlines=True)
  for line in readelf.stdout:
    if 'SONAME' in line:
      toc += line
  return readelf.wait(), toc


def CollectDynSym(args):
  """Replaces: nm --format=posix -g -D -p $sofile | cut -f1-2 -d' '"""
  toc = ''
  nm = subprocess.Popen(wrapper_utils.CommandToRun(
      [args.nm, '--format=posix', '-g', '-D', '-p', args.sofile]),
                        stdout=subprocess.PIPE,
                        bufsize=-1,
                        universal_newlines=True)
  for line in nm.stdout:
    toc += ' '.join(line.split(' ', 2)[:2]) + '\n'
  return nm.wait(), toc


def CollectTOC(args):
  result, toc = CollectSONAME(args)
  if result == 0:
    result, dynsym = CollectDynSym(args)
    toc += dynsym
  return result, toc


def UpdateTOC(tocfile, toc):
  if os.path.exists(tocfile):
    old_toc = open(tocfile, 'r').read()
  else:
    old_toc = None
  if toc != old_toc:
    open(tocfile, 'w').write(toc)


def CollectInputs(out, args):
  for x in args:
    if x.startswith('@'):
      with open(x[1:]) as rsp:
        CollectInputs(out, shlex.split(rsp.read()))
    elif not x.startswith('-') and (x.endswith('.o') or x.endswith('.a')):
      out.write(x)
      out.write('\n')


def InterceptFlag(flag, command):
  ret = flag in command
  if ret:
    command.remove(flag)
  return ret


def SafeDelete(path):
  try:
    os.unlink(path)
  except OSError:
    pass


def main():
  parser = argparse.ArgumentParser(description=__doc__)
  parser.add_argument('--readelf',
                      required=True,
                      help='The readelf binary to run',
                      metavar='PATH')
  parser.add_argument('--nm',
                      required=True,
                      help='The nm binary to run',
                      metavar='PATH')
  parser.add_argument('--strip',
                      help='The strip binary to run',
                      metavar='PATH')
  parser.add_argument('--dwp', help='The dwp binary to run', metavar='PATH')
  parser.add_argument('--sofile',
                      required=True,
                      help='Shared object file produced by linking command',
                      metavar='FILE')
  parser.add_argument('--tocfile',
                      required=True,
                      help='Output table-of-contents file',
                      metavar='FILE')
  parser.add_argument('--map-file',
                      help=('Use --Wl,-Map to generate a map file. Will be '
                            'gzipped if extension ends with .gz'),
                      metavar='FILE')
  parser.add_argument('--output',
                      required=True,
                      help='Final output shared object file',
                      metavar='FILE')
  parser.add_argument('command', nargs='+',
                      help='Linking command')
  args = parser.parse_args()

  # Work-around for gold being slow-by-default. http://crbug.com/632230
  fast_env = dict(os.environ)
  fast_env['LC_ALL'] = 'C'

  # Extract flags passed through ldflags but meant for this script.
  # https://crbug.com/954311 tracks finding a better way to plumb these.
  partitioned_library = InterceptFlag('--partitioned-library', args.command)
  collect_inputs_only = InterceptFlag('--collect-inputs-only', args.command)

  # Partitioned .so libraries are used only for splitting apart in a subsequent
  # step.
  #
  # - The TOC file optimization isn't useful, because the partition libraries
  #   must always be re-extracted if the combined library changes (and nothing
  #   should be depending on the combined library's dynamic symbol table).
  # - Stripping isn't necessary, because the combined library is not used in
  #   production or published.
  #
  # Both of these operations could still be done, they're needless work, and
  # tools would need to be updated to handle and/or not complain about
  # partitioned libraries. Instead, to keep Ninja happy, simply create dummy
  # files for the TOC and stripped lib.
  if collect_inputs_only or partitioned_library:
    open(args.output, 'w').close()
    open(args.tocfile, 'w').close()

  # Instead of linking, records all inputs to a file. This is used by
  # enable_resource_allowlist_generation in order to avoid needing to
  # link (which is slow) to build the resources allowlist.
  if collect_inputs_only:
    if args.map_file:
      open(args.map_file, 'w').close()
    if args.dwp:
      open(args.sofile + '.dwp', 'w').close()

    with open(args.sofile, 'w') as f:
      CollectInputs(f, args.command)
    return 0

  # First, run the actual link.
  command = wrapper_utils.CommandToRun(args.command)
  result = wrapper_utils.RunLinkWithOptionalMapFile(command,
                                                    env=fast_env,
                                                    map_file=args.map_file)

  if result != 0:
    return result

  # If dwp is set, then package debug info for this SO.
  dwp_proc = None
  if args.dwp:
    # Explicit delete to account for symlinks (when toggling between
    # debug/release).
    SafeDelete(args.sofile + '.dwp')
    # Suppress warnings about duplicate CU entries (https://crbug.com/1264130)
    dwp_proc = subprocess.Popen(wrapper_utils.CommandToRun(
        [args.dwp, '-e', args.sofile, '-o', args.sofile + '.dwp']),
                                stderr=subprocess.DEVNULL)

  if not partitioned_library:
    # Next, generate the contents of the TOC file.
    result, toc = CollectTOC(args)
    if result != 0:
      return result

    # If there is an existing TOC file with identical contents, leave it alone.
    # Otherwise, write out the TOC file.
    UpdateTOC(args.tocfile, toc)

    # Finally, strip the linked shared object file (if desired).
    if args.strip:
      result = subprocess.call(
          wrapper_utils.CommandToRun(
              [args.strip, '-o', args.output, args.sofile]))

  if dwp_proc:
    dwp_result = dwp_proc.wait()
    if dwp_result != 0:
      sys.stderr.write('dwp failed with error code {}\n'.format(dwp_result))
      return dwp_result

  return result


if __name__ == "__main__":
  sys.exit(main())