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