| #!/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. |
| -D<sym> Define a macro for the preprocessor. |
| /fo<out> Set path of output .res file. |
| /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 = [] |
| 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('-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 == '/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', 'input', |
| 'show_includes']) |
| return Flags(includes=includes, defines=defines, output=output, 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 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.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 almost works. The one exception |
| # is struct tagCRGB in dlgs.h, but that will be fixed in the next major |
| # SDK release too. |
| # TODO(thakis): Remove this once an SDK with the fix has been released. |
| preprocessed_output = re.sub('typedef struct tagCRGB\s*{[^}]*} CRGB;', '', |
| preprocessed_output) |
| 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): rc 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) |
| return p.returncode |
| |
| |
| 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) |
| return RunRc(preprocessed_output, is_utf8, flags) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |