chromium/components/resources/protobufs/binary_proto_generator.py

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

"""
 Converts a given ASCII proto into a binary resource.

"""
from __future__ import print_function
import abc
from importlib import util as imp_util
import optparse
import os
import re
import subprocess
import sys
import traceback


class GoogleProtobufModuleImporter:
  """A custom module importer for importing google.protobuf.

  See PEP #302 (https://www.python.org/dev/peps/pep-0302/) for full information
  on the Importer Protocol.
  """

  def __init__(self, paths):
    """Creates a loader that searches |paths| for google.protobuf modules."""
    self._paths = paths

  def _fullname_to_filepath(self, fullname):
    """Converts a full module name to a corresponding path to a .py file.

    e.g. google.protobuf.text_format -> pyproto/google/protobuf/text_format.py
    """
    for path in self._paths:
      filepath = os.path.join(path, fullname.replace('.', os.sep) + '.py')
      if os.path.isfile(filepath):
        return filepath
    return None

  def _module_exists(self, fullname):
    return self._fullname_to_filepath(fullname) is not None

  def find_module(self, fullname, path=None):
    """Returns a loader module for the google.protobuf module in pyproto."""
    if (fullname.startswith('google.protobuf.')
        and self._module_exists(fullname)):
      # Per PEP #302, this will result in self.load_module getting used
      # to load |fullname|.
      return self

    # Per PEP #302, if the module cannot be loaded, then return None.
    return None

  def load_module(self, fullname):
    """Loads the module specified by |fullname| and returns the module."""
    if fullname in sys.modules:
      # Per PEP #302, if |fullname| is in sys.modules, it must be returned.
      return sys.modules[fullname]

    if (not fullname.startswith('google.protobuf.') or
        not self._module_exists(fullname)):
      # Per PEP #302, raise ImportError if the requested module/package
      # cannot be loaded. This should never get reached for this simple loader,
      # but is included for completeness.
      raise ImportError(fullname)

    filepath = self._fullname_to_filepath(fullname)
    spec = imp_util.spec_from_file_location(fullname, filepath)
    loaded = imp_util.module_from_spec(spec)
    spec.loader.exec_module(loaded)

    return loaded

class BinaryProtoGenerator:

  # If the script is run in a virtualenv
  # (https://virtualenv.pypa.io/en/stable/), then no google.protobuf library
  # should be brought in from site-packages. Passing -S into the interpreter in
  # a virtualenv actually destroys the ability to import standard library
  # functions like optparse, so this script should not be wrapped if we're in a
  # virtualenv.
  def _IsInVirtualEnv(self):
    # This is the way used by pip and other software to detect virtualenv.
    return hasattr(sys, 'real_prefix')

  def _ImportProtoModules(self, paths):
    """Import the protobuf modules we need. |paths| is list of import paths"""
    for path in paths:
      # Put the path to our proto libraries in front, so that we don't use
      # system protobuf.
      sys.path.insert(1, path)

    if self._IsInVirtualEnv():
      # Add a custom module loader. When run in a virtualenv that has
      # google.protobuf installed, the site-package was getting searched first
      # despite that pyproto/ is at the start of the sys.path. The module
      # loaders in the meta_path precede all other imports (including even
      # builtins), which allows the proper google.protobuf from pyproto to be
      # found.
      sys.meta_path.append(GoogleProtobufModuleImporter(paths))

    import google.protobuf.text_format as text_format
    globals()['text_format'] = text_format
    self.ImportProtoModule()

  def _GenerateBinaryProtos(self, opts):
    """ Read the ASCII proto and generate one or more binary protos. """
    # Read the ASCII
    with open(opts.infile, 'r') as ifile:
      ascii_pb_str = ifile.read()

    # Parse it into a structured PB
    full_pb = self.EmptyProtoInstance()
    text_format.Merge(ascii_pb_str, full_pb)

    self.ValidatePb(opts, full_pb);
    self.ProcessPb(opts, full_pb)

  @abc.abstractmethod
  def ImportProtoModule(self):
    """ Import the proto module to be used by the generator. """
    pass

  @abc.abstractmethod
  def EmptyProtoInstance(self):
    """ Returns an empty proto instance to be filled by the generator."""
    pass

  @abc.abstractmethod
  def ValidatePb(self, opts, pb):
    """ Validate the basic values of the protobuf.  The
        file_type_policies_unittest.cc will also validate it by platform,
        but this will catch errors earlier.
    """
    pass

  @abc.abstractmethod
  def ProcessPb(self, opts, pb):
    """ Process the parsed prototobuf. """
    pass

  def AddCommandLineOptions(self, parser):
    """ Allows subclasses to add any options the command line parser. """
    pass

  def AddExtraCommandLineArgsForVirtualEnvRun(self, opts, command):
    """ Allows subclasses to add any extra command line arguments when running
        this under a virtualenv."""
    pass

  def VerifyArgs(self, opts):
    """ Allows subclasses to check command line parameters before running. """
    return True

  def Run(self):
    parser = optparse.OptionParser()
    # TODO(crbug.com/41255210): Remove this once the bug is fixed.
    parser.add_option('-w', '--wrap', action="store_true", default=False,
                      help='Wrap this script in another python '
                      'execution to disable site-packages.  This is a '
                      'fix for http://crbug.com/605592')

    parser.add_option('-i', '--infile',
                      help='The ASCII proto file to read.')
    parser.add_option('-d', '--outdir',
                      help='Directory underwhich binary file(s) will be ' +
                           'written')
    parser.add_option('-o', '--outbasename',
                      help='Basename of the binary file to write to.')
    parser.add_option('-p', '--path', action="append",
                      help='Repeat this as needed.  Directory(s) containing ' +
                      'the your_proto_definition_pb2.py and ' +
                      'google.protobuf.text_format modules')
    self.AddCommandLineOptions(parser)

    (opts, args) = parser.parse_args()
    if opts.infile is None or opts.outdir is None or opts.outbasename is None:
      parser.print_help()
      return 1

    if opts.wrap and not self._IsInVirtualEnv():
      # Run this script again with different args to the interpreter to suppress
      # the inclusion of libraries, like google.protobuf, from site-packages,
      # which is checked before sys.path when resolving imports. We want to
      # specifically import the libraries injected into the sys.path in
      # ImportProtoModules().
      command = [sys.executable, '-S', '-s', sys.argv[0]]
      command += ['-i', opts.infile]
      command += ['-d', opts.outdir]
      command += ['-o', opts.outbasename]
      for path in opts.path:
        command += ['-p', path]

      self.AddExtraCommandLineArgsForVirtualEnvRun(opts, command);
      sys.exit(subprocess.call(command))

    self._ImportProtoModules(opts.path)

    if not self.VerifyArgs(opts):
      print("Wrong arguments")
      return 1

    try:
      self._GenerateBinaryProtos(opts)
    except Exception as e:
      print("ERROR: Failed to render binary version of %s:\n  %s\n%s" %
            (opts.infile, str(e), traceback.format_exc()))
      return 1