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."""
7import ast
8import logging
9import optparse
10import os
11import os.path
12import shutil
13import subprocess
14import sys
15import tempfile
16
17
18_USAGE = """\
19Usage: %prog [options] [imports-file]
20
21Creates an import library from imports-file.
22
23Note: 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
50class _Error(Exception):
51  pass
52
53
54class _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
171def 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
216if __name__ == '__main__':
217  sys.exit(main())
218