1#!/usr/bin/env python3 2# Copyright 2017 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""usage: rc.py [options] input.res 7A resource compiler for .rc files. 8 9options: 10-h, --help Print this message. 11-I<dir> Add include path, used for both headers and resources. 12-imsvc<dir> Add system include path, used for preprocessing only. 13/winsysroot<d> Set winsysroot, used for preprocessing only. 14-D<sym> Define a macro for the preprocessor. 15/fo<out> Set path of output .res file. 16/nologo Ignored (rc.py doesn't print a logo by default). 17/showIncludes Print referenced header and resource files.""" 18 19from collections import namedtuple 20import codecs 21import os 22import re 23import subprocess 24import sys 25import tempfile 26 27 28THIS_DIR = os.path.abspath(os.path.dirname(__file__)) 29SRC_DIR = \ 30 os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR)))) 31 32 33def ParseFlags(): 34 """Parses flags off sys.argv and returns the parsed flags.""" 35 # Can't use optparse / argparse because of /fo flag :-/ 36 includes = [] 37 imsvcs = [] 38 winsysroot = [] 39 defines = [] 40 output = None 41 input = None 42 show_includes = False 43 # Parse. 44 for flag in sys.argv[1:]: 45 if flag == '-h' or flag == '--help': 46 print(__doc__) 47 sys.exit(0) 48 if flag.startswith('-I'): 49 includes.append(flag) 50 elif flag.startswith('-imsvc'): 51 imsvcs.append(flag) 52 elif flag.startswith('/winsysroot'): 53 winsysroot = [flag] 54 elif flag.startswith('-D'): 55 defines.append(flag) 56 elif flag.startswith('/fo'): 57 if output: 58 print('rc.py: error: multiple /fo flags', '/fo' + output, flag, 59 file=sys.stderr) 60 sys.exit(1) 61 output = flag[3:] 62 elif flag == '/nologo': 63 pass 64 elif flag == '/showIncludes': 65 show_includes = True 66 elif (flag.startswith('-') or 67 (flag.startswith('/') and not os.path.exists(flag))): 68 print('rc.py: error: unknown flag', flag, file=sys.stderr) 69 print(__doc__, file=sys.stderr) 70 sys.exit(1) 71 else: 72 if input: 73 print('rc.py: error: multiple inputs:', input, flag, file=sys.stderr) 74 sys.exit(1) 75 input = flag 76 # Validate and set default values. 77 if not input: 78 print('rc.py: error: no input file', file=sys.stderr) 79 sys.exit(1) 80 if not output: 81 output = os.path.splitext(input)[0] + '.res' 82 Flags = namedtuple('Flags', [ 83 'includes', 'defines', 'output', 'imsvcs', 'winsysroot', 'input', 84 'show_includes' 85 ]) 86 return Flags(includes=includes, 87 defines=defines, 88 output=output, 89 imsvcs=imsvcs, 90 winsysroot=winsysroot, 91 input=input, 92 show_includes=show_includes) 93 94 95def ReadInput(input): 96 """"Reads input and returns it. For UTF-16LEBOM input, converts to UTF-8.""" 97 # Microsoft's rc.exe only supports unicode in the form of UTF-16LE with a BOM. 98 # Our rc binary sniffs for UTF-16LE. If that's not found, if /utf-8 is 99 # passed, the input is treated as UTF-8. If /utf-8 is not passed and the 100 # input is not UTF-16LE, then our rc errors out on characters outside of 101 # 7-bit ASCII. Since the driver always converts UTF-16LE to UTF-8 here (for 102 # the preprocessor, which doesn't support UTF-16LE), our rc will either see 103 # UTF-8 with the /utf-8 flag (for UTF-16LE input), or ASCII input. 104 # This is compatible with Microsoft rc.exe. If we wanted, we could expose 105 # a /utf-8 flag for the driver for UTF-8 .rc inputs too. 106 # TODO(thakis): Microsoft's rc.exe supports BOM-less UTF-16LE. We currently 107 # don't, but for chrome it currently doesn't matter. 108 is_utf8 = False 109 try: 110 with open(input, 'rb') as rc_file: 111 rc_file_data = rc_file.read() 112 if rc_file_data.startswith(codecs.BOM_UTF16_LE): 113 rc_file_data = rc_file_data[2:].decode('utf-16le').encode('utf-8') 114 is_utf8 = True 115 except IOError: 116 print('rc.py: failed to open', input, file=sys.stderr) 117 sys.exit(1) 118 except UnicodeDecodeError: 119 print('rc.py: failed to decode UTF-16 despite BOM', input, file=sys.stderr) 120 sys.exit(1) 121 return rc_file_data, is_utf8 122 123 124def Preprocess(rc_file_data, flags): 125 """Runs the input file through the preprocessor.""" 126 clang = os.path.join(SRC_DIR, 'third_party', 'llvm-build', 127 'Release+Asserts', 'bin', 'clang-cl') 128 # Let preprocessor write to a temp file so that it doesn't interfere 129 # with /showIncludes output on stdout. 130 if sys.platform == 'win32': 131 clang += '.exe' 132 temp_handle, temp_file = tempfile.mkstemp(suffix='.i') 133 # Closing temp_handle immediately defeats the purpose of mkstemp(), but I 134 # can't figure out how to let write to the temp file on Windows otherwise. 135 os.close(temp_handle) 136 clang_cmd = [clang, '/P', '/DRC_INVOKED', '/TC', '-', '/Fi' + temp_file] 137 if flags.imsvcs: 138 clang_cmd += ['/X'] 139 if os.path.dirname(flags.input): 140 # This must precede flags.includes. 141 clang_cmd.append('-I' + os.path.dirname(flags.input)) 142 if flags.show_includes: 143 clang_cmd.append('/showIncludes') 144 clang_cmd += flags.imsvcs + flags.winsysroot + flags.includes + flags.defines 145 p = subprocess.Popen(clang_cmd, stdin=subprocess.PIPE) 146 p.communicate(input=rc_file_data) 147 if p.returncode != 0: 148 sys.exit(p.returncode) 149 preprocessed_output = open(temp_file, 'rb').read() 150 os.remove(temp_file) 151 152 # rc.exe has a wacko preprocessor: 153 # https://msdn.microsoft.com/en-us/library/windows/desktop/aa381033(v=vs.85).aspx 154 # """RC treats files with the .c and .h extensions in a special manner. It 155 # assumes that a file with one of these extensions does not contain 156 # resources. If a file has the .c or .h file name extension, RC ignores all 157 # lines in the file except the preprocessor directives.""" 158 # Thankfully, the Microsoft headers are mostly good about putting everything 159 # in the system headers behind `if !defined(RC_INVOKED)`, so regular 160 # preprocessing with RC_INVOKED defined works. 161 return preprocessed_output 162 163 164def RunRc(preprocessed_output, is_utf8, flags): 165 if sys.platform.startswith('linux'): 166 rc = os.path.join(THIS_DIR, 'linux64', 'rc') 167 elif sys.platform == 'darwin': 168 rc = os.path.join(THIS_DIR, 'mac', 'rc') 169 elif sys.platform == 'win32': 170 rc = os.path.join(THIS_DIR, 'win', 'rc.exe') 171 else: 172 print('rc.py: error: unsupported platform', sys.platform, file=sys.stderr) 173 sys.exit(1) 174 rc_cmd = [rc] 175 # Make sure rc-relative resources can be found: 176 if os.path.dirname(flags.input): 177 rc_cmd.append('/cd' + os.path.dirname(flags.input)) 178 rc_cmd.append('/fo' + flags.output) 179 if is_utf8: 180 rc_cmd.append('/utf-8') 181 # TODO(thakis): cl currently always prints full paths for /showIncludes, 182 # but clang-cl /P doesn't. Which one is right? 183 if flags.show_includes: 184 rc_cmd.append('/showIncludes') 185 # Microsoft rc.exe searches for referenced files relative to -I flags in 186 # addition to the pwd, so -I flags need to be passed both to both 187 # the preprocessor and rc. 188 rc_cmd += flags.includes 189 p = subprocess.Popen(rc_cmd, stdin=subprocess.PIPE) 190 p.communicate(input=preprocessed_output) 191 192 if flags.show_includes and p.returncode == 0: 193 TOOL_DIR = os.path.dirname(os.path.relpath(THIS_DIR)).replace("\\", "/") 194 # Since tool("rc") can't have deps, add deps on this script and on rc.py 195 # and its deps here, so that rc edges become dirty if rc.py changes. 196 print('Note: including file: {}/tool_wrapper.py'.format(TOOL_DIR)) 197 print('Note: including file: {}/rc/rc.py'.format(TOOL_DIR)) 198 print( 199 'Note: including file: {}/rc/linux64/rc.sha1'.format(TOOL_DIR)) 200 print('Note: including file: {}/rc/mac/rc.sha1'.format(TOOL_DIR)) 201 print( 202 'Note: including file: {}/rc/win/rc.exe.sha1'.format(TOOL_DIR)) 203 204 return p.returncode 205 206 207def CompareToMsRcOutput(preprocessed_output, is_utf8, flags): 208 msrc_in = flags.output + '.preprocessed.rc' 209 210 # Strip preprocessor line markers. 211 preprocessed_output = re.sub(br'^#.*$', b'', preprocessed_output, flags=re.M) 212 if is_utf8: 213 preprocessed_output = preprocessed_output.decode('utf-8').encode('utf-16le') 214 with open(msrc_in, 'wb') as f: 215 f.write(preprocessed_output) 216 217 msrc_out = flags.output + '_ms_rc' 218 msrc_cmd = ['rc', '/nologo', '/x', '/fo' + msrc_out] 219 220 # Make sure rc-relative resources can be found. rc.exe looks for external 221 # resource files next to the file, but the preprocessed file isn't where the 222 # input was. 223 # Note that rc searches external resource files in the order of 224 # 1. next to the input file 225 # 2. relative to cwd 226 # 3. next to -I directories 227 # Changing the cwd means we'd have to rewrite all -I flags, so just add 228 # the input file dir as -I flag. That technically gets the order of 1 and 2 229 # wrong, but in Chromium's build the cwd is the gn out dir, and generated 230 # files there are in obj/ and gen/, so this difference doesn't matter in 231 # practice. 232 if os.path.dirname(flags.input): 233 msrc_cmd += [ '-I' + os.path.dirname(flags.input) ] 234 235 # Microsoft rc.exe searches for referenced files relative to -I flags in 236 # addition to the pwd, so -I flags need to be passed both to both 237 # the preprocessor and rc. 238 msrc_cmd += flags.includes 239 240 # Input must come last. 241 msrc_cmd += [ msrc_in ] 242 243 rc_exe_exit_code = subprocess.call(msrc_cmd) 244 # Assert Microsoft rc.exe and rc.py produced identical .res files. 245 if rc_exe_exit_code == 0: 246 import filecmp 247 assert filecmp.cmp(msrc_out, flags.output) 248 return rc_exe_exit_code 249 250 251def main(): 252 # This driver has to do these things: 253 # 1. Parse flags. 254 # 2. Convert the input from UTF-16LE to UTF-8 if needed. 255 # 3. Pass the input through a preprocessor (and clean up the preprocessor's 256 # output in minor ways). 257 # 4. Call rc for the heavy lifting. 258 flags = ParseFlags() 259 rc_file_data, is_utf8 = ReadInput(flags.input) 260 preprocessed_output = Preprocess(rc_file_data, flags) 261 rc_exe_exit_code = RunRc(preprocessed_output, is_utf8, flags) 262 263 # 5. On Windows, we also call Microsoft's rc.exe and check that we produced 264 # the same output. 265 # Since Microsoft's rc has a preprocessor that only accepts 32 characters 266 # for macro names, feed the clang-preprocessed source into it instead 267 # of using ms rc's preprocessor. 268 if sys.platform == 'win32' and rc_exe_exit_code == 0: 269 rc_exe_exit_code = CompareToMsRcOutput(preprocessed_output, is_utf8, flags) 270 271 return rc_exe_exit_code 272 273 274if __name__ == '__main__': 275 sys.exit(main()) 276