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