chromium/tools/grit/preprocess_if_expr.py

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

import argparse
import errno
import io
import json
import os
import sys

# For Node, EvaluateExpression
import grit.node.base
# For CheckConditionalElements
import grit.format.html_inline

_CWD = os.getcwd()


class PreprocessIfExprNode(grit.node.base.Node):
  def __init__(self):
    super().__init__()

  def PreprocessIfExpr(self, content, removal_comments_extension):
    return grit.format.html_inline.CheckConditionalElements(
        self, content, removal_comments_extension)

  def EvaluateCondition(self, expr):
    return grit.node.base.Node.EvaluateExpression(expr, self.defines,
                                                  self.target_platform, {})

  def SetDefines(self, defines):
    self.defines = defines

  def SetTargetPlatform(self, target_platform):
    self.target_platform = target_platform

  @staticmethod
  def Construct(defines, target_platform):
    node = PreprocessIfExprNode()
    node.SetDefines(defines)
    node.SetTargetPlatform(target_platform or sys.platform)
    return node


def ParseDefinesArg(definesArg):
  defines = {}
  for define in definesArg:
    parts = [part.strip() for part in define.split('=', 1)]
    name = parts[0]
    val = True if len(parts) == 1 else parts[1]
    if (val == "1" or val == "true"):
      val = True
    elif (val == "0" or val == "false"):
      val = False
    defines[name] = val
  return defines


def ExtensionForComments(input_file):
  """Get the file extension that determines the comment style.

  Returns the file extension that determines the format of the
  'grit-removed-lines' comments. '.ts' or '.js' will produce '/*...*/'-style
  comments, '.html; will produce '<!-- -->'-style comments.
  """
  split = os.path.splitext(input_file)
  extension = split[1]
  # .html.ts and .html.js files should still use HTML comments.
  if os.path.splitext(split[0])[1] == '.html':
    extension = '.html'
  return extension


def main(argv):
  parser = argparse.ArgumentParser()
  parser.add_argument('--in-folder', required=True)
  parser.add_argument('--out-folder', required=True)
  parser.add_argument('--out-manifest')
  parser.add_argument('--in-files', required=True, nargs="*")
  parser.add_argument('-D', '--defines', action='append')
  parser.add_argument('-E', '--environment')
  parser.add_argument('-t', '--target')
  parser.add_argument('--enable_removal_comments', action='store_true')
  args = parser.parse_args(argv)

  in_folder = os.path.normpath(os.path.join(_CWD, args.in_folder))
  out_folder = os.path.normpath(os.path.join(_CWD, args.out_folder))

  defines = ParseDefinesArg(args.defines)

  node = PreprocessIfExprNode.Construct(defines, args.target)

  for input_file in args.in_files:
    in_path = os.path.join(in_folder, input_file)
    content = ""
    with open(in_path, encoding='utf-8') as f:
      content = f.read()

    removal_comments_extension = None  # None means no removal comments
    if args.enable_removal_comments:
      removal_comments_extension = ExtensionForComments(input_file)

    try:
      preprocessed = node.PreprocessIfExpr(content, removal_comments_extension)
    except:
      raise Exception('Error processing %s' % in_path)
    out_path = os.path.join(out_folder, input_file)
    out_dir = os.path.dirname(out_path)
    assert out_dir.startswith(out_folder), \
           'Cannot preprocess files to locations not under %s.' % out_dir
    try:
      os.makedirs(out_dir)
    except OSError as e:
      # Ignore directory exists errors. This can happen if two build rules
      # for overlapping directories hit the makedirs line at the same time.
      if e.errno != errno.EEXIST:
        raise

    # Delete the target file before witing it, as it may be hardlinked to other
    # files, which can break the build. This is the case in particular if the
    # file was "copied" to different locations with GN (as GN's copy is actually
    # a hard link under the hood). See https://crbug.com/1332497
    if os.path.exists(out_path):
      os.remove(out_path)

    # Detect and delete any stale TypeScript files present in the output folder,
    # corresponding to input .js files, since they can get picked up by
    # subsequent ts_library() invocations and cause transient build failures.
    # This can happen when a file is migrated from JS to TS  and a bot is
    # switched from building a later CL to building an earlier CL.
    [pathname, extension] = os.path.splitext(out_path)
    if extension == '.js':
      to_check = pathname + '.ts'
      if os.path.exists(to_check):
        os.remove(to_check)

    with open(out_path, mode='wb') as f:
      f.write(preprocessed.encode('utf-8'))

  if args.out_manifest:
    manifest_data = {}
    manifest_data['base_dir'] = '%s' % args.out_folder
    manifest_data['files'] = args.in_files
    manifest_file = open(
        os.path.normpath(os.path.join(_CWD, args.out_manifest)), 'w',
        encoding='utf-8', newline='\n')
    json.dump(manifest_data, manifest_file)
  return


if __name__ == '__main__':
  main(sys.argv[1:])