• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2012 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Utility functions for Windows builds.
6
7This file is copied to the build directory as part of toolchain setup and
8is used to set up calls to tools used by the build that need wrappers.
9"""
10
11
12import os
13import re
14import shutil
15import subprocess
16import stat
17import sys
18
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  exit_code = WinTool().Dispatch(args)
28  if exit_code is not None:
29    sys.exit(exit_code)
30
31
32class WinTool(object):
33  """This class performs all the Windows tooling steps. The methods can either
34  be executed directly, or dispatched from an argument list."""
35
36  def _UseSeparateMspdbsrv(self, env, args):
37    """Allows to use a unique instance of mspdbsrv.exe per linker instead of a
38    shared one."""
39    if len(args) < 1:
40      raise Exception("Not enough arguments")
41
42    if args[0] != 'link.exe':
43      return
44
45    # Use the output filename passed to the linker to generate an endpoint name
46    # for mspdbsrv.exe.
47    endpoint_name = None
48    for arg in args:
49      m = _LINK_EXE_OUT_ARG.match(arg)
50      if m:
51        endpoint_name = re.sub(r'\W+', '',
52            '%s_%d' % (m.group('out'), os.getpid()))
53        break
54
55    if endpoint_name is None:
56      return
57
58    # Adds the appropriate environment variable. This will be read by link.exe
59    # to know which instance of mspdbsrv.exe it should connect to (if it's
60    # not set then the default endpoint is used).
61    env['_MSPDBSRV_ENDPOINT_'] = endpoint_name
62
63  def Dispatch(self, args):
64    """Dispatches a string command to a method."""
65    if len(args) < 1:
66      raise Exception("Not enough arguments")
67
68    method = "Exec%s" % self._CommandifyName(args[0])
69    return getattr(self, method)(*args[1:])
70
71  def _CommandifyName(self, name_string):
72    """Transforms a tool name like recursive-mirror to RecursiveMirror."""
73    return name_string.title().replace('-', '')
74
75  def _GetEnv(self, arch):
76    """Gets the saved environment from a file for a given architecture."""
77    # The environment is saved as an "environment block" (see CreateProcess
78    # and msvs_emulation for details). We convert to a dict here.
79    # Drop last 2 NULs, one for list terminator, one for trailing vs. separator.
80    pairs = open(arch).read()[:-2].split('\0')
81    kvs = [item.split('=', 1) for item in pairs]
82    return dict(kvs)
83
84  def ExecDeleteFile(self, path):
85    """Simple file delete command."""
86    if os.path.exists(path):
87      os.unlink(path)
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, dummy_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      # Try to diagnose crbug.com/741603
111      if not os.path.exists(dest):
112        raise Exception("Copying of %s to %s failed" % (source, dest))
113
114  def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args):
115    """Filter diagnostic output from link that looks like:
116    '   Creating library ui.dll.lib and object ui.dll.exp'
117    This happens when there are exports from the dll or exe.
118    """
119    env = self._GetEnv(arch)
120    if use_separate_mspdbsrv == 'True':
121      self._UseSeparateMspdbsrv(env, args)
122    if sys.platform == 'win32':
123      args = list(args)  # *args is a tuple by default, which is read-only.
124      args[0] = args[0].replace('/', '\\')
125    # https://docs.python.org/2/library/subprocess.html:
126    # "On Unix with shell=True [...] if args is a sequence, the first item
127    # specifies the command string, and any additional items will be treated as
128    # additional arguments to the shell itself.  That is to say, Popen does the
129    # equivalent of:
130    #   Popen(['/bin/sh', '-c', args[0], args[1], ...])"
131    # For that reason, since going through the shell doesn't seem necessary on
132    # non-Windows don't do that there.
133    pe_name = None
134    for arg in args:
135      m = _LINK_EXE_OUT_ARG.match(arg)
136      if m:
137        pe_name = m.group('out')
138    link = subprocess.Popen(args, shell=sys.platform == 'win32', env=env,
139                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
140    # Read output one line at a time as it shows up to avoid OOM failures when
141    # GBs of output is produced.
142    for line in link.stdout:
143      line = line.decode('utf8')
144      if (not line.startswith('   Creating library ')
145          and not line.startswith('Generating code')
146          and not line.startswith('Finished generating code')):
147        print(line.rstrip())
148    return link.wait()
149
150  def ExecAsmWrapper(self, arch, *args):
151    """Filter logo banner from invocations of asm.exe."""
152    env = self._GetEnv(arch)
153    if sys.platform == 'win32':
154      # Windows ARM64 uses clang-cl as assembler which has '/' as path
155      # separator, convert it to '\\' when running on Windows.
156      args = list(args) # *args is a tuple by default, which is read-only
157      args[0] = args[0].replace('/', '\\')
158    # See comment in ExecLinkWrapper() for why shell=False on non-win.
159    popen = subprocess.Popen(args, shell=sys.platform == 'win32', env=env,
160                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
161    out, _ = popen.communicate()
162    for line in out.decode('utf8').splitlines():
163      if not line.startswith(' Assembling: '):
164        print(line)
165    return popen.returncode
166
167  def ExecRcWrapper(self, arch, *args):
168    """Converts .rc files to .res files."""
169    env = self._GetEnv(arch)
170    args = list(args)
171    rcpy_args = args[:]
172    rcpy_args[0:1] = [sys.executable, os.path.join(BASE_DIR, 'rc', 'rc.py')]
173    rcpy_args.append('/showIncludes')
174    return subprocess.call(rcpy_args, env=env)
175
176  def ExecActionWrapper(self, arch, rspfile, *dirname):
177    """Runs an action command line from a response file using the environment
178    for |arch|. If |dirname| is supplied, use that as the working directory."""
179    env = self._GetEnv(arch)
180    # TODO(scottmg): This is a temporary hack to get some specific variables
181    # through to actions that are set after GN-time. http://crbug.com/333738.
182    for k, v in os.environ.items():
183      if k not in env:
184        env[k] = v
185    args = open(rspfile).read()
186    dirname = dirname[0] if dirname else None
187    return subprocess.call(args, shell=True, env=env, cwd=dirname)
188
189
190if __name__ == '__main__':
191  sys.exit(main(sys.argv[1:]))
192