# Copyright 2015 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # Runs the Microsoft Message Compiler (mc.exe). # # Usage: message_compiler.py [*] import difflib import filecmp import os import re import shutil import subprocess import sys import tempfile def main(): env_file, rest = sys.argv[1], sys.argv[2:] # Parse some argument flags. header_dir = None resource_dir = None input_file = None for i, arg in enumerate(rest): if arg == '-h' and len(rest) > i + 1: assert header_dir == None header_dir = rest[i + 1] elif arg == '-r' and len(rest) > i + 1: assert resource_dir == None resource_dir = rest[i + 1] elif arg.endswith('.mc') or arg.endswith('.man'): assert input_file == None input_file = arg # Copy checked-in outputs to final location. THIS_DIR = os.path.abspath(os.path.dirname(__file__)) assert header_dir == resource_dir source = os.path.join(THIS_DIR, "..", "..", "third_party", "win_build_output", re.sub(r'^(?:[^/]+/)?gen/', 'mc/', header_dir)) # Set copy_function to shutil.copy to update the timestamp on the destination. shutil.copytree(source, header_dir, copy_function=shutil.copy, dirs_exist_ok=True) # On non-Windows, that's all we can do. if sys.platform != 'win32': return # On Windows, run mc.exe on the input and check that its outputs are # identical to the checked-in outputs. # Read the environment block from the file. This is stored in the format used # by CreateProcess. Drop last 2 NULs, one for list terminator, one for # trailing vs. separator. env_pairs = open(env_file).read()[:-2].split('\0') env_dict = dict([item.split('=', 1) for item in env_pairs]) extension = os.path.splitext(input_file)[1] if extension in ['.man', '.mc']: # For .man files, mc's output changed significantly from Version 10.0.15063 # to Version 10.0.16299. We should always have the output of the current # default SDK checked in and compare to that. Early out if a different SDK # is active. This also happens with .mc files. # TODO(thakis): Check in new baselines and compare to 16299 instead once # we use the 2017 Fall Creator's Update by default. mc_help = subprocess.check_output(['mc.exe', '/?'], env=env_dict, stderr=subprocess.STDOUT, shell=True) version = re.search(br'Message Compiler\s+Version (\S+)', mc_help).group(1) if version != '10.0.15063': return # mc writes to stderr, so this explicitly redirects to stdout and eats it. try: tmp_dir = tempfile.mkdtemp() delete_tmp_dir = True if header_dir: rest[rest.index('-h') + 1] = tmp_dir header_dir = tmp_dir if resource_dir: rest[rest.index('-r') + 1] = tmp_dir resource_dir = tmp_dir # This needs shell=True to search the path in env_dict for the mc # executable. subprocess.check_output(['mc.exe'] + rest, env=env_dict, stderr=subprocess.STDOUT, shell=True) # We require all source code (in particular, the header generated here) to # be UTF-8. jinja can output the intermediate .mc file in UTF-8 or UTF-16LE. # However, mc.exe only supports Unicode via the -u flag, and it assumes when # that is specified that the input is UTF-16LE (and errors out on UTF-8 # files, assuming they're ANSI). Even with -u specified and UTF16-LE input, # it generates an ANSI header, and includes broken versions of the message # text in the comment before the value. To work around this, for any invalid # // comment lines, we simply drop the line in the header after building it. # Also, mc.exe apparently doesn't always write #define lines in # deterministic order, so manually sort each block of #defines. if header_dir: header_file = os.path.join( header_dir, os.path.splitext(os.path.basename(input_file))[0] + '.h') header_contents = [] with open(header_file, 'rb') as f: define_block = [] # The current contiguous block of #defines. for line in f.readlines(): if line.startswith('//') and '?' in line: continue if line.startswith('#define '): define_block.append(line) continue # On the first non-#define line, emit the sorted preceding #define # block. header_contents += sorted(define_block, key=lambda s: s.split()[-1]) define_block = [] header_contents.append(line) # If the .h file ends with a #define block, flush the final block. header_contents += sorted(define_block, key=lambda s: s.split()[-1]) with open(header_file, 'wb') as f: f.write(''.join(header_contents)) # mc.exe invocation and post-processing are complete, now compare the output # in tmp_dir to the checked-in outputs. diff = filecmp.dircmp(tmp_dir, source) if diff.diff_files or set(diff.left_list) != set(diff.right_list): print('mc.exe output different from files in %s, see %s' % (source, tmp_dir)) diff.report() for f in diff.diff_files: if f.endswith('.bin'): continue fromfile = os.path.join(source, f) tofile = os.path.join(tmp_dir, f) print(''.join( difflib.unified_diff( open(fromfile, 'U').readlines(), open(tofile, 'U').readlines(), fromfile, tofile))) delete_tmp_dir = False sys.exit(1) except subprocess.CalledProcessError as e: print(e.output) sys.exit(e.returncode) finally: if os.path.exists(tmp_dir) and delete_tmp_dir: shutil.rmtree(tmp_dir) if __name__ == '__main__': main()