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
9These functions are executed via gyp-win-tool when using the ninja generator.
10"""
11
12import os
13import re
14import shutil
15import subprocess
16import stat
17import string
18import sys
19
20BASE_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
26def 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
33class 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,
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 '):
127        print line
128    return link.returncode
129
130  def ExecLinkWithManifests(self, arch, embed_manifest, out, ldcmd, resname,
131                            mt, rc, intermediate_manifest, *manifests):
132    """A wrapper for handling creating a manifest resource and then executing
133    a link command."""
134    # The 'normal' way to do manifests is to have link generate a manifest
135    # based on gathering dependencies from the object files, then merge that
136    # manifest with other manifests supplied as sources, convert the merged
137    # manifest to a resource, and then *relink*, including the compiled
138    # version of the manifest resource. This breaks incremental linking, and
139    # is generally overly complicated. Instead, we merge all the manifests
140    # provided (along with one that includes what would normally be in the
141    # linker-generated one, see msvs_emulation.py), and include that into the
142    # first and only link. We still tell link to generate a manifest, but we
143    # only use that to assert that our simpler process did not miss anything.
144    variables = {
145      'python': sys.executable,
146      'arch': arch,
147      'out': out,
148      'ldcmd': ldcmd,
149      'resname': resname,
150      'mt': mt,
151      'rc': rc,
152      'intermediate_manifest': intermediate_manifest,
153      'manifests': ' '.join(manifests),
154    }
155    add_to_ld = ''
156    if manifests:
157      subprocess.check_call(
158          '%(python)s gyp-win-tool manifest-wrapper %(arch)s %(mt)s -nologo '
159          '-manifest %(manifests)s -out:%(out)s.manifest' % variables)
160      if embed_manifest == 'True':
161        subprocess.check_call(
162            '%(python)s gyp-win-tool manifest-to-rc %(arch)s %(out)s.manifest'
163          ' %(out)s.manifest.rc %(resname)s' % variables)
164        subprocess.check_call(
165            '%(python)s gyp-win-tool rc-wrapper %(arch)s %(rc)s '
166            '%(out)s.manifest.rc' % variables)
167        add_to_ld = ' %(out)s.manifest.res' % variables
168    subprocess.check_call(ldcmd + add_to_ld)
169
170    # Run mt.exe on the theoretically complete manifest we generated, merging
171    # it with the one the linker generated to confirm that the linker
172    # generated one does not add anything. This is strictly unnecessary for
173    # correctness, it's only to verify that e.g. /MANIFESTDEPENDENCY was not
174    # used in a #pragma comment.
175    if manifests:
176      # Merge the intermediate one with ours to .assert.manifest, then check
177      # that .assert.manifest is identical to ours.
178      subprocess.check_call(
179          '%(python)s gyp-win-tool manifest-wrapper %(arch)s %(mt)s -nologo '
180          '-manifest %(out)s.manifest %(intermediate_manifest)s '
181          '-out:%(out)s.assert.manifest' % variables)
182      assert_manifest = '%(out)s.assert.manifest' % variables
183      our_manifest = '%(out)s.manifest' % variables
184      # Load and normalize the manifests. mt.exe sometimes removes whitespace,
185      # and sometimes doesn't unfortunately.
186      with open(our_manifest, 'rb') as our_f:
187        with open(assert_manifest, 'rb') as assert_f:
188          our_data = our_f.read().translate(None, string.whitespace)
189          assert_data = assert_f.read().translate(None, string.whitespace)
190      if our_data != assert_data:
191        os.unlink(out)
192        def dump(filename):
193          sys.stderr.write('%s\n-----\n' % filename)
194          with open(filename, 'rb') as f:
195            sys.stderr.write(f.read() + '\n-----\n')
196        dump(intermediate_manifest)
197        dump(our_manifest)
198        dump(assert_manifest)
199        sys.stderr.write(
200            'Linker generated manifest "%s" added to final manifest "%s" '
201            '(result in "%s"). '
202            'Were /MANIFEST switches used in #pragma statements? ' % (
203              intermediate_manifest, our_manifest, assert_manifest))
204        return 1
205
206  def ExecManifestWrapper(self, arch, *args):
207    """Run manifest tool with environment set. Strip out undesirable warning
208    (some XML blocks are recognized by the OS loader, but not the manifest
209    tool)."""
210    env = self._GetEnv(arch)
211    popen = subprocess.Popen(args, shell=True, env=env,
212                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
213    out, _ = popen.communicate()
214    for line in out.splitlines():
215      if line and 'manifest authoring warning 81010002' not in line:
216        print line
217    return popen.returncode
218
219  def ExecManifestToRc(self, arch, *args):
220    """Creates a resource file pointing a SxS assembly manifest.
221    |args| is tuple containing path to resource file, path to manifest file
222    and resource name which can be "1" (for executables) or "2" (for DLLs)."""
223    manifest_path, resource_path, resource_name = args
224    with open(resource_path, 'wb') as output:
225      output.write('#include <windows.h>\n%s RT_MANIFEST "%s"' % (
226        resource_name,
227        os.path.abspath(manifest_path).replace('\\', '/')))
228
229  def ExecMidlWrapper(self, arch, outdir, tlb, h, dlldata, iid, proxy, idl,
230                      *flags):
231    """Filter noisy filenames output from MIDL compile step that isn't
232    quietable via command line flags.
233    """
234    args = ['midl', '/nologo'] + list(flags) + [
235        '/out', outdir,
236        '/tlb', tlb,
237        '/h', h,
238        '/dlldata', dlldata,
239        '/iid', iid,
240        '/proxy', proxy,
241        idl]
242    env = self._GetEnv(arch)
243    popen = subprocess.Popen(args, shell=True, env=env,
244                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
245    out, _ = popen.communicate()
246    # Filter junk out of stdout, and write filtered versions. Output we want
247    # to filter is pairs of lines that look like this:
248    # Processing C:\Program Files (x86)\Microsoft SDKs\...\include\objidl.idl
249    # objidl.idl
250    lines = out.splitlines()
251    prefixes = ('Processing ', '64 bit Processing ')
252    processing = set(os.path.basename(x)
253                     for x in lines if x.startswith(prefixes))
254    for line in lines:
255      if not line.startswith(prefixes) and line not in processing:
256        print line
257    return popen.returncode
258
259  def ExecAsmWrapper(self, arch, *args):
260    """Filter logo banner from invocations of asm.exe."""
261    env = self._GetEnv(arch)
262    # MSVS doesn't assemble x64 asm files.
263    if arch == 'environment.x64':
264      return 0
265    popen = subprocess.Popen(args, shell=True, env=env,
266                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
267    out, _ = popen.communicate()
268    for line in out.splitlines():
269      if (not line.startswith('Copyright (C) Microsoft Corporation') and
270          not line.startswith('Microsoft (R) Macro Assembler') and
271          not line.startswith(' Assembling: ') and
272          line):
273        print line
274    return popen.returncode
275
276  def ExecRcWrapper(self, arch, *args):
277    """Filter logo banner from invocations of rc.exe. Older versions of RC
278    don't support the /nologo flag."""
279    env = self._GetEnv(arch)
280    popen = subprocess.Popen(args, shell=True, env=env,
281                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
282    out, _ = popen.communicate()
283    for line in out.splitlines():
284      if (not line.startswith('Microsoft (R) Windows (R) Resource Compiler') and
285          not line.startswith('Copyright (C) Microsoft Corporation') and
286          line):
287        print line
288    return popen.returncode
289
290  def ExecActionWrapper(self, arch, rspfile, *dir):
291    """Runs an action command line from a response file using the environment
292    for |arch|. If |dir| is supplied, use that as the working directory."""
293    env = self._GetEnv(arch)
294    # TODO(scottmg): This is a temporary hack to get some specific variables
295    # through to actions that are set after gyp-time. http://crbug.com/333738.
296    for k, v in os.environ.iteritems():
297      if k not in env:
298        env[k] = v
299    args = open(rspfile).read()
300    dir = dir[0] if dir else None
301    return subprocess.call(args, shell=True, env=env, cwd=dir)
302
303  def ExecClCompile(self, project_dir, selected_files):
304    """Executed by msvs-ninja projects when the 'ClCompile' target is used to
305    build selected C/C++ files."""
306    project_dir = os.path.relpath(project_dir, BASE_DIR)
307    selected_files = selected_files.split(';')
308    ninja_targets = [os.path.join(project_dir, filename) + '^^'
309        for filename in selected_files]
310    cmd = ['ninja.exe']
311    cmd.extend(ninja_targets)
312    return subprocess.call(cmd, shell=True, cwd=BASE_DIR)
313
314if __name__ == '__main__':
315  sys.exit(main(sys.argv[1:]))
316