blob: 2ab41225fba8d22a974286835e4c454ef2e54f54 [file] [log] [blame]
#!/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: [options] input.res
A resource compiler for .rc files.
-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 ( 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__))
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':
if flag.startswith('-I'):
elif flag.startswith('-imsvc'):
elif flag.startswith('/winsysroot'):
winsysroot = [flag]
elif flag.startswith('-D'):
elif flag.startswith('/fo'):
if output:
print(' error: multiple /fo flags', '/fo' + output, flag,
output = flag[3:]
elif flag == '/nologo':
elif flag == '/showIncludes':
show_includes = True
elif (flag.startswith('-') or
(flag.startswith('/') and not os.path.exists(flag))):
print(' error: unknown flag', flag, file=sys.stderr)
print(__doc__, file=sys.stderr)
if input:
print(' error: multiple inputs:', input, flag, file=sys.stderr)
input = flag
# Validate and set default values.
if not input:
print(' error: no input file', file=sys.stderr)
if not output:
output = os.path.splitext(input)[0] + '.res'
Flags = namedtuple('Flags', [
'includes', 'defines', 'output', 'imsvcs', 'winsysroot', 'input',
return Flags(includes=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
with open(input, 'rb') as rc_file:
rc_file_data =
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(' failed to open', input, file=sys.stderr)
except UnicodeDecodeError:
print(' failed to decode UTF-16 despite BOM', input, file=sys.stderr)
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.
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 += flags.imsvcs + flags.winsysroot + flags.includes + flags.defines
p = subprocess.Popen(clang_cmd, stdin=subprocess.PIPE)
if p.returncode != 0:
preprocessed_output = open(temp_file, 'rb').read()
# rc.exe has a wacko preprocessor:
# """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')
print(' error: unsupported platform', sys.platform, file=sys.stderr)
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:
# TODO(thakis): cl currently always prints full paths for /showIncludes,
# but clang-cl /P doesn't. Which one is right?
if flags.show_includes:
# 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)
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
# and its deps here, so that rc edges become dirty if changes.
print('Note: including file: {}/'.format(TOOL_DIR))
print('Note: including file: {}/rc/'.format(TOOL_DIR))
'Note: including file: {}/rc/linux64/rc.sha1'.format(TOOL_DIR))
print('Note: including file: {}/rc/mac/rc.sha1'.format(TOOL_DIR))
'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:
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 =
# Assert Microsoft rc.exe and 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__':