Home | History | Annotate | Download | only in gyp
      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