message_compiler.py 5.8 KB

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