|
1
|
|
|
#!/usr/bin/env python |
|
2
|
|
|
|
|
3
|
|
|
# Copyright (c) 2012 Google Inc. All rights reserved. |
|
4
|
|
|
# Use of this source code is governed by a BSD-style license that can be |
|
5
|
|
|
# found in the LICENSE file. |
|
6
|
|
|
|
|
7
|
|
|
"""Utility functions for Windows builds. |
|
8
|
|
|
|
|
9
|
|
|
These functions are executed via gyp-win-tool when using the ninja generator. |
|
10
|
|
|
""" |
|
11
|
|
|
|
|
12
|
|
|
import os |
|
13
|
|
|
import re |
|
14
|
|
|
import shutil |
|
15
|
|
|
import subprocess |
|
16
|
|
|
import stat |
|
17
|
|
|
import string |
|
18
|
|
|
import sys |
|
19
|
|
|
|
|
20
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
21
|
|
|
|
|
22
|
|
|
# A regex matching an argument corresponding to the output filename passed to |
|
23
|
|
|
# link.exe. |
|
24
|
|
|
_LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE) |
|
25
|
|
|
|
|
26
|
|
|
def main(args): |
|
27
|
|
|
executor = WinTool() |
|
28
|
|
|
exit_code = executor.Dispatch(args) |
|
29
|
|
|
if exit_code is not None: |
|
30
|
|
|
sys.exit(exit_code) |
|
31
|
|
|
|
|
32
|
|
|
|
|
33
|
|
|
class WinTool(object): |
|
34
|
|
|
"""This class performs all the Windows tooling steps. The methods can either |
|
35
|
|
|
be executed directly, or dispatched from an argument list.""" |
|
36
|
|
|
|
|
37
|
|
|
def _UseSeparateMspdbsrv(self, env, args): |
|
38
|
|
|
"""Allows to use a unique instance of mspdbsrv.exe per linker instead of a |
|
39
|
|
|
shared one.""" |
|
40
|
|
|
if len(args) < 1: |
|
41
|
|
|
raise Exception("Not enough arguments") |
|
42
|
|
|
|
|
43
|
|
|
if args[0] != 'link.exe': |
|
44
|
|
|
return |
|
45
|
|
|
|
|
46
|
|
|
# Use the output filename passed to the linker to generate an endpoint name |
|
47
|
|
|
# for mspdbsrv.exe. |
|
48
|
|
|
endpoint_name = None |
|
49
|
|
|
for arg in args: |
|
50
|
|
|
m = _LINK_EXE_OUT_ARG.match(arg) |
|
51
|
|
|
if m: |
|
52
|
|
|
endpoint_name = re.sub(r'\W+', '', |
|
53
|
|
|
'%s_%d' % (m.group('out'), os.getpid())) |
|
54
|
|
|
break |
|
55
|
|
|
|
|
56
|
|
|
if endpoint_name is None: |
|
57
|
|
|
return |
|
58
|
|
|
|
|
59
|
|
|
# Adds the appropriate environment variable. This will be read by link.exe |
|
60
|
|
|
# to know which instance of mspdbsrv.exe it should connect to (if it's |
|
61
|
|
|
# not set then the default endpoint is used). |
|
62
|
|
|
env['_MSPDBSRV_ENDPOINT_'] = endpoint_name |
|
63
|
|
|
|
|
64
|
|
|
def Dispatch(self, args): |
|
65
|
|
|
"""Dispatches a string command to a method.""" |
|
66
|
|
|
if len(args) < 1: |
|
67
|
|
|
raise Exception("Not enough arguments") |
|
68
|
|
|
|
|
69
|
|
|
method = "Exec%s" % self._CommandifyName(args[0]) |
|
70
|
|
|
return getattr(self, method)(*args[1:]) |
|
71
|
|
|
|
|
72
|
|
|
def _CommandifyName(self, name_string): |
|
73
|
|
|
"""Transforms a tool name like recursive-mirror to RecursiveMirror.""" |
|
74
|
|
|
return name_string.title().replace('-', '') |
|
75
|
|
|
|
|
76
|
|
|
def _GetEnv(self, arch): |
|
77
|
|
|
"""Gets the saved environment from a file for a given architecture.""" |
|
78
|
|
|
# The environment is saved as an "environment block" (see CreateProcess |
|
79
|
|
|
# and msvs_emulation for details). We convert to a dict here. |
|
80
|
|
|
# Drop last 2 NULs, one for list terminator, one for trailing vs. separator. |
|
81
|
|
|
pairs = open(arch).read()[:-2].split('\0') |
|
82
|
|
|
kvs = [item.split('=', 1) for item in pairs] |
|
83
|
|
|
return dict(kvs) |
|
84
|
|
|
|
|
85
|
|
|
def ExecStamp(self, path): |
|
86
|
|
|
"""Simple stamp command.""" |
|
87
|
|
|
open(path, 'w').close() |
|
88
|
|
|
|
|
89
|
|
|
def ExecRecursiveMirror(self, source, dest): |
|
90
|
|
|
"""Emulation of rm -rf out && cp -af in out.""" |
|
91
|
|
|
if os.path.exists(dest): |
|
92
|
|
|
if os.path.isdir(dest): |
|
93
|
|
|
def _on_error(fn, path, excinfo): |
|
94
|
|
|
# The operation failed, possibly because the file is set to |
|
95
|
|
|
# read-only. If that's why, make it writable and try the op again. |
|
96
|
|
|
if not os.access(path, os.W_OK): |
|
97
|
|
|
os.chmod(path, stat.S_IWRITE) |
|
98
|
|
|
fn(path) |
|
99
|
|
|
shutil.rmtree(dest, onerror=_on_error) |
|
100
|
|
|
else: |
|
101
|
|
|
if not os.access(dest, os.W_OK): |
|
102
|
|
|
# Attempt to make the file writable before deleting it. |
|
103
|
|
|
os.chmod(dest, stat.S_IWRITE) |
|
104
|
|
|
os.unlink(dest) |
|
105
|
|
|
|
|
106
|
|
|
if os.path.isdir(source): |
|
107
|
|
|
shutil.copytree(source, dest) |
|
108
|
|
|
else: |
|
109
|
|
|
shutil.copy2(source, dest) |
|
110
|
|
|
|
|
111
|
|
|
def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args): |
|
112
|
|
|
"""Filter diagnostic output from link that looks like: |
|
113
|
|
|
' Creating library ui.dll.lib and object ui.dll.exp' |
|
114
|
|
|
This happens when there are exports from the dll or exe. |
|
115
|
|
|
""" |
|
116
|
|
|
env = self._GetEnv(arch) |
|
117
|
|
|
if use_separate_mspdbsrv == 'True': |
|
118
|
|
|
self._UseSeparateMspdbsrv(env, args) |
|
119
|
|
|
link = subprocess.Popen([args[0].replace('/', '\\')] + list(args[1:]), |
|
120
|
|
|
shell=True, |
|
121
|
|
|
env=env, |
|
122
|
|
|
stdout=subprocess.PIPE, |
|
123
|
|
|
stderr=subprocess.STDOUT) |
|
124
|
|
|
out, _ = link.communicate() |
|
125
|
|
|
for line in out.splitlines(): |
|
126
|
|
|
if (not line.startswith(' Creating library ') and |
|
127
|
|
|
not line.startswith('Generating code') and |
|
128
|
|
|
not line.startswith('Finished generating code')): |
|
129
|
|
|
print line |
|
130
|
|
|
return link.returncode |
|
131
|
|
|
|
|
132
|
|
|
def ExecLinkWithManifests(self, arch, embed_manifest, out, ldcmd, resname, |
|
133
|
|
|
mt, rc, intermediate_manifest, *manifests): |
|
134
|
|
|
"""A wrapper for handling creating a manifest resource and then executing |
|
135
|
|
|
a link command.""" |
|
136
|
|
|
# The 'normal' way to do manifests is to have link generate a manifest |
|
137
|
|
|
# based on gathering dependencies from the object files, then merge that |
|
138
|
|
|
# manifest with other manifests supplied as sources, convert the merged |
|
139
|
|
|
# manifest to a resource, and then *relink*, including the compiled |
|
140
|
|
|
# version of the manifest resource. This breaks incremental linking, and |
|
141
|
|
|
# is generally overly complicated. Instead, we merge all the manifests |
|
142
|
|
|
# provided (along with one that includes what would normally be in the |
|
143
|
|
|
# linker-generated one, see msvs_emulation.py), and include that into the |
|
144
|
|
|
# first and only link. We still tell link to generate a manifest, but we |
|
145
|
|
|
# only use that to assert that our simpler process did not miss anything. |
|
146
|
|
|
variables = { |
|
147
|
|
|
'python': sys.executable, |
|
148
|
|
|
'arch': arch, |
|
149
|
|
|
'out': out, |
|
150
|
|
|
'ldcmd': ldcmd, |
|
151
|
|
|
'resname': resname, |
|
152
|
|
|
'mt': mt, |
|
153
|
|
|
'rc': rc, |
|
154
|
|
|
'intermediate_manifest': intermediate_manifest, |
|
155
|
|
|
'manifests': ' '.join(manifests), |
|
156
|
|
|
} |
|
157
|
|
|
add_to_ld = '' |
|
158
|
|
|
if manifests: |
|
159
|
|
|
subprocess.check_call( |
|
160
|
|
|
'%(python)s gyp-win-tool manifest-wrapper %(arch)s %(mt)s -nologo ' |
|
161
|
|
|
'-manifest %(manifests)s -out:%(out)s.manifest' % variables) |
|
162
|
|
|
if embed_manifest == 'True': |
|
163
|
|
|
subprocess.check_call( |
|
164
|
|
|
'%(python)s gyp-win-tool manifest-to-rc %(arch)s %(out)s.manifest' |
|
165
|
|
|
' %(out)s.manifest.rc %(resname)s' % variables) |
|
166
|
|
|
subprocess.check_call( |
|
167
|
|
|
'%(python)s gyp-win-tool rc-wrapper %(arch)s %(rc)s ' |
|
168
|
|
|
'%(out)s.manifest.rc' % variables) |
|
169
|
|
|
add_to_ld = ' %(out)s.manifest.res' % variables |
|
170
|
|
|
subprocess.check_call(ldcmd + add_to_ld) |
|
171
|
|
|
|
|
172
|
|
|
# Run mt.exe on the theoretically complete manifest we generated, merging |
|
173
|
|
|
# it with the one the linker generated to confirm that the linker |
|
174
|
|
|
# generated one does not add anything. This is strictly unnecessary for |
|
175
|
|
|
# correctness, it's only to verify that e.g. /MANIFESTDEPENDENCY was not |
|
176
|
|
|
# used in a #pragma comment. |
|
177
|
|
|
if manifests: |
|
178
|
|
|
# Merge the intermediate one with ours to .assert.manifest, then check |
|
179
|
|
|
# that .assert.manifest is identical to ours. |
|
180
|
|
|
subprocess.check_call( |
|
181
|
|
|
'%(python)s gyp-win-tool manifest-wrapper %(arch)s %(mt)s -nologo ' |
|
182
|
|
|
'-manifest %(out)s.manifest %(intermediate_manifest)s ' |
|
183
|
|
|
'-out:%(out)s.assert.manifest' % variables) |
|
184
|
|
|
assert_manifest = '%(out)s.assert.manifest' % variables |
|
185
|
|
|
our_manifest = '%(out)s.manifest' % variables |
|
186
|
|
|
# Load and normalize the manifests. mt.exe sometimes removes whitespace, |
|
187
|
|
|
# and sometimes doesn't unfortunately. |
|
188
|
|
|
with open(our_manifest, 'rb') as our_f: |
|
189
|
|
|
with open(assert_manifest, 'rb') as assert_f: |
|
190
|
|
|
our_data = our_f.read().translate(None, string.whitespace) |
|
191
|
|
|
assert_data = assert_f.read().translate(None, string.whitespace) |
|
192
|
|
|
if our_data != assert_data: |
|
193
|
|
|
os.unlink(out) |
|
194
|
|
|
def dump(filename): |
|
195
|
|
|
sys.stderr.write('%s\n-----\n' % filename) |
|
196
|
|
|
with open(filename, 'rb') as f: |
|
197
|
|
|
sys.stderr.write(f.read() + '\n-----\n') |
|
198
|
|
|
dump(intermediate_manifest) |
|
199
|
|
|
dump(our_manifest) |
|
200
|
|
|
dump(assert_manifest) |
|
201
|
|
|
sys.stderr.write( |
|
202
|
|
|
'Linker generated manifest "%s" added to final manifest "%s" ' |
|
203
|
|
|
'(result in "%s"). ' |
|
204
|
|
|
'Were /MANIFEST switches used in #pragma statements? ' % ( |
|
205
|
|
|
intermediate_manifest, our_manifest, assert_manifest)) |
|
206
|
|
|
return 1 |
|
207
|
|
|
|
|
208
|
|
|
def ExecManifestWrapper(self, arch, *args): |
|
209
|
|
|
"""Run manifest tool with environment set. Strip out undesirable warning |
|
210
|
|
|
(some XML blocks are recognized by the OS loader, but not the manifest |
|
211
|
|
|
tool).""" |
|
212
|
|
|
env = self._GetEnv(arch) |
|
213
|
|
|
popen = subprocess.Popen(args, shell=True, env=env, |
|
214
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
|
215
|
|
|
out, _ = popen.communicate() |
|
216
|
|
|
for line in out.splitlines(): |
|
217
|
|
|
if line and 'manifest authoring warning 81010002' not in line: |
|
218
|
|
|
print line |
|
219
|
|
|
return popen.returncode |
|
220
|
|
|
|
|
221
|
|
|
def ExecManifestToRc(self, arch, *args): |
|
222
|
|
|
"""Creates a resource file pointing a SxS assembly manifest. |
|
223
|
|
|
|args| is tuple containing path to resource file, path to manifest file |
|
224
|
|
|
and resource name which can be "1" (for executables) or "2" (for DLLs).""" |
|
225
|
|
|
manifest_path, resource_path, resource_name = args |
|
226
|
|
|
with open(resource_path, 'wb') as output: |
|
227
|
|
|
output.write('#include <windows.h>\n%s RT_MANIFEST "%s"' % ( |
|
228
|
|
|
resource_name, |
|
229
|
|
|
os.path.abspath(manifest_path).replace('\\', '/'))) |
|
230
|
|
|
|
|
231
|
|
|
def ExecMidlWrapper(self, arch, outdir, tlb, h, dlldata, iid, proxy, idl, |
|
232
|
|
|
*flags): |
|
233
|
|
|
"""Filter noisy filenames output from MIDL compile step that isn't |
|
234
|
|
|
quietable via command line flags. |
|
235
|
|
|
""" |
|
236
|
|
|
args = ['midl', '/nologo'] + list(flags) + [ |
|
237
|
|
|
'/out', outdir, |
|
238
|
|
|
'/tlb', tlb, |
|
239
|
|
|
'/h', h, |
|
240
|
|
|
'/dlldata', dlldata, |
|
241
|
|
|
'/iid', iid, |
|
242
|
|
|
'/proxy', proxy, |
|
243
|
|
|
idl] |
|
244
|
|
|
env = self._GetEnv(arch) |
|
245
|
|
|
popen = subprocess.Popen(args, shell=True, env=env, |
|
246
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
|
247
|
|
|
out, _ = popen.communicate() |
|
248
|
|
|
# Filter junk out of stdout, and write filtered versions. Output we want |
|
249
|
|
|
# to filter is pairs of lines that look like this: |
|
250
|
|
|
# Processing C:\Program Files (x86)\Microsoft SDKs\...\include\objidl.idl |
|
251
|
|
|
# objidl.idl |
|
252
|
|
|
lines = out.splitlines() |
|
253
|
|
|
prefixes = ('Processing ', '64 bit Processing ') |
|
254
|
|
|
processing = set(os.path.basename(x) |
|
255
|
|
|
for x in lines if x.startswith(prefixes)) |
|
256
|
|
|
for line in lines: |
|
257
|
|
|
if not line.startswith(prefixes) and line not in processing: |
|
258
|
|
|
print line |
|
259
|
|
|
return popen.returncode |
|
260
|
|
|
|
|
261
|
|
|
def ExecAsmWrapper(self, arch, *args): |
|
262
|
|
|
"""Filter logo banner from invocations of asm.exe.""" |
|
263
|
|
|
env = self._GetEnv(arch) |
|
264
|
|
|
popen = subprocess.Popen(args, shell=True, env=env, |
|
265
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
|
266
|
|
|
out, _ = popen.communicate() |
|
267
|
|
|
for line in out.splitlines(): |
|
268
|
|
|
if (not line.startswith('Copyright (C) Microsoft Corporation') and |
|
269
|
|
|
not line.startswith('Microsoft (R) Macro Assembler') and |
|
270
|
|
|
not line.startswith(' Assembling: ') and |
|
271
|
|
|
line): |
|
272
|
|
|
print line |
|
273
|
|
|
return popen.returncode |
|
274
|
|
|
|
|
275
|
|
|
def ExecRcWrapper(self, arch, *args): |
|
276
|
|
|
"""Filter logo banner from invocations of rc.exe. Older versions of RC |
|
277
|
|
|
don't support the /nologo flag.""" |
|
278
|
|
|
env = self._GetEnv(arch) |
|
279
|
|
|
popen = subprocess.Popen(args, shell=True, env=env, |
|
280
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
|
281
|
|
|
out, _ = popen.communicate() |
|
282
|
|
|
for line in out.splitlines(): |
|
283
|
|
|
if (not line.startswith('Microsoft (R) Windows (R) Resource Compiler') and |
|
284
|
|
|
not line.startswith('Copyright (C) Microsoft Corporation') and |
|
285
|
|
|
line): |
|
286
|
|
|
print line |
|
287
|
|
|
return popen.returncode |
|
288
|
|
|
|
|
289
|
|
|
def ExecActionWrapper(self, arch, rspfile, *dir): |
|
290
|
|
|
"""Runs an action command line from a response file using the environment |
|
291
|
|
|
for |arch|. If |dir| is supplied, use that as the working directory.""" |
|
292
|
|
|
env = self._GetEnv(arch) |
|
293
|
|
|
# TODO(scottmg): This is a temporary hack to get some specific variables |
|
294
|
|
|
# through to actions that are set after gyp-time. http://crbug.com/333738. |
|
295
|
|
|
for k, v in os.environ.iteritems(): |
|
296
|
|
|
if k not in env: |
|
297
|
|
|
env[k] = v |
|
298
|
|
|
args = open(rspfile).read() |
|
299
|
|
|
dir = dir[0] if dir else None |
|
300
|
|
|
return subprocess.call(args, shell=True, env=env, cwd=dir) |
|
301
|
|
|
|
|
302
|
|
|
def ExecClCompile(self, project_dir, selected_files): |
|
303
|
|
|
"""Executed by msvs-ninja projects when the 'ClCompile' target is used to |
|
304
|
|
|
build selected C/C++ files.""" |
|
305
|
|
|
project_dir = os.path.relpath(project_dir, BASE_DIR) |
|
306
|
|
|
selected_files = selected_files.split(';') |
|
307
|
|
|
ninja_targets = [os.path.join(project_dir, filename) + '^^' |
|
308
|
|
|
for filename in selected_files] |
|
309
|
|
|
cmd = ['ninja.exe'] |
|
310
|
|
|
cmd.extend(ninja_targets) |
|
311
|
|
|
return subprocess.call(cmd, shell=True, cwd=BASE_DIR) |
|
312
|
|
|
|
|
313
|
|
|
if __name__ == '__main__': |
|
314
|
|
|
sys.exit(main(sys.argv[1:])) |
|
315
|
|
|
|