#!/usr/bin/env python
# Copyright 2017 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.

"""usage: rc.py [options] input.res
A resource compiler for .rc files.

options:
-h, --help     Print this message.
-I<dir>        Add include path.
-D<sym>        Define a macro for the preprocessor.
/fo<out>       Set path of output .res file.
/showIncludes  Print referenced header and resource files."""

from __future__ import print_function
from collections import namedtuple
import codecs
import os
import re
import subprocess
import sys
import tempfile


THIS_DIR = os.path.abspath(os.path.dirname(__file__))
SRC_DIR = \
    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR))))


def ParseFlags():
  """Parses flags off sys.argv and returns the parsed flags."""
  # Can't use optparse / argparse because of /fo flag :-/
  includes = []
  defines = []
  output = None
  input = None
  show_includes = False
  # Parse.
  for flag in sys.argv[1:]:
    if flag == '-h' or flag == '--help':
      print(__doc__)
      sys.exit(0)
    if flag.startswith('-I'):
      includes.append(flag)
    elif flag.startswith('-D'):
      defines.append(flag)
    elif flag.startswith('/fo'):
      if output:
        print('rc.py: error: multiple /fo flags', '/fo' + output, flag,
              file=sys.stderr)
        sys.exit(1)
      output = flag[3:]
    elif flag == '/showIncludes':
      show_includes = True
    elif (flag.startswith('-') or
          (flag.startswith('/') and not os.path.exists(flag))):
      print('rc.py: error: unknown flag', flag, file=sys.stderr)
      print(__doc__, file=sys.stderr)
      sys.exit(1)
    else:
      if input:
        print('rc.py: error: multiple inputs:', input, flag, file=sys.stderr)
        sys.exit(1)
      input = flag
  # Validate and set default values.
  if not input:
    print('rc.py: error: no input file', file=sys.stderr)
    sys.exit(1)
  if not output:
    output = os.path.splitext(input)[0] + '.res'
  Flags = namedtuple('Flags', ['includes', 'defines', 'output', 'input',
                               'show_includes'])
  return Flags(includes=includes, defines=defines, output=output, input=input,
               show_includes=show_includes)


def ReadInput(input):
  """"Reads input and returns it. For UTF-16LEBOM input, converts to UTF-8."""
  # Microsoft's rc.exe only supports unicode in the form of UTF-16LE with a BOM.
  # Our rc binary sniffs for UTF-16LE.  If that's not found, if /utf-8 is
  # passed, the input is treated as UTF-8.  If /utf-8 is not passed and the
  # input is not UTF-16LE, then our rc errors out on characters outside of
  # 7-bit ASCII.  Since the driver always converts UTF-16LE to UTF-8 here (for
  # the preprocessor, which doesn't support UTF-16LE), our rc will either see
  # UTF-8 with the /utf-8 flag (for UTF-16LE input), or ASCII input.
  # This is compatible with Microsoft rc.exe.  If we wanted, we could expose
  # a /utf-8 flag for the driver for UTF-8 .rc inputs too.
  # TODO(thakis): Microsoft's rc.exe supports BOM-less UTF-16LE. We currently
  # don't, but for chrome it currently doesn't matter.
  is_utf8 = False
  try:
    with open(input, 'rb') as rc_file:
      rc_file_data = rc_file.read()
      if rc_file_data.startswith(codecs.BOM_UTF16_LE):
        rc_file_data = rc_file_data[2:].decode('utf-16le').encode('utf-8')
        is_utf8 = True
  except IOError:
    print('failed to open', input, file=sys.stderr)
    sys.exit(1)
  except UnicodeDecodeError:
    print('failed to decode UTF-16 despite BOM', input, file=sys.stderr)
    sys.exit(1)
  return rc_file_data, is_utf8


def Preprocess(rc_file_data, flags):
  """Runs the input file through the preprocessor."""
  clang = os.path.join(SRC_DIR, 'third_party', 'llvm-build',
                       'Release+Asserts', 'bin', 'clang-cl')
  # Let preprocessor write to a temp file so that it doesn't interfere
  # with /showIncludes output on stdout.
  if sys.platform == 'win32':
    clang += '.exe'
  temp_handle, temp_file = tempfile.mkstemp(suffix='.i')
  # Closing temp_handle immediately defeats the purpose of mkstemp(), but I
  # can't figure out how to let write to the temp file on Windows otherwise.
  os.close(temp_handle)
  clang_cmd = ([clang, '/P', '/DRC_INVOKED', '/TC', '-', '/Fi' + temp_file] +
               flags.includes + flags.defines)
  if os.path.dirname(flags.input):
    clang_cmd.append('-I' + os.path.dirname(flags.input))
  if flags.show_includes:
    clang_cmd.append('/showIncludes')
  p = subprocess.Popen(clang_cmd, stdin=subprocess.PIPE)
  p.communicate(input=rc_file_data)
  if p.returncode != 0:
    sys.exit(p.returncode)
  preprocessed_output = open(temp_file, 'rb').read()
  os.remove(temp_file)

  # rc.exe has a wacko preprocessor:
  # https://msdn.microsoft.com/en-us/library/windows/desktop/aa381033(v=vs.85).aspx
  # """RC treats files with the .c and .h extensions in a special manner. It
  # assumes that a file with one of these extensions does not contain
  # resources. If a file has the .c or .h file name extension, RC ignores all
  # lines in the file except the preprocessor directives."""
  # Thankfully, the Microsoft headers are mostly good about putting everything
  # in the system headers behind `if !defined(RC_INVOKED)`, so regular
  # preprocessing with RC_INVOKED defined almost works. The one exception
  # is struct tagCRGB in dlgs.h, but that will be fixed in the next major
  # SDK release too.
  # TODO(thakis): Remove this once an SDK with the fix has been released.
  preprocessed_output = re.sub('typedef struct tagCRGB\s*{[^}]*} CRGB;', '',
                               preprocessed_output)
  return preprocessed_output


def RunRc(preprocessed_output, is_utf8, flags):
  if sys.platform.startswith('linux'):
    rc = os.path.join(THIS_DIR, 'linux64', 'rc')
  elif sys.platform == 'darwin':
    rc = os.path.join(THIS_DIR, 'mac', 'rc')
  elif sys.platform == 'win32':
    rc = os.path.join(THIS_DIR, 'win', 'rc.exe')
  else:
    print('rc.py: error: unsupported platform', sys.platform, file=sys.stderr)
    sys.exit(1)
  rc_cmd = [rc]
  # Make sure rc-relative resources can be found:
  if os.path.dirname(flags.input):
    rc_cmd.append('/cd' + os.path.dirname(flags.input))
  rc_cmd.append('/fo' + flags.output)
  if is_utf8:
    rc_cmd.append('/utf-8')
  # TODO(thakis): rc currently always prints full paths for /showIncludes,
  # but clang-cl /P doesn't.  Which one is right?
  if flags.show_includes:
    rc_cmd.append('/showIncludes')
  # Microsoft rc.exe searches for referenced files relative to -I flags in
  # addition to the pwd, so -I flags need to be passed both to both
  # the preprocessor and rc.
  rc_cmd += flags.includes
  p = subprocess.Popen(rc_cmd, stdin=subprocess.PIPE)
  p.communicate(input=preprocessed_output)
  return p.returncode


def main():
  # This driver has to do these things:
  # 1. Parse flags.
  # 2. Convert the input from UTF-16LE to UTF-8 if needed.
  # 3. Pass the input through a preprocessor (and clean up the preprocessor's
  #    output in minor ways).
  # 4. Call rc for the heavy lifting.
  flags = ParseFlags()
  rc_file_data, is_utf8 = ReadInput(flags.input)
  preprocessed_output = Preprocess(rc_file_data, flags)
  return RunRc(preprocessed_output, is_utf8, flags)


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