Home | History | Annotate | Download | only in importlibs
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 #
      6 """Creates an import library from an import description file."""
      7 import ast
      8 import logging
      9 import optparse
     10 import os
     11 import os.path
     12 import shutil
     13 import subprocess
     14 import sys
     15 import tempfile
     16 
     17 
     18 _USAGE = """\
     19 Usage: %prog [options] [imports-file]
     20 
     21 Creates an import library from imports-file.
     22 
     23 Note: this script uses the microsoft assembler (ml.exe) and the library tool
     24     (lib.exe), both of which must be in path.
     25 """
     26 
     27 
     28 _ASM_STUB_HEADER = """\
     29 ; This file is autogenerated by create_importlib_win.py, do not edit.
     30 .386
     31 .MODEL FLAT, C
     32 .CODE
     33 
     34 ; Stubs to provide mangled names to lib.exe for the
     35 ; correct generation of import libs.
     36 """
     37 
     38 
     39 _DEF_STUB_HEADER = """\
     40 ; This file is autogenerated by create_importlib_win.py, do not edit.
     41 
     42 ; Export declarations for generating import libs.
     43 """
     44 
     45 
     46 _LOGGER = logging.getLogger()
     47 
     48 
     49 
     50 class _Error(Exception):
     51   pass
     52 
     53 
     54 class _ImportLibraryGenerator(object):
     55   def __init__(self, temp_dir):
     56     self._temp_dir = temp_dir
     57 
     58   def _Shell(self, cmd, **kw):
     59     ret = subprocess.call(cmd, **kw)
     60     _LOGGER.info('Running "%s" returned %d.', cmd, ret)
     61     if ret != 0:
     62       raise _Error('Command "%s" returned %d.' % (cmd, ret))
     63 
     64   def _ReadImportsFile(self, imports_file):
     65     # Slurp the imports file.
     66     return ast.literal_eval(open(imports_file).read())
     67 
     68   def _WriteStubsFile(self, import_names, output_file):
     69     output_file.write(_ASM_STUB_HEADER)
     70 
     71     for name in import_names:
     72       output_file.write('%s PROC\n' % name)
     73       output_file.write('%s ENDP\n' % name)
     74 
     75     output_file.write('END\n')
     76 
     77   def _WriteDefFile(self, dll_name, import_names, output_file):
     78     output_file.write(_DEF_STUB_HEADER)
     79     output_file.write('NAME %s\n' % dll_name)
     80     output_file.write('EXPORTS\n')
     81     for name in import_names:
     82       name = name.split('@')[0]
     83       output_file.write('  %s\n' % name)
     84 
     85   def _CreateObj(self, dll_name, imports):
     86     """Writes an assembly file containing empty declarations.
     87 
     88     For each imported function of the form:
     89 
     90     AddClipboardFormatListener@4 PROC
     91     AddClipboardFormatListener@4 ENDP
     92 
     93     The resulting object file is then supplied to lib.exe with a .def file
     94     declaring the corresponding non-adorned exports as they appear on the
     95     exporting DLL, e.g.
     96 
     97     EXPORTS
     98       AddClipboardFormatListener
     99 
    100     In combination, the .def file and the .obj file cause lib.exe to generate
    101     an x86 import lib with public symbols named like
    102     "__imp__AddClipboardFormatListener@4", binding to exports named like
    103     "AddClipboardFormatListener".
    104 
    105     All of this is perpetrated in a temporary directory, as the intermediate
    106     artifacts are quick and easy to produce, and of no interest to anyone
    107     after the fact."""
    108 
    109     # Create an .asm file to provide stdcall-like stub names to lib.exe.
    110     asm_name = dll_name + '.asm'
    111     _LOGGER.info('Writing asm file "%s".', asm_name)
    112     with open(os.path.join(self._temp_dir, asm_name), 'wb') as stubs_file:
    113       self._WriteStubsFile(imports, stubs_file)
    114 
    115     # Invoke on the assembler to compile it to .obj.
    116     obj_name = dll_name + '.obj'
    117     cmdline = ['ml.exe', '/nologo', '/c', asm_name, '/Fo', obj_name]
    118     self._Shell(cmdline, cwd=self._temp_dir, stdout=open(os.devnull))
    119 
    120     return obj_name
    121 
    122   def _CreateImportLib(self, dll_name, imports, architecture, output_file):
    123     """Creates an import lib binding imports to dll_name for architecture.
    124 
    125     On success, writes the import library to output file.
    126     """
    127     obj_file = None
    128 
    129     # For x86 architecture we have to provide an object file for correct
    130     # name mangling between the import stubs and the exported functions.
    131     if architecture == 'x86':
    132       obj_file = self._CreateObj(dll_name, imports)
    133 
    134     # Create the corresponding .def file. This file has the non stdcall-adorned
    135     # names, as exported by the destination DLL.
    136     def_name = dll_name + '.def'
    137     _LOGGER.info('Writing def file "%s".', def_name)
    138     with open(os.path.join(self._temp_dir, def_name), 'wb') as def_file:
    139       self._WriteDefFile(dll_name, imports, def_file)
    140 
    141     # Invoke on lib.exe to create the import library.
    142     # We generate everything into the temporary directory, as the .exp export
    143     # files will be generated at the same path as the import library, and we
    144     # don't want those files potentially gunking the works.
    145     dll_base_name, ext = os.path.splitext(dll_name)
    146     lib_name = dll_base_name + '.lib'
    147     cmdline = ['lib.exe',
    148                '/machine:%s' % architecture,
    149                '/def:%s' % def_name,
    150                '/out:%s' % lib_name]
    151     if obj_file:
    152       cmdline.append(obj_file)
    153 
    154     self._Shell(cmdline, cwd=self._temp_dir, stdout=open(os.devnull))
    155 
    156     # Copy the .lib file to the output directory.
    157     shutil.copyfile(os.path.join(self._temp_dir, lib_name), output_file)
    158     _LOGGER.info('Created "%s".', output_file)
    159 
    160   def CreateImportLib(self, imports_file, output_file):
    161     # Read the imports file.
    162     imports = self._ReadImportsFile(imports_file)
    163 
    164     # Creates the requested import library in the output directory.
    165     self._CreateImportLib(imports['dll_name'],
    166                           imports['imports'],
    167                           imports.get('architecture', 'x86'),
    168                           output_file)
    169 
    170 
    171 def main():
    172   parser = optparse.OptionParser(usage=_USAGE)
    173   parser.add_option('-o', '--output-file',
    174                     help='Specifies the output file path.')
    175   parser.add_option('-k', '--keep-temp-dir',
    176                     action='store_true',
    177                     help='Keep the temporary directory.')
    178   parser.add_option('-v', '--verbose',
    179                     action='store_true',
    180                     help='Verbose logging.')
    181 
    182   options, args = parser.parse_args()
    183 
    184   if len(args) != 1:
    185     parser.error('You must provide an imports file.')
    186 
    187   if not options.output_file:
    188     parser.error('You must provide an output file.')
    189 
    190   options.output_file = os.path.abspath(options.output_file)
    191 
    192   if options.verbose:
    193     logging.basicConfig(level=logging.INFO)
    194   else:
    195     logging.basicConfig(level=logging.WARN)
    196 
    197 
    198   temp_dir = tempfile.mkdtemp()
    199   _LOGGER.info('Created temporary directory "%s."', temp_dir)
    200   try:
    201     # Create a generator and create the import lib.
    202     generator = _ImportLibraryGenerator(temp_dir)
    203 
    204     ret = generator.CreateImportLib(args[0], options.output_file)
    205   except Exception, e:
    206     _LOGGER.exception('Failed to create import lib.')
    207     ret = 1
    208   finally:
    209     if not options.keep_temp_dir:
    210       shutil.rmtree(temp_dir)
    211       _LOGGER.info('Deleted temporary directory "%s."', temp_dir)
    212 
    213   return ret
    214 
    215 
    216 if __name__ == '__main__':
    217   sys.exit(main())
    218