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
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275

build / toolchain / win / rc / rc.py [blame]

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

"""usage: rc.py [options] input.res
A resource compiler for .rc files.

options:
-h, --help     Print this message.
-I<dir>        Add include path, used for both headers and resources.
-imsvc<dir>    Add system include path, used for preprocessing only.
/winsysroot<d> Set winsysroot, used for preprocessing only.
-D<sym>        Define a macro for the preprocessor.
/fo<out>       Set path of output .res file.
/nologo        Ignored (rc.py doesn't print a logo by default).
/showIncludes  Print referenced header and resource files."""

from collections import namedtuple
import codecs
import os
import re
import subprocess
import sys
import tempfile


THIS_DIR = os.path.abspath(os.path.dirname(__file__))
SRC_DIR = \
    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR))))


def ParseFlags():
  """Parses flags off sys.argv and returns the parsed flags."""
  # Can't use optparse / argparse because of /fo flag :-/
  includes = []
  imsvcs = []
  winsysroot = []
  defines = []
  output = None
  input = None
  show_includes = False
  # Parse.
  for flag in sys.argv[1:]:
    if flag == '-h' or flag == '--help':
      print(__doc__)
      sys.exit(0)
    if flag.startswith('-I'):
      includes.append(flag)
    elif flag.startswith('-imsvc'):
      imsvcs.append(flag)
    elif flag.startswith('/winsysroot'):
      winsysroot = [flag]
    elif flag.startswith('-D'):
      defines.append(flag)
    elif flag.startswith('/fo'):
      if output:
        print('rc.py: error: multiple /fo flags', '/fo' + output, flag,
              file=sys.stderr)
        sys.exit(1)
      output = flag[3:]
    elif flag == '/nologo':
      pass
    elif flag == '/showIncludes':
      show_includes = True
    elif (flag.startswith('-') or
          (flag.startswith('/') and not os.path.exists(flag))):
      print('rc.py: error: unknown flag', flag, file=sys.stderr)
      print(__doc__, file=sys.stderr)
      sys.exit(1)
    else:
      if input:
        print('rc.py: error: multiple inputs:', input, flag, file=sys.stderr)
        sys.exit(1)
      input = flag
  # Validate and set default values.
  if not input:
    print('rc.py: error: no input file', file=sys.stderr)
    sys.exit(1)
  if not output:
    output = os.path.splitext(input)[0] + '.res'
  Flags = namedtuple('Flags', [
      'includes', 'defines', 'output', 'imsvcs', 'winsysroot', 'input',
      'show_includes'
  ])
  return Flags(includes=includes,
               defines=defines,
               output=output,
               imsvcs=imsvcs,
               winsysroot=winsysroot,
               input=input,
               show_includes=show_includes)


def ReadInput(input):
  """"Reads input and returns it. For UTF-16LEBOM input, converts to UTF-8."""
  # Microsoft's rc.exe only supports unicode in the form of UTF-16LE with a BOM.
  # Our rc binary sniffs for UTF-16LE.  If that's not found, if /utf-8 is
  # passed, the input is treated as UTF-8.  If /utf-8 is not passed and the
  # input is not UTF-16LE, then our rc errors out on characters outside of
  # 7-bit ASCII.  Since the driver always converts UTF-16LE to UTF-8 here (for
  # the preprocessor, which doesn't support UTF-16LE), our rc will either see
  # UTF-8 with the /utf-8 flag (for UTF-16LE input), or ASCII input.
  # This is compatible with Microsoft rc.exe.  If we wanted, we could expose
  # a /utf-8 flag for the driver for UTF-8 .rc inputs too.
  # TODO(thakis): Microsoft's rc.exe supports BOM-less UTF-16LE. We currently
  # don't, but for chrome it currently doesn't matter.
  is_utf8 = False
  try:
    with open(input, 'rb') as rc_file:
      rc_file_data = rc_file.read()
      if rc_file_data.startswith(codecs.BOM_UTF16_LE):
        rc_file_data = rc_file_data[2:].decode('utf-16le').encode('utf-8')
        is_utf8 = True
  except IOError:
    print('rc.py: failed to open', input, file=sys.stderr)
    sys.exit(1)
  except UnicodeDecodeError:
    print('rc.py: failed to decode UTF-16 despite BOM', input, file=sys.stderr)
    sys.exit(1)
  return rc_file_data, is_utf8


def Preprocess(rc_file_data, flags):
  """Runs the input file through the preprocessor."""
  clang = os.path.join(SRC_DIR, 'third_party', 'llvm-build',
                       'Release+Asserts', 'bin', 'clang-cl')
  # Let preprocessor write to a temp file so that it doesn't interfere
  # with /showIncludes output on stdout.
  if sys.platform == 'win32':
    clang += '.exe'
  temp_handle, temp_file = tempfile.mkstemp(suffix='.i')
  # Closing temp_handle immediately defeats the purpose of mkstemp(), but I
  # can't figure out how to let write to the temp file on Windows otherwise.
  os.close(temp_handle)
  clang_cmd = [clang, '/P', '/DRC_INVOKED', '/TC', '-', '/Fi' + temp_file]
  if flags.imsvcs:
    clang_cmd += ['/X']
  if os.path.dirname(flags.input):
    # This must precede flags.includes.
    clang_cmd.append('-I' + os.path.dirname(flags.input))
  if flags.show_includes:
    clang_cmd.append('/showIncludes')
  clang_cmd += flags.imsvcs + flags.winsysroot + flags.includes + flags.defines
  p = subprocess.Popen(clang_cmd, stdin=subprocess.PIPE)
  p.communicate(input=rc_file_data)
  if p.returncode != 0:
    sys.exit(p.returncode)
  preprocessed_output = open(temp_file, 'rb').read()
  os.remove(temp_file)

  # rc.exe has a wacko preprocessor:
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa381033(v=vs.85).aspx
  # """RC treats files with the .c and .h extensions in a special manner. It
  # assumes that a file with one of these extensions does not contain
  # resources. If a file has the .c or .h file name extension, RC ignores all
  # lines in the file except the preprocessor directives."""
  # Thankfully, the Microsoft headers are mostly good about putting everything
  # in the system headers behind `if !defined(RC_INVOKED)`, so regular
  # preprocessing with RC_INVOKED defined works.
  return preprocessed_output


def RunRc(preprocessed_output, is_utf8, flags):
  if sys.platform.startswith('linux'):
    rc = os.path.join(THIS_DIR, 'linux64', 'rc')
  elif sys.platform == 'darwin':
    rc = os.path.join(THIS_DIR, 'mac', 'rc')
  elif sys.platform == 'win32':
    rc = os.path.join(THIS_DIR, 'win', 'rc.exe')
  else:
    print('rc.py: error: unsupported platform', sys.platform, file=sys.stderr)
    sys.exit(1)
  rc_cmd = [rc]
  # Make sure rc-relative resources can be found:
  if os.path.dirname(flags.input):
    rc_cmd.append('/cd' + os.path.dirname(flags.input))
  rc_cmd.append('/fo' + flags.output)
  if is_utf8:
    rc_cmd.append('/utf-8')
  # TODO(thakis): cl currently always prints full paths for /showIncludes,
  # but clang-cl /P doesn't.  Which one is right?
  if flags.show_includes:
    rc_cmd.append('/showIncludes')
  # Microsoft rc.exe searches for referenced files relative to -I flags in
  # addition to the pwd, so -I flags need to be passed both to both
  # the preprocessor and rc.
  rc_cmd += flags.includes
  p = subprocess.Popen(rc_cmd, stdin=subprocess.PIPE)
  p.communicate(input=preprocessed_output)

  if flags.show_includes and p.returncode == 0:
    TOOL_DIR = os.path.dirname(os.path.relpath(THIS_DIR)).replace("\\", "/")
    # Since tool("rc") can't have deps, add deps on this script and on rc.py
    # and its deps here, so that rc edges become dirty if rc.py changes.
    print('Note: including file: {}/tool_wrapper.py'.format(TOOL_DIR))
    print('Note: including file: {}/rc/rc.py'.format(TOOL_DIR))
    print(
        'Note: including file: {}/rc/linux64/rc.sha1'.format(TOOL_DIR))
    print('Note: including file: {}/rc/mac/rc.sha1'.format(TOOL_DIR))
    print(
        'Note: including file: {}/rc/win/rc.exe.sha1'.format(TOOL_DIR))

  return p.returncode


def CompareToMsRcOutput(preprocessed_output, is_utf8, flags):
  msrc_in = flags.output + '.preprocessed.rc'

  # Strip preprocessor line markers.
  preprocessed_output = re.sub(br'^#.*$', b'', preprocessed_output, flags=re.M)
  if is_utf8:
    preprocessed_output = preprocessed_output.decode('utf-8').encode('utf-16le')
  with open(msrc_in, 'wb') as f:
    f.write(preprocessed_output)

  msrc_out = flags.output + '_ms_rc'
  msrc_cmd = ['rc', '/nologo', '/x', '/fo' + msrc_out]

  # Make sure rc-relative resources can be found. rc.exe looks for external
  # resource files next to the file, but the preprocessed file isn't where the
  # input was.
  # Note that rc searches external resource files in the order of
  # 1. next to the input file
  # 2. relative to cwd
  # 3. next to -I directories
  # Changing the cwd means we'd have to rewrite all -I flags, so just add
  # the input file dir as -I flag. That technically gets the order of 1 and 2
  # wrong, but in Chromium's build the cwd is the gn out dir, and generated
  # files there are in obj/ and gen/, so this difference doesn't matter in
  # practice.
  if os.path.dirname(flags.input):
    msrc_cmd += [ '-I' + os.path.dirname(flags.input) ]

  # Microsoft rc.exe searches for referenced files relative to -I flags in
  # addition to the pwd, so -I flags need to be passed both to both
  # the preprocessor and rc.
  msrc_cmd += flags.includes

  # Input must come last.
  msrc_cmd += [ msrc_in ]

  rc_exe_exit_code = subprocess.call(msrc_cmd)
  # Assert Microsoft rc.exe and rc.py produced identical .res files.
  if rc_exe_exit_code == 0:
    import filecmp
    assert filecmp.cmp(msrc_out, flags.output)
  return rc_exe_exit_code


def main():
  # This driver has to do these things:
  # 1. Parse flags.
  # 2. Convert the input from UTF-16LE to UTF-8 if needed.
  # 3. Pass the input through a preprocessor (and clean up the preprocessor's
  #    output in minor ways).
  # 4. Call rc for the heavy lifting.
  flags = ParseFlags()
  rc_file_data, is_utf8 = ReadInput(flags.input)
  preprocessed_output = Preprocess(rc_file_data, flags)
  rc_exe_exit_code = RunRc(preprocessed_output, is_utf8, flags)

  # 5. On Windows, we also call Microsoft's rc.exe and check that we produced
  #   the same output.
  # Since Microsoft's rc has a preprocessor that only accepts 32 characters
  # for macro names, feed the clang-preprocessed source into it instead
  # of using ms rc's preprocessor.
  if sys.platform == 'win32' and rc_exe_exit_code == 0:
    rc_exe_exit_code = CompareToMsRcOutput(preprocessed_output, is_utf8, flags)

  return rc_exe_exit_code


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