|  | # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Utility functions for Windows builds. | 
|  |  | 
|  | This file is copied to the build directory as part of toolchain setup and | 
|  | is used to set up calls to tools used by the build that need wrappers. | 
|  | """ | 
|  |  | 
|  | import os | 
|  | import re | 
|  | import shutil | 
|  | import subprocess | 
|  | import stat | 
|  | import string | 
|  | import sys | 
|  |  | 
|  | # tool_wrapper.py doesn't get invoked through python.bat so the Python bin | 
|  | # directory doesn't get added to the path. The Python module search logic | 
|  | # handles this fine and finds win32file.pyd. However the Windows module | 
|  | # search logic then looks for pywintypes27.dll and other DLLs in the path and | 
|  | # if it finds versions with a different bitness first then win32file.pyd will | 
|  | # fail to load with a cryptic error: | 
|  | #     ImportError: DLL load failed: %1 is not a valid Win32 application. | 
|  | if sys.platform == 'win32': | 
|  | os.environ['PATH'] = os.path.dirname(sys.executable) + \ | 
|  | os.pathsep + os.environ['PATH'] | 
|  | import win32file    # pylint: disable=import-error | 
|  |  | 
|  | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | 
|  |  | 
|  | # A regex matching an argument corresponding to the output filename passed to | 
|  | # link.exe. | 
|  | _LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE) | 
|  | _LINK_PDB_OUT_ARG = re.compile('/PDB:(?P<out>.+)$', re.IGNORECASE) | 
|  | _LINK_ERROR = re.compile('.* error LNK(\d+):') | 
|  |  | 
|  | # Retry links when this error is hit, to try to deal with crbug.com/782660 | 
|  | _LINKER_RETRY_ERRORS = 1201 | 
|  | # Maximum number of linker retries. | 
|  | _LINKER_RETRIES = 3 | 
|  |  | 
|  | def main(args): | 
|  | exit_code = WinTool().Dispatch(args) | 
|  | if exit_code is not None: | 
|  | sys.exit(exit_code) | 
|  |  | 
|  |  | 
|  | class WinTool(object): | 
|  | """This class performs all the Windows tooling steps. The methods can either | 
|  | be executed directly, or dispatched from an argument list.""" | 
|  |  | 
|  | def _UseSeparateMspdbsrv(self, env, args): | 
|  | """Allows to use a unique instance of mspdbsrv.exe per linker instead of a | 
|  | shared one.""" | 
|  | if len(args) < 1: | 
|  | raise Exception("Not enough arguments") | 
|  |  | 
|  | if args[0] != 'link.exe': | 
|  | return | 
|  |  | 
|  | # Use the output filename passed to the linker to generate an endpoint name | 
|  | # for mspdbsrv.exe. | 
|  | endpoint_name = None | 
|  | for arg in args: | 
|  | m = _LINK_EXE_OUT_ARG.match(arg) | 
|  | if m: | 
|  | endpoint_name = re.sub(r'\W+', '', | 
|  | '%s_%d' % (m.group('out'), os.getpid())) | 
|  | break | 
|  |  | 
|  | if endpoint_name is None: | 
|  | return | 
|  |  | 
|  | # Adds the appropriate environment variable. This will be read by link.exe | 
|  | # to know which instance of mspdbsrv.exe it should connect to (if it's | 
|  | # not set then the default endpoint is used). | 
|  | env['_MSPDBSRV_ENDPOINT_'] = endpoint_name | 
|  |  | 
|  | def Dispatch(self, args): | 
|  | """Dispatches a string command to a method.""" | 
|  | if len(args) < 1: | 
|  | raise Exception("Not enough arguments") | 
|  |  | 
|  | method = "Exec%s" % self._CommandifyName(args[0]) | 
|  | return getattr(self, method)(*args[1:]) | 
|  |  | 
|  | def _CommandifyName(self, name_string): | 
|  | """Transforms a tool name like recursive-mirror to RecursiveMirror.""" | 
|  | return name_string.title().replace('-', '') | 
|  |  | 
|  | def _GetEnv(self, arch): | 
|  | """Gets the saved environment from a file for a given architecture.""" | 
|  | # The environment is saved as an "environment block" (see CreateProcess | 
|  | # and msvs_emulation for details). We convert to a dict here. | 
|  | # Drop last 2 NULs, one for list terminator, one for trailing vs. separator. | 
|  | pairs = open(arch).read()[:-2].split('\0') | 
|  | kvs = [item.split('=', 1) for item in pairs] | 
|  | return dict(kvs) | 
|  |  | 
|  | def ExecDeleteFile(self, path): | 
|  | """Simple file delete command.""" | 
|  | if os.path.exists(path): | 
|  | os.unlink(path) | 
|  |  | 
|  | def ExecRecursiveMirror(self, source, dest): | 
|  | """Emulation of rm -rf out && cp -af in out.""" | 
|  | if os.path.exists(dest): | 
|  | if os.path.isdir(dest): | 
|  | def _on_error(fn, path, dummy_excinfo): | 
|  | # The operation failed, possibly because the file is set to | 
|  | # read-only. If that's why, make it writable and try the op again. | 
|  | if not os.access(path, os.W_OK): | 
|  | os.chmod(path, stat.S_IWRITE) | 
|  | fn(path) | 
|  | shutil.rmtree(dest, onerror=_on_error) | 
|  | else: | 
|  | if not os.access(dest, os.W_OK): | 
|  | # Attempt to make the file writable before deleting it. | 
|  | os.chmod(dest, stat.S_IWRITE) | 
|  | os.unlink(dest) | 
|  |  | 
|  | if os.path.isdir(source): | 
|  | shutil.copytree(source, dest) | 
|  | else: | 
|  | shutil.copy2(source, dest) | 
|  | # Try to diagnose crbug.com/741603 | 
|  | if not os.path.exists(dest): | 
|  | raise Exception("Copying of %s to %s failed" % (source, dest)) | 
|  |  | 
|  | def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args): | 
|  | """Filter diagnostic output from link that looks like: | 
|  | '   Creating library ui.dll.lib and object ui.dll.exp' | 
|  | This happens when there are exports from the dll or exe. | 
|  | """ | 
|  | env = self._GetEnv(arch) | 
|  | if use_separate_mspdbsrv == 'True': | 
|  | self._UseSeparateMspdbsrv(env, args) | 
|  | if sys.platform == 'win32': | 
|  | args = list(args)  # *args is a tuple by default, which is read-only. | 
|  | args[0] = args[0].replace('/', '\\') | 
|  | # https://docs.python.org/2/library/subprocess.html: | 
|  | # "On Unix with shell=True [...] if args is a sequence, the first item | 
|  | # specifies the command string, and any additional items will be treated as | 
|  | # additional arguments to the shell itself.  That is to say, Popen does the | 
|  | # equivalent of: | 
|  | #   Popen(['/bin/sh', '-c', args[0], args[1], ...])" | 
|  | # For that reason, since going through the shell doesn't seem necessary on | 
|  | # non-Windows don't do that there. | 
|  | pdb_name = None | 
|  | pe_name = None | 
|  | for arg in args: | 
|  | m = _LINK_PDB_OUT_ARG.match(arg) | 
|  | if m: | 
|  | pdb_name = m.group('out') | 
|  | m = _LINK_EXE_OUT_ARG.match(arg) | 
|  | if m: | 
|  | pe_name = m.group('out') | 
|  | for retry_count in range(_LINKER_RETRIES): | 
|  | retry = False | 
|  | link = subprocess.Popen(args, shell=sys.platform == 'win32', env=env, | 
|  | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 
|  | # Read output one line at a time as it shows up to avoid OOM failures when | 
|  | # GBs of output is produced. | 
|  | for line in link.stdout: | 
|  | if (not line.startswith('   Creating library ') and | 
|  | not line.startswith('Generating code') and | 
|  | not line.startswith('Finished generating code')): | 
|  | m = _LINK_ERROR.match(line) | 
|  | if m: | 
|  | error_code = int(m.groups()[0]) | 
|  | if error_code == _LINKER_RETRY_ERRORS: | 
|  | print 'Retrying link due to error %d' % error_code | 
|  | if pdb_name: | 
|  | shutil.copyfile(pdb_name, pdb_name + 'failure_backup') | 
|  | retry = True | 
|  | print line, | 
|  | result = link.wait() | 
|  | if not retry: | 
|  | break | 
|  | if result == 0 and sys.platform == 'win32': | 
|  | # Flush the file buffers to try to work around a Windows 10 kernel bug, | 
|  | # https://crbug.com/644525 | 
|  | output_handle = win32file.CreateFile(pe_name, win32file.GENERIC_WRITE, | 
|  | 0, None, win32file.OPEN_EXISTING, 0, 0) | 
|  | win32file.FlushFileBuffers(output_handle) | 
|  | output_handle.Close() | 
|  | return result | 
|  |  | 
|  | def ExecAsmWrapper(self, arch, *args): | 
|  | """Filter logo banner from invocations of asm.exe.""" | 
|  | env = self._GetEnv(arch) | 
|  | popen = subprocess.Popen(args, shell=True, env=env, | 
|  | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 
|  | out, _ = popen.communicate() | 
|  | for line in out.splitlines(): | 
|  | # Split to avoid triggering license checks: | 
|  | if (not line.startswith('Copy' + 'right (C' + | 
|  | ') Microsoft Corporation') and | 
|  | not line.startswith('Microsoft (R) Macro Assembler') and | 
|  | not line.startswith(' Assembling: ') and | 
|  | line): | 
|  | print line | 
|  | return popen.returncode | 
|  |  | 
|  | def ExecRcWrapper(self, arch, *args): | 
|  | """Converts .rc files to .res files.""" | 
|  | env = self._GetEnv(arch) | 
|  |  | 
|  | # We run two resource compilers: | 
|  | # 1. A custom one at build/toolchain/win/rc/rc.py which can run on | 
|  | #    non-Windows, and which has /showIncludes support so we can track | 
|  | #    dependencies (e.g. on .ico files) of .rc files. | 
|  | # 2. On Windows, regular Microsoft rc.exe, to make sure rc.py produces | 
|  | #    bitwise identical output. | 
|  |  | 
|  | # 1. Run our rc.py. | 
|  | # Also pass /showIncludes to track dependencies of .rc files. | 
|  | args = list(args) | 
|  | rcpy_args = args[:] | 
|  | rcpy_args[0:1] = [sys.executable, os.path.join(BASE_DIR, 'rc', 'rc.py')] | 
|  | rcpy_res_output = rcpy_args[-2] | 
|  | assert rcpy_res_output.startswith('/fo') | 
|  | assert rcpy_res_output.endswith('.res') | 
|  | rc_res_output = rcpy_res_output + '_ms_rc' | 
|  | args[-2] = rc_res_output | 
|  | rcpy_args.append('/showIncludes') | 
|  | rc_exe_exit_code = subprocess.call(rcpy_args, env=env) | 
|  | if rc_exe_exit_code == 0: | 
|  | # Since tool("rc") can't have deps, add deps on this script and on rc.py | 
|  | # and its deps here, so that rc edges become dirty if rc.py changes. | 
|  | print 'Note: including file: ../../build/toolchain/win/tool_wrapper.py' | 
|  | print 'Note: including file: ../../build/toolchain/win/rc/rc.py' | 
|  | print 'Note: including file: ../../build/toolchain/win/rc/linux64/rc.sha1' | 
|  | print 'Note: including file: ../../build/toolchain/win/rc/mac/rc.sha1' | 
|  | print 'Note: including file: ../../build/toolchain/win/rc/win/rc.exe.sha1' | 
|  |  | 
|  | # 2. Run Microsoft rc.exe. | 
|  | if sys.platform == 'win32' and rc_exe_exit_code == 0: | 
|  | popen = subprocess.Popen(args, shell=True, env=env, | 
|  | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 
|  | out, _ = popen.communicate() | 
|  | # Filter logo banner from invocations of rc.exe. Older versions of RC | 
|  | # don't support the /nologo flag. | 
|  | for line in out.splitlines(): | 
|  | if (not line.startswith('Microsoft (R) Windows (R) Resource Compiler') | 
|  | and not line.startswith('Copy' + 'right (C' + | 
|  | ') Microsoft Corporation') | 
|  | and line): | 
|  | print line | 
|  | rc_exe_exit_code = popen.returncode | 
|  | # Assert Microsoft rc.exe and rc.py produced identical .res files. | 
|  | if rc_exe_exit_code == 0: | 
|  | import filecmp | 
|  | # Strip "/fo" prefix. | 
|  | assert filecmp.cmp(rc_res_output[3:], rcpy_res_output[3:]) | 
|  | return rc_exe_exit_code | 
|  |  | 
|  | def ExecActionWrapper(self, arch, rspfile, *dirname): | 
|  | """Runs an action command line from a response file using the environment | 
|  | for |arch|. If |dirname| is supplied, use that as the working directory.""" | 
|  | env = self._GetEnv(arch) | 
|  | # TODO(scottmg): This is a temporary hack to get some specific variables | 
|  | # through to actions that are set after GN-time. http://crbug.com/333738. | 
|  | for k, v in os.environ.iteritems(): | 
|  | if k not in env: | 
|  | env[k] = v | 
|  | args = open(rspfile).read() | 
|  | dirname = dirname[0] if dirname else None | 
|  | return subprocess.call(args, shell=True, env=env, cwd=dirname) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main(sys.argv[1:])) |