summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/toolchain/win/rc/rc.py
blob: 2ab41225fba8d22a974286835e4c454ef2e54f54 (plain)
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
276
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# 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 __future__ import print_function
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())