#!/usr/bin/env python
# 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.
#
"""Creates an import library from an import description file."""
import ast
import logging
import optparse
import os
import os.path
import shutil
import subprocess
import sys
import tempfile


_USAGE = """\
Usage: %prog [options] [imports-file]

Creates an import library from imports-file.

Note: this script uses the microsoft assembler (ml.exe) and the library tool
    (lib.exe), both of which must be in path.
"""


_ASM_STUB_HEADER = """\
; This file is autogenerated by create_importlib_win.py, do not edit.
.386
.MODEL FLAT, C
.CODE

; Stubs to provide mangled names to lib.exe for the
; correct generation of import libs.
"""


_DEF_STUB_HEADER = """\
; This file is autogenerated by create_importlib_win.py, do not edit.

; Export declarations for generating import libs.
"""


_LOGGER = logging.getLogger()



class _Error(Exception):
  pass


class _ImportLibraryGenerator(object):
  def __init__(self, temp_dir):
    self._temp_dir = temp_dir

  def _Shell(self, cmd, **kw):
    ret = subprocess.call(cmd, **kw)
    _LOGGER.info('Running "%s" returned %d.', cmd, ret)
    if ret != 0:
      raise _Error('Command "%s" returned %d.' % (cmd, ret))

  def _ReadImportsFile(self, imports_file):
    # Slurp the imports file.
    return ast.literal_eval(open(imports_file).read())

  def _WriteStubsFile(self, import_names, output_file):
    output_file.write(_ASM_STUB_HEADER)

    for name in import_names:
      output_file.write('%s PROC\n' % name)
      output_file.write('%s ENDP\n' % name)

    output_file.write('END\n')

  def _WriteDefFile(self, dll_name, import_names, output_file):
    output_file.write(_DEF_STUB_HEADER)
    output_file.write('NAME %s\n' % dll_name)
    output_file.write('EXPORTS\n')
    for name in import_names:
      name = name.split('@')[0]
      output_file.write('  %s\n' % name)

  def _CreateObj(self, dll_name, imports):
    """Writes an assembly file containing empty declarations.

    For each imported function of the form:

    AddClipboardFormatListener@4 PROC
    AddClipboardFormatListener@4 ENDP

    The resulting object file is then supplied to lib.exe with a .def file
    declaring the corresponding non-adorned exports as they appear on the
    exporting DLL, e.g.

    EXPORTS
      AddClipboardFormatListener

    In combination, the .def file and the .obj file cause lib.exe to generate
    an x86 import lib with public symbols named like
    "__imp__AddClipboardFormatListener@4", binding to exports named like
    "AddClipboardFormatListener".

    All of this is perpetrated in a temporary directory, as the intermediate
    artifacts are quick and easy to produce, and of no interest to anyone
    after the fact."""

    # Create an .asm file to provide stdcall-like stub names to lib.exe.
    asm_name = dll_name + '.asm'
    _LOGGER.info('Writing asm file "%s".', asm_name)
    with open(os.path.join(self._temp_dir, asm_name), 'wb') as stubs_file:
      self._WriteStubsFile(imports, stubs_file)

    # Invoke on the assembler to compile it to .obj.
    obj_name = dll_name + '.obj'
    cmdline = ['ml.exe', '/nologo', '/c', asm_name, '/Fo', obj_name]
    self._Shell(cmdline, cwd=self._temp_dir, stdout=open(os.devnull))

    return obj_name

  def _CreateImportLib(self, dll_name, imports, architecture, output_file):
    """Creates an import lib binding imports to dll_name for architecture.

    On success, writes the import library to output file.
    """
    obj_file = None

    # For x86 architecture we have to provide an object file for correct
    # name mangling between the import stubs and the exported functions.
    if architecture == 'x86':
      obj_file = self._CreateObj(dll_name, imports)

    # Create the corresponding .def file. This file has the non stdcall-adorned
    # names, as exported by the destination DLL.
    def_name = dll_name + '.def'
    _LOGGER.info('Writing def file "%s".', def_name)
    with open(os.path.join(self._temp_dir, def_name), 'wb') as def_file:
      self._WriteDefFile(dll_name, imports, def_file)

    # Invoke on lib.exe to create the import library.
    # We generate everything into the temporary directory, as the .exp export
    # files will be generated at the same path as the import library, and we
    # don't want those files potentially gunking the works.
    dll_base_name, ext = os.path.splitext(dll_name)
    lib_name = dll_base_name + '.lib'
    cmdline = ['lib.exe',
               '/machine:%s' % architecture,
               '/def:%s' % def_name,
               '/out:%s' % lib_name]
    if obj_file:
      cmdline.append(obj_file)

    self._Shell(cmdline, cwd=self._temp_dir, stdout=open(os.devnull))

    # Copy the .lib file to the output directory.
    shutil.copyfile(os.path.join(self._temp_dir, lib_name), output_file)
    _LOGGER.info('Created "%s".', output_file)

  def CreateImportLib(self, imports_file, output_file):
    # Read the imports file.
    imports = self._ReadImportsFile(imports_file)

    # Creates the requested import library in the output directory.
    self._CreateImportLib(imports['dll_name'],
                          imports['imports'],
                          imports.get('architecture', 'x86'),
                          output_file)


def main():
  parser = optparse.OptionParser(usage=_USAGE)
  parser.add_option('-o', '--output-file',
                    help='Specifies the output file path.')
  parser.add_option('-k', '--keep-temp-dir',
                    action='store_true',
                    help='Keep the temporary directory.')
  parser.add_option('-v', '--verbose',
                    action='store_true',
                    help='Verbose logging.')

  options, args = parser.parse_args()

  if len(args) != 1:
    parser.error('You must provide an imports file.')

  if not options.output_file:
    parser.error('You must provide an output file.')

  options.output_file = os.path.abspath(options.output_file)

  if options.verbose:
    logging.basicConfig(level=logging.INFO)
  else:
    logging.basicConfig(level=logging.WARN)


  temp_dir = tempfile.mkdtemp()
  _LOGGER.info('Created temporary directory "%s."', temp_dir)
  try:
    # Create a generator and create the import lib.
    generator = _ImportLibraryGenerator(temp_dir)

    ret = generator.CreateImportLib(args[0], options.output_file)
  except Exception, e:
    _LOGGER.exception('Failed to create import lib.')
    ret = 1
  finally:
    if not options.keep_temp_dir:
      shutil.rmtree(temp_dir)
      _LOGGER.info('Deleted temporary directory "%s."', temp_dir)

  return ret


if __name__ == '__main__':
  sys.exit(main())
