1# Copyright 2015 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5# Runs the Microsoft Message Compiler (mc.exe). 6# 7# Usage: message_compiler.py <environment_file> [<args to mc.exe>*] 8 9 10import difflib 11import distutils.dir_util 12import filecmp 13import os 14import re 15import shutil 16import subprocess 17import sys 18import tempfile 19 20def main(): 21 env_file, rest = sys.argv[1], sys.argv[2:] 22 23 # Parse some argument flags. 24 header_dir = None 25 resource_dir = None 26 input_file = None 27 for i, arg in enumerate(rest): 28 if arg == '-h' and len(rest) > i + 1: 29 assert header_dir == None 30 header_dir = rest[i + 1] 31 elif arg == '-r' and len(rest) > i + 1: 32 assert resource_dir == None 33 resource_dir = rest[i + 1] 34 elif arg.endswith('.mc') or arg.endswith('.man'): 35 assert input_file == None 36 input_file = arg 37 38 # Copy checked-in outputs to final location. 39 THIS_DIR = os.path.abspath(os.path.dirname(__file__)) 40 assert header_dir == resource_dir 41 source = os.path.join(THIS_DIR, "..", "..", 42 "third_party", "win_build_output", 43 re.sub(r'^(?:[^/]+/)?gen/', 'mc/', header_dir)) 44 distutils.dir_util.copy_tree(source, header_dir, preserve_times=False) 45 46 # On non-Windows, that's all we can do. 47 if sys.platform != 'win32': 48 return 49 50 # On Windows, run mc.exe on the input and check that its outputs are 51 # identical to the checked-in outputs. 52 53 # Read the environment block from the file. This is stored in the format used 54 # by CreateProcess. Drop last 2 NULs, one for list terminator, one for 55 # trailing vs. separator. 56 env_pairs = open(env_file).read()[:-2].split('\0') 57 env_dict = dict([item.split('=', 1) for item in env_pairs]) 58 59 extension = os.path.splitext(input_file)[1] 60 if extension in ['.man', '.mc']: 61 # For .man files, mc's output changed significantly from Version 10.0.15063 62 # to Version 10.0.16299. We should always have the output of the current 63 # default SDK checked in and compare to that. Early out if a different SDK 64 # is active. This also happens with .mc files. 65 # TODO(thakis): Check in new baselines and compare to 16299 instead once 66 # we use the 2017 Fall Creator's Update by default. 67 mc_help = subprocess.check_output(['mc.exe', '/?'], env=env_dict, 68 stderr=subprocess.STDOUT, shell=True) 69 version = re.search(br'Message Compiler\s+Version (\S+)', mc_help).group(1) 70 if version != '10.0.15063': 71 return 72 73 # mc writes to stderr, so this explicitly redirects to stdout and eats it. 74 try: 75 tmp_dir = tempfile.mkdtemp() 76 delete_tmp_dir = True 77 if header_dir: 78 rest[rest.index('-h') + 1] = tmp_dir 79 header_dir = tmp_dir 80 if resource_dir: 81 rest[rest.index('-r') + 1] = tmp_dir 82 resource_dir = tmp_dir 83 84 # This needs shell=True to search the path in env_dict for the mc 85 # executable. 86 subprocess.check_output(['mc.exe'] + rest, 87 env=env_dict, 88 stderr=subprocess.STDOUT, 89 shell=True) 90 # We require all source code (in particular, the header generated here) to 91 # be UTF-8. jinja can output the intermediate .mc file in UTF-8 or UTF-16LE. 92 # However, mc.exe only supports Unicode via the -u flag, and it assumes when 93 # that is specified that the input is UTF-16LE (and errors out on UTF-8 94 # files, assuming they're ANSI). Even with -u specified and UTF16-LE input, 95 # it generates an ANSI header, and includes broken versions of the message 96 # text in the comment before the value. To work around this, for any invalid 97 # // comment lines, we simply drop the line in the header after building it. 98 # Also, mc.exe apparently doesn't always write #define lines in 99 # deterministic order, so manually sort each block of #defines. 100 if header_dir: 101 header_file = os.path.join( 102 header_dir, os.path.splitext(os.path.basename(input_file))[0] + '.h') 103 header_contents = [] 104 with open(header_file, 'rb') as f: 105 define_block = [] # The current contiguous block of #defines. 106 for line in f.readlines(): 107 if line.startswith('//') and '?' in line: 108 continue 109 if line.startswith('#define '): 110 define_block.append(line) 111 continue 112 # On the first non-#define line, emit the sorted preceding #define 113 # block. 114 header_contents += sorted(define_block, key=lambda s: s.split()[-1]) 115 define_block = [] 116 header_contents.append(line) 117 # If the .h file ends with a #define block, flush the final block. 118 header_contents += sorted(define_block, key=lambda s: s.split()[-1]) 119 with open(header_file, 'wb') as f: 120 f.write(''.join(header_contents)) 121 122 # mc.exe invocation and post-processing are complete, now compare the output 123 # in tmp_dir to the checked-in outputs. 124 diff = filecmp.dircmp(tmp_dir, source) 125 if diff.diff_files or set(diff.left_list) != set(diff.right_list): 126 print('mc.exe output different from files in %s, see %s' % (source, 127 tmp_dir)) 128 diff.report() 129 for f in diff.diff_files: 130 if f.endswith('.bin'): continue 131 fromfile = os.path.join(source, f) 132 tofile = os.path.join(tmp_dir, f) 133 print(''.join( 134 difflib.unified_diff( 135 open(fromfile, 'U').readlines(), 136 open(tofile, 'U').readlines(), fromfile, tofile))) 137 delete_tmp_dir = False 138 sys.exit(1) 139 except subprocess.CalledProcessError as e: 140 print(e.output) 141 sys.exit(e.returncode) 142 finally: 143 if os.path.exists(tmp_dir) and delete_tmp_dir: 144 shutil.rmtree(tmp_dir) 145 146if __name__ == '__main__': 147 main() 148