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