1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150

build / win / message_compiler.py [blame]

# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Runs the Microsoft Message Compiler (mc.exe).
#
# Usage: message_compiler.py <environment_file> [<args to mc.exe>*]


import difflib
import filecmp
import os
import re
import shutil
import subprocess
import sys
import tempfile

def main():
  env_file, rest = sys.argv[1], sys.argv[2:]

  # Parse some argument flags.
  header_dir = None
  resource_dir = None
  input_file = None
  for i, arg in enumerate(rest):
    if arg == '-h' and len(rest) > i + 1:
      assert header_dir == None
      header_dir = rest[i + 1]
    elif arg == '-r' and len(rest) > i + 1:
      assert resource_dir == None
      resource_dir = rest[i + 1]
    elif arg.endswith('.mc') or arg.endswith('.man'):
      assert input_file == None
      input_file = arg

  # Copy checked-in outputs to final location.
  THIS_DIR = os.path.abspath(os.path.dirname(__file__))
  assert header_dir == resource_dir
  source = os.path.join(THIS_DIR, "..", "..",
      "third_party", "win_build_output",
      re.sub(r'^(?:[^/]+/)?gen/', 'mc/', header_dir))
  # Set copy_function to shutil.copy to update the timestamp on the destination.
  shutil.copytree(source,
                  header_dir,
                  copy_function=shutil.copy,
                  dirs_exist_ok=True)

  # On non-Windows, that's all we can do.
  if sys.platform != 'win32':
    return

  # On Windows, run mc.exe on the input and check that its outputs are
  # identical to the checked-in outputs.

  # Read the environment block from the file. This is stored in the format used
  # by CreateProcess. Drop last 2 NULs, one for list terminator, one for
  # trailing vs. separator.
  env_pairs = open(env_file).read()[:-2].split('\0')
  env_dict = dict([item.split('=', 1) for item in env_pairs])

  extension = os.path.splitext(input_file)[1]
  if extension in ['.man', '.mc']:
    # For .man files, mc's output changed significantly from Version 10.0.15063
    # to Version 10.0.16299.  We should always have the output of the current
    # default SDK checked in and compare to that. Early out if a different SDK
    # is active. This also happens with .mc files.
    # TODO(thakis): Check in new baselines and compare to 16299 instead once
    # we use the 2017 Fall Creator's Update by default.
    mc_help = subprocess.check_output(['mc.exe', '/?'], env=env_dict,
                                      stderr=subprocess.STDOUT, shell=True)
    version = re.search(br'Message Compiler\s+Version (\S+)', mc_help).group(1)
    if version != b'10.0.22621':
      return

  # mc writes to stderr, so this explicitly redirects to stdout and eats it.
  try:
    tmp_dir = tempfile.mkdtemp()
    delete_tmp_dir = True
    if header_dir:
      rest[rest.index('-h') + 1] = tmp_dir
      header_dir = tmp_dir
    if resource_dir:
      rest[rest.index('-r') + 1] = tmp_dir
      resource_dir = tmp_dir

    # This needs shell=True to search the path in env_dict for the mc
    # executable.
    subprocess.check_output(['mc.exe'] + rest,
                            env=env_dict,
                            stderr=subprocess.STDOUT,
                            shell=True)
    # We require all source code (in particular, the header generated here) to
    # be UTF-8. jinja can output the intermediate .mc file in UTF-8 or UTF-16LE.
    # However, mc.exe only supports Unicode via the -u flag, and it assumes when
    # that is specified that the input is UTF-16LE (and errors out on UTF-8
    # files, assuming they're ANSI). Even with -u specified and UTF16-LE input,
    # it generates an ANSI header, and includes broken versions of the message
    # text in the comment before the value. To work around this, for any invalid
    # // comment lines, we simply drop the line in the header after building it.
    # Also, mc.exe apparently doesn't always write #define lines in
    # deterministic order, so manually sort each block of #defines.
    if header_dir:
      header_file = os.path.join(
          header_dir, os.path.splitext(os.path.basename(input_file))[0] + '.h')
      header_contents = []
      with open(header_file, 'rb') as f:
        define_block = []  # The current contiguous block of #defines.
        for line in f.readlines():
          if line.startswith(b'//') and b'?' in line:
            continue
          if line.startswith(b'#define '):
            define_block.append(line)
            continue
          # On the first non-#define line, emit the sorted preceding #define
          # block.
          header_contents += sorted(define_block, key=lambda s: s.split()[-1])
          define_block = []
          header_contents.append(line)
        # If the .h file ends with a #define block, flush the final block.
        header_contents += sorted(define_block, key=lambda s: s.split()[-1])
      with open(header_file, 'wb') as f:
        f.write(b''.join(header_contents))

    # mc.exe invocation and post-processing are complete, now compare the output
    # in tmp_dir to the checked-in outputs.
    diff = filecmp.dircmp(tmp_dir, source)
    if diff.diff_files or set(diff.left_list) != set(diff.right_list):
      print('mc.exe output different from files in %s, see %s' % (source,
                                                                  tmp_dir))
      diff.report()
      for f in diff.diff_files:
        if f.endswith('.bin'): continue
        fromfile = os.path.join(source, f)
        tofile = os.path.join(tmp_dir, f)
        print(''.join(
            difflib.unified_diff(
                open(fromfile).readlines(),
                open(tofile).readlines(), fromfile, tofile)))
      delete_tmp_dir = False
      sys.exit(1)
  except subprocess.CalledProcessError as e:
    print(e.output)
    sys.exit(e.returncode)
  finally:
    if os.path.exists(tmp_dir) and delete_tmp_dir:
      shutil.rmtree(tmp_dir)

if __name__ == '__main__':
  main()