1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
build / win / message_compiler.py [blame]
# 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>*]
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 != b'10.0.22621':
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(b'//') and b'?' in line:
continue
if line.startswith(b'#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(b''.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).readlines(),
open(tofile).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()