| # 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 <environment_file> [<args to mc.exe>*] |
| |
| from __future__ import print_function |
| |
| import difflib |
| import distutils.dir_util |
| 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)) |
| distutils.dir_util.copy_tree(source, header_dir, preserve_times=False) |
| |
| # 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() |