################################################################################ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ from __future__ import absolute_import from __future__ import print_function import glob import logging import multiprocessing import os import platform import shutil import subprocess import sys import time import warnings import pkg_resources GRPC_TOOLS = 'grpcio-tools>=1.29.0,<=1.50.0' PROTO_PATHS = ['proto'] PYFLINK_ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) DEFAULT_PYTHON_OUTPUT_PATH = os.path.join(PYFLINK_ROOT_PATH, 'fn_execution') def generate_proto_files(force=True, output_dir=DEFAULT_PYTHON_OUTPUT_PATH): try: import grpc_tools # noqa # pylint: disable=unused-import except ImportError: warnings.warn('Installing grpcio-tools is recommended for development.') proto_dirs = [os.path.join(PYFLINK_ROOT_PATH, path) for path in PROTO_PATHS] proto_files = sum( [glob.glob(os.path.join(d, '*.proto')) for d in proto_dirs], []) out_dir = os.path.join(PYFLINK_ROOT_PATH, output_dir) out_files = [path for path in glob.glob(os.path.join(out_dir, '*_pb2.py*'))] if out_files and not proto_files and not force: # We have out_files but no protos; assume they're up to date. # This is actually the common case (e.g. installation from an sdist). logging.info('No proto files; using existing generated files.') return elif not out_files and not proto_files: raise RuntimeError( 'No proto files found in %s.' % proto_dirs) # Regenerate iff the proto files or this file are newer. elif force or not out_files or len(out_files) < len(proto_files) or ( min(os.path.getmtime(path) for path in out_files) <= max(os.path.getmtime(path) for path in proto_files + [os.path.realpath(__file__)])): try: from grpc_tools import protoc except ImportError: if platform.system() == 'Windows': # For Windows, grpcio-tools has to be installed manually. raise RuntimeError( 'Cannot generate protos for Windows since grpcio-tools package is ' 'not installed. Please install this package manually ' 'using \'pip install "grpcio-tools>=1.29.0,<=1.50.0"\'.') # Use a subprocess to avoid messing with this process' path and imports. # Note that this requires a separate module from setup.py for Windows: # https://docs.python.org/2/library/multiprocessing.html#windows p = multiprocessing.Process( target=_install_grpcio_tools_and_generate_proto_files(force, output_dir)) p.start() p.join() if p.exitcode: raise ValueError("Proto generation failed (see log for details).") else: _check_grpcio_tools_version() logging.info('Regenerating out-of-date Python proto definitions.') builtin_protos = pkg_resources.resource_filename('grpc_tools', '_proto') args = ( [sys.executable] + # expecting to be called from command line ['--proto_path=%s' % builtin_protos] + ['--proto_path=%s' % d for d in proto_dirs] + ['--python_out=%s' % out_dir] + ['--pyi_out=%s' % out_dir] + proto_files) ret_code = protoc.main(args) if ret_code: raise RuntimeError( 'Protoc returned non-zero status (see logs for details): ' '%s' % ret_code) for output_file in os.listdir(output_dir): if output_file.endswith('_pb2.py') or output_file.endswith('_pb2.pyi'): _add_license_header(output_dir, output_file) # Though wheels are available for grpcio-tools, setup_requires uses # easy_install which doesn't understand them. This means that it is # compiled from scratch (which is expensive as it compiles the full # protoc compiler). Instead, we attempt to install a wheel in a temporary # directory and add it to the path as needed. # See https://github.com/pypa/setuptools/issues/377 def _install_grpcio_tools_and_generate_proto_files(force, output_dir): install_path = os.path.join(PYFLINK_ROOT_PATH, '..', '.eggs', 'grpcio-wheels') build_path = install_path + '-build' if os.path.exists(build_path): shutil.rmtree(build_path) logging.warning('Installing grpcio-tools into %s', install_path) try: start = time.time() # since '--prefix' option is only supported for pip 8.0+, so here we fallback to # use '--install-option' when the pip version is lower than 8.0.0. pip_version = pkg_resources.get_distribution("pip").version from pkg_resources import parse_version if parse_version(pip_version) >= parse_version('8.0.0'): subprocess.check_call( [sys.executable, '-m', 'pip', 'install', '--prefix', install_path, '--build', build_path, '--upgrade', GRPC_TOOLS, "-I"]) else: subprocess.check_call( [sys.executable, '-m', 'pip', 'install', '--install-option', '--prefix=' + install_path, '--build', build_path, '--upgrade', GRPC_TOOLS, "-I"]) from distutils.dist import Distribution install_obj = Distribution().get_command_obj('install', create=True) install_obj.prefix = install_path install_obj.finalize_options() logging.warning( 'Installing grpcio-tools took %0.2f seconds.', time.time() - start) finally: sys.stderr.flush() shutil.rmtree(build_path, ignore_errors=True) sys.path.append(install_obj.install_purelib) pkg_resources.working_set.add_entry(install_obj.install_purelib) if install_obj.install_purelib != install_obj.install_platlib: sys.path.append(install_obj.install_platlib) pkg_resources.working_set.add_entry(install_obj.install_platlib) try: generate_proto_files(force, output_dir) finally: sys.stderr.flush() def _add_license_header(dir, file_name): with open(os.path.join(dir, file_name), 'r') as original_file: original_data = original_file.read() tmp_file_name = file_name + '.tmp' with open(os.path.join(dir, tmp_file_name), 'w') as tmp_file: tmp_file.write( '################################################################################\n' '# Licensed to the Apache Software Foundation (ASF) under one\n' '# or more contributor license agreements. See the NOTICE file\n' '# distributed with this work for additional information\n' '# regarding copyright ownership. The ASF licenses this file\n' '# to you under the Apache License, Version 2.0 (the\n' '# "License"); you may not use this file except in compliance\n' '# with the License. You may obtain a copy of the License at\n' '#\n' '# http://www.apache.org/licenses/LICENSE-2.0\n' '#\n' '# Unless required by applicable law or agreed to in writing, software\n' '# distributed under the License is distributed on an "AS IS" BASIS,\n' '# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' '# See the License for the specific language governing permissions and\n' '# limitations under the License.\n' '################################################################################\n' ) tmp_file.write(original_data) if os.path.exists(os.path.join(dir, file_name)): os.remove(os.path.join(dir, file_name)) os.rename(os.path.join(dir, tmp_file_name), os.path.join(dir, file_name)) def _check_grpcio_tools_version(): version = pkg_resources.get_distribution("grpcio-tools").parsed_version from pkg_resources import parse_version if version < parse_version('1.29.0') or version > parse_version('1.50.0'): raise RuntimeError( "Version of grpcio-tools must be between 1.29.0 and 1.50.0, got %s" % version) if __name__ == '__main__': generate_proto_files()