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, 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 314 if __name__ == '__main__': 315 sys.exit(main(sys.argv[1:])) 316