chromium/third_party/google_input_tools/update.py

#!/usr/bin/python
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Performs pull of google-input-tools from local clone of GitHub repository."""

import json
import logging
import optparse
import os
import re
import shutil
import subprocess

_BASE_REGEX_STRING = r'^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)'
require_regex = re.compile(_BASE_REGEX_STRING % 'require')
provide_regex = re.compile(_BASE_REGEX_STRING % 'provide')

# Entry-points required to build a virtual keyboard.
namespaces = [
    'i18n.input.chrome.inputview.Controller',
    'i18n.input.chrome.inputview.content.compact.letter',
    'i18n.input.chrome.inputview.content.compact.util',
    'i18n.input.chrome.inputview.content.compact.symbol',
    'i18n.input.chrome.inputview.content.compact.more',
    'i18n.input.chrome.inputview.content.compact.numberpad',
    'i18n.input.chrome.inputview.content.ContextlayoutUtil',
    'i18n.input.chrome.inputview.content.util',
    'i18n.input.chrome.inputview.EmojiType',
    'i18n.input.chrome.inputview.layouts.CompactSpaceRow',
    'i18n.input.chrome.inputview.layouts.RowsOf101',
    'i18n.input.chrome.inputview.layouts.RowsOf102',
    'i18n.input.chrome.inputview.layouts.RowsOfCompact',
    'i18n.input.chrome.inputview.layouts.RowsOfJP',
    'i18n.input.chrome.inputview.layouts.RowsOfNumberpad',
    'i18n.input.chrome.inputview.layouts.SpaceRow',
    'i18n.input.chrome.inputview.layouts.util',
    'i18n.input.chrome.inputview.layouts.material.CompactSpaceRow',
    'i18n.input.chrome.inputview.layouts.material.RowsOf101',
    'i18n.input.chrome.inputview.layouts.material.RowsOf102',
    'i18n.input.chrome.inputview.layouts.material.RowsOfCompact',
    'i18n.input.chrome.inputview.layouts.material.RowsOfJP',
    'i18n.input.chrome.inputview.layouts.material.RowsOfNumberpad',
    'i18n.input.chrome.inputview.layouts.material.SpaceRow',
    'i18n.input.chrome.inputview.layouts.material.util',
    'i18n.input.hwt.util'
]

# Any additional required files.
extras = [
    'common.css',
    'emoji.css'
]


def process_file(filename):
  """Extracts provided and required namespaces.

  Description:
    Scans Javascript file for provied and required namespaces.

  Args:
    filename: name of the file to process.

  Returns:
    Pair of lists, where the first list contains namespaces provided by the file
    and the second contains a list of requirements.
  """
  provides = []
  requires = []
  file_handle = open(filename, 'r')
  try:
    for line in file_handle:
      if re.match(require_regex, line):
        requires.append(re.search(require_regex, line).group(1))
      if re.match(provide_regex, line):
        provides.append(re.search(provide_regex, line).group(1))
  finally:
    file_handle.close()
  return provides, requires


def expand_directories(refs):
  """Expands any directory references into inputs.

  Description:
    Looks for any directories in the provided references.  Found directories
    are recursively searched for .js files.

  Args:
    refs: a list of directories.

  Returns:
    Pair of maps, where the first maps each namepace to the filename that
    provides the namespace, and the second maps a filename to prerequisite
    namespaces.
  """
  providers = {}
  requirements = {}
  for ref in refs:
    if os.path.isdir(ref):
      for (root, _, files) in os.walk(ref):
        for name in files:
          if name.endswith('js'):
            filename = os.path.join(root, name)
            provides, requires = process_file(filename)
            for p in provides:
              providers[p] = filename
            requirements[filename] = []
            for r in requires:
              requirements[filename].append(r)
  return providers, requirements


def extract_dependencies(namespace, providers, requirements, dependencies):
  """Recursively extracts all dependencies for a namespace.

  Description:
    Recursively extracts all dependencies for a namespace.

  Args:
    namespace: The namespace to process.
    providers: Mapping of namespace to filename that provides the namespace.
    requirements: Mapping of filename to a list of prerequisite namespaces.
    dependencies: List of files required to build inputview.
  Returns:
  """

  if namespace in providers:
    filename = providers[namespace]
    if filename not in dependencies:
      for ns in requirements[filename]:
        extract_dependencies(ns, providers, requirements, dependencies)
    dependencies.add(filename)


def home_dir():
  """Resolves the user's home directory."""

  return os.path.expanduser('~')


def expand_path_relative_to_home(path):
  """Resolves a path that is relative to the home directory.

  Args:
    path: Relative path.

  Returns:
    Resolved path.
  """

  return os.path.join(os.path.expanduser('~'), path)


def get_google_input_tools_sandbox_from_options(options):
  """Generate the input-input-tools path from the --input flag.

  Args:
    options: Flags to update.py.
  Returns:
    Path to the google-input-tools sandbox.
  """

  path = options.input
  if not path:
    path = expand_path_relative_to_home('google-input-tools')
    print 'Unspecified path for google-input-tools. Defaulting to %s' % path
  return path


def get_closure_library_sandbox_from_options(options):
  """Generate the closure-library path from the --input flag.

  Args:
    options: Flags to update.py.
  Returns:
    Path to the closure-library sandbox.
  """

  path = options.lib
  if not path:
    path = expand_path_relative_to_home('closure-library')
    print 'Unspecified path for closure-library. Defaulting to %s' % path
  return path


def copy_file(source, target):
  """Copies a file from the source to the target location.

  Args:
    source: Path to the source file to copy.
    target: Path to the target location to copy the file.
  """

  if not os.path.exists(os.path.dirname(target)):
    os.makedirs(os.path.dirname(target))
  shutil.copy(source, target)
  # Ensure correct file permissions.
  if target.endswith('py'):
    subprocess.call(['chmod', '+x', target])
  else:
    subprocess.call(['chmod', '-x', target])


def update_file(filename, input_source, closure_source, target_files):
  """Updates files in third_party/google_input_tools.

  Args:
    filename: The file to update.
    input_source: Root of the google_input_tools sandbox.
    closure_source: Root of the closure_library sandbox.
    target_files: List of relative paths to target files.
  """

  target = ''
  if filename.startswith(input_source):
    target = os.path.join('src', filename[len(input_source)+1:])
  elif filename.startswith(closure_source):
    target = os.path.join('third_party/closure_library',
                          filename[len(closure_source)+1:])
  if target:
    copy_file(filename, target)
    target_files.append(os.path.relpath(target, os.getcwd()))


def generate_build_file(target_files):
  """Updates inputview.json.

  Args:
    target_files: List of files required to build inputview.js.
  """

  sorted_files = sorted(target_files)
  with open('inputview.json', 'w') as file_handle:
    json_data = {'inputview_sources': sorted_files}
    json_str = json.dumps(json_data, indent=2, separators=(',', ': '))
    file_handle.write(json_str.replace('\"', '\''))


def copy_dir(input_path, sub_dir):
  """Copies all files in a subdirectory of google-input-tools.

  Description:
    Recursive copy of a directory under google-input-tools.  Used to copy
    localization and resource files.

  Args:
    input_path: Path to the google-input-tools-sandbox.
    sub_dir: Subdirectory to copy within google-input-tools sandbox.
  """
  source_dir = os.path.join(input_path, 'chrome', 'os', sub_dir)
  for (root, _, files) in os.walk(source_dir):
    for name in files:
      filename = os.path.join(root, name)
      relative_path = filename[len(source_dir) + 1:]
      target = os.path.join('src', 'chrome', 'os', sub_dir,
                            relative_path)
      copy_file(filename, target)


def main():
  """The entrypoint for this script."""

  logging.basicConfig(format='update.py: %(message)s', level=logging.INFO)

  usage = 'usage: %prog [options] arg'
  parser = optparse.OptionParser(usage)
  parser.add_option('-i',
                    '--input',
                    dest='input',
                    action='append',
                    help='Path to the google-input-tools sandbox.')
  parser.add_option('-l',
                    '--lib',
                    dest='lib',
                    action='store',
                    help='Path to the closure-library sandbox.')

  (options, _) = parser.parse_args()

  input_path = get_google_input_tools_sandbox_from_options(options)[0]
  closure_library_path = get_closure_library_sandbox_from_options(options)[0]

  if not os.path.isdir(input_path):
    print 'Could not find google-input-tools sandbox.'
    exit(1)
  if not os.path.isdir(closure_library_path):
    print 'Could not find closure-library sandbox.'
    exit(1)

  (providers, requirements) = expand_directories([
      os.path.join(input_path, 'chrome'),
      closure_library_path])

  dependencies = set()
  for name in namespaces:
    extract_dependencies(name, providers, requirements, dependencies)

  target_files = []
  for name in dependencies:
    update_file(name, input_path, closure_library_path, target_files)

  generate_build_file(target_files)

  # Copy resources
  copy_dir(input_path, 'inputview/_locales')
  copy_dir(input_path, 'inputview/images')
  copy_dir(input_path, 'inputview/config')
  copy_dir(input_path, 'inputview/layouts')
  copy_dir(input_path, 'sounds')

  # Copy extra support files.
  for name in extras:
    source = os.path.join(input_path, 'chrome', 'os', 'inputview', name)
    target = os.path.join('src', 'chrome', 'os', 'inputview', name)
    copy_file(source, target)


if __name__ == '__main__':
  main()