mysqlx-connector-python/cpydist/__init__.py (372 lines of code) (raw):

# Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, as # published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an # additional permission to link the program and your derivative works # with the separately licensed software that they have either included with # the program or referenced in the documentation. # # Without limiting anything contained in the foregoing, this file, # which is part of MySQL Connector/Python, is also subject to the # Universal FOSS Exception, version 1.0, a copy of which can be found at # http://oss.oracle.com/licenses/universal-foss-exception. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """Connector/Python packaging system.""" import logging import os import platform import shutil import sys import tempfile from glob import glob from pathlib import Path from subprocess import PIPE, Popen, check_call from sysconfig import get_config_vars, get_python_version from setuptools import Command from setuptools.command.build_ext import build_ext from setuptools.command.install import install from setuptools.command.install_lib import install_lib try: from setuptools.logging import set_threshold except ImportError: set_threshold = None from .utils import ARCH, write_info_bin, write_info_src # Abseil libraries to link in the later stage ABSL_LIBS_EXT = "lib" if os.name == "nt" else "a" ABSL_LIBS = ( "absl_str_format_internal", "absl_strings", "absl_strings_internal", "absl_string_view", "absl_log_initialize", "absl_log_entry", "absl_log_flags", "absl_log_severity", "absl_log_internal_check_op", "absl_log_internal_conditions", "absl_log_internal_message", "absl_log_internal_nullguard", "absl_log_internal_proto", "absl_log_internal_format", "absl_log_internal_globals", "absl_log_internal_log_sink_set", "absl_log_sink", "absl_raw_logging_internal", "absl_log_globals", "utf8_validity", "absl_cord", "absl_cordz_info", "absl_cordz_handle", "absl_cordz_functions", "absl_cord_internal", "absl_crc_cord_state", "absl_crc32c", "absl_crc_cpu_detect", "absl_crc_internal", "absl_exponential_biased", "absl_synchronization", "absl_graphcycles_internal", "absl_kernel_timeout_internal", "absl_time", "absl_time_zone", "absl_int128", "absl_examine_stack", "absl_stacktrace", "absl_symbolize", "absl_demangle_internal", "absl_debugging_internal", "absl_malloc_internal", "absl_throw_delegate", "absl_strerror", "absl_raw_hash_set", "absl_hash", "absl_city", "absl_low_level_hash", "absl_base", "absl_spinlock_wait", "absl_status", "absl_statusor", "absl_bad_optional_access", ) # Load version information VERSION = [999, 0, 0, "a", 0] VERSION_TEXT = "999.0.0" VERSION_EXTRA = "" EDITION = "" version_py = os.path.join("lib", "mysqlx", "version.py") with open(version_py, "rb") as fp: exec(compile(fp.read(), version_py, "exec")) if "MACOSX_DEPLOYMENT_TARGET" in get_config_vars(): get_config_vars()["MACOSX_DEPLOYMENT_TARGET"] = "11.0" COMMON_USER_OPTIONS = [ ( "byte-code-only", None, "remove Python .py files; leave byte code .pyc only", ), ("edition=", None, "Edition added in the package name after the version"), ("label=", None, "label added in the package name after the name"), ("debug", None, "turn debugging on"), ( "keep-temp", "k", "keep the pseudo-installation tree around after creating the " "distribution archive", ), ] CEXT_OPTIONS = [ ( "with-protobuf-include-dir=", None, "location of Protobuf include directory", ), ("with-protobuf-lib-dir=", None, "location of Protobuf library directory"), ("with-protoc=", None, "location of Protobuf protoc binary"), ("extra-compile-args=", None, "extra compile args"), ("extra-link-args=", None, "extra link args"), ] LOGGER = logging.getLogger(__name__) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(levelname)s[%(name)s]: %(message)s")) LOGGER.addHandler(handler) LOGGER.setLevel(logging.WARNING) class BaseCommand(Command): """Base command class for Connector/Python.""" user_options = COMMON_USER_OPTIONS + CEXT_OPTIONS boolean_options = ["debug", "byte_code_only", "keep_temp"] with_mysqlxpb_cext = False with_protobuf_include_dir = None with_protobuf_lib_dir = None with_protoc = None extra_compile_args = None extra_link_args = None byte_code_only = False edition = None label = None debug = False keep_temp = False build_base = None log = LOGGER _mysql_info = {} _build_mysql_lib_dir = None _build_protobuf_lib_dir = None def initialize_options(self): """Initialize the options.""" self.with_mysqlxpb_cext = False self.with_protobuf_include_dir = None self.with_protobuf_lib_dir = None self.with_protoc = None self.extra_compile_args = None self.extra_link_args = None self.byte_code_only = False self.edition = None self.label = None self.debug = False self.keep_temp = False def finalize_options(self): """Finalize the options.""" if self.debug: self.log.setLevel(logging.DEBUG) if set_threshold: # Set setuptools logging level to DEBUG try: set_threshold(1) except AttributeError: pass if not self.with_protobuf_include_dir: self.with_protobuf_include_dir = os.environ.get("PROTOBUF_INCLUDE_DIR") if not self.with_protobuf_lib_dir: self.with_protobuf_lib_dir = os.environ.get("PROTOBUF_LIB_DIR") if not self.with_protoc: self.with_protoc = os.environ.get("PROTOC") if not self.extra_compile_args: self.extra_compile_args = os.environ.get("EXTRA_COMPILE_ARGS") if not self.extra_link_args: self.extra_link_args = os.environ.get("EXTRA_LINK_ARGS") cmd_build_ext = self.distribution.get_command_obj("build_ext") cmd_build_ext.with_protobuf_include_dir = self.with_protobuf_include_dir cmd_build_ext.with_protobuf_lib_dir = self.with_protobuf_lib_dir cmd_build_ext.with_protoc = self.with_protoc cmd_build_ext.extra_compile_args = self.extra_compile_args cmd_build_ext.extra_link_args = self.extra_link_args install = self.distribution.get_command_obj("install") install.with_protobuf_include_dir = self.with_protobuf_include_dir install.with_protobuf_lib_dir = self.with_protobuf_lib_dir install.with_protoc = self.with_protoc install.extra_compile_args = self.extra_compile_args install.extra_link_args = self.extra_link_args self.distribution.package_data = { "mysqlx": ["py.typed"], } def remove_temp(self): """Remove temporary build files.""" if not self.keep_temp: cmd_build = self.get_finalized_command("build") if not self.dry_run: shutil.rmtree(cmd_build.build_base) class BuildExt(build_ext, BaseCommand): """Command class for building the Connector/Python C Extensions.""" description = "build MySQL Connector/Python C extensions" user_options = build_ext.user_options + CEXT_OPTIONS boolean_options = build_ext.boolean_options + BaseCommand.boolean_options def _finalize_protobuf(self): if not self.with_protobuf_include_dir: self.with_protobuf_include_dir = os.environ.get( "MYSQLXPB_PROTOBUF_INCLUDE_DIR" ) if not self.with_protobuf_lib_dir: self.with_protobuf_lib_dir = os.environ.get("MYSQLXPB_PROTOBUF_LIB_DIR") if not self.with_protoc: self.with_protoc = os.environ.get("MYSQLXPB_PROTOC") if self.with_protobuf_include_dir: self.log.info( "Protobuf include directory: %s", self.with_protobuf_include_dir, ) if not os.path.isdir(self.with_protobuf_include_dir): self.log.error("Protobuf include dir should be a directory") sys.exit(1) else: self.log.error("Unable to find Protobuf include directory") sys.exit(1) if self.with_protobuf_lib_dir: self.log.info("Protobuf library directory: %s", self.with_protobuf_lib_dir) if not os.path.isdir(self.with_protobuf_lib_dir): self.log.error("Protobuf library dir should be a directory") sys.exit(1) else: self.log.error("Unable to find Protobuf library directory") sys.exit(1) if self.with_protoc: self.log.info("Protobuf protoc binary: %s", self.with_protoc) if not os.path.isfile(self.with_protoc): self.log.error("Protobuf protoc binary is not valid") sys.exit(1) else: self.log.error("Unable to find Protobuf protoc binary") sys.exit(1) if not os.path.exists(self._build_protobuf_lib_dir): os.makedirs(self._build_protobuf_lib_dir) self.log.info("Copying Protobuf libraries") # load protobuf-related static libraries libs = glob(os.path.join(self.with_protobuf_lib_dir, "libprotobuf*")) # load absl-related static libraries libs += glob( os.path.join(self.with_protobuf_lib_dir, f"*absl_*.{ABSL_LIBS_EXT}") ) libs += glob( os.path.join(self.with_protobuf_lib_dir, f"*utf8_*.{ABSL_LIBS_EXT}") ) for lib in libs: if os.path.isfile(lib): self.log.info("copying %s -> %s", lib, self._build_protobuf_lib_dir) shutil.copy2(lib, self._build_protobuf_lib_dir) # Remove all but static libraries to force static linking if os.name == "posix": self.log.info( "Removing non-static Protobuf libraries from %s", self._build_protobuf_lib_dir, ) for lib in os.listdir(self._build_protobuf_lib_dir): lib_path = os.path.join(self._build_protobuf_lib_dir, lib) if os.path.isfile(lib_path) and not lib.endswith((".a", ".dylib")): os.unlink(os.path.join(self._build_protobuf_lib_dir, lib)) def _run_protoc(self): base_path = os.path.join(os.getcwd(), "src", "mysqlxpb", "mysqlx") command = [self.with_protoc, "-I"] command.append(os.path.join(base_path, "protocol")) command.extend(glob(os.path.join(base_path, "protocol", "*.proto"))) command.append(f"--cpp_out={base_path}") self.log.info("Running protoc command: %s", " ".join(command)) check_call(command) def initialize_options(self): """Initialize the options.""" build_ext.initialize_options(self) BaseCommand.initialize_options(self) def finalize_options(self): """Finalize the options.""" build_ext.finalize_options(self) BaseCommand.finalize_options(self) self.log.info("Python architecture: %s", ARCH) self._build_mysql_lib_dir = os.path.join(self.build_temp, "capi", "lib") self._build_protobuf_lib_dir = os.path.join(self.build_temp, "protobuf", "lib") self.with_mysqlxpb_cext = any( ( self.with_protobuf_include_dir, self.with_protobuf_lib_dir, self.with_protoc, ) ) if self.with_mysqlxpb_cext: self._finalize_protobuf() def run(self): """Run the command.""" # Generate docs/INFO_SRC write_info_src(VERSION_TEXT) disabled = [] # Extensions to be disabled for ext in self.extensions: # Add Protobuf include and library dirs if not self.with_mysqlxpb_cext: self.log.warning("The '_mysqlxpb' C extension will not be built") disabled.append(ext) continue if platform.system() == "Darwin": symbol_file = tempfile.NamedTemporaryFile() ext.extra_link_args.extend(["-exported_symbols_list", symbol_file.name]) with open(symbol_file.name, "w") as fp: fp.write("_PyInit__mysqlxpb") fp.write("\n") ext.include_dirs.append(self.with_protobuf_include_dir) ext.library_dirs.append(self._build_protobuf_lib_dir) ext.libraries.append("libprotobuf" if os.name == "nt" else "protobuf") ext.libraries.extend(ABSL_LIBS) if os.name != "nt": # Add -std=c++14 needed for Protobuf 4.25.3 ext.extra_compile_args.append("-std=c++14") self._run_protoc() # Suppress unknown pragmas if os.name == "posix": ext.extra_compile_args.append("-Wno-unknown-pragmas") if os.name != "nt": is_macos = platform.system() == "Darwin" cc = os.environ.get("CC", "clang" if is_macos else "gcc") cxx = os.environ.get("CXX", "clang++" if is_macos else "g++") cmd_cc_ver = [cc, "-v"] self.log.info("Executing: %s", " ".join(cmd_cc_ver)) proc = Popen(cmd_cc_ver, stdout=PIPE, universal_newlines=True) self.log.info(proc.communicate()) cmd_cxx_ver = [cxx, "-v"] self.log.info("Executing: %s", " ".join(cmd_cxx_ver)) proc = Popen(cmd_cxx_ver, stdout=PIPE, universal_newlines=True) self.log.info(proc.communicate()) # Remove disabled extensions for ext in disabled: self.extensions.remove(ext) build_ext.run(self) # Generate docs/INFO_BIN if self.with_mysqlxpb_cext: mysql_version = "N/A" compiler = ( self.compiler.compiler_so[0] if hasattr(self.compiler, "compiler_so") else None ) write_info_bin(mysql_version, compiler) class InstallLib(install_lib): """InstallLib Connector/Python implementation.""" user_options = install_lib.user_options + [ ( "byte-code-only", None, "remove Python .py files; leave byte code .pyc only", ), ] boolean_options = ["byte-code-only"] log = LOGGER def initialize_options(self): """Initialize the options.""" install_lib.initialize_options(self) self.byte_code_only = False def finalize_options(self): """Finalize the options.""" install_lib.finalize_options(self) self.set_undefined_options("install", ("byte_code_only", "byte_code_only")) self.set_undefined_options("build", ("build_base", "build_dir")) def run(self): """Run the command.""" if not os.path.exists(self.build_dir): Path(self.build_dir).mkdir(parents=True, exist_ok=True) self.build() outfiles = self.install() # (Optionally) compile .py to .pyc if outfiles is not None and self.distribution.has_pure_modules(): self.byte_compile(outfiles) if self.byte_code_only: if get_python_version().startswith("3"): for base, _, files in os.walk(self.install_dir): for filename in files: if filename.endswith(".pyc"): new_name = f"{filename.split('.')[0]}.pyc" os.rename( os.path.join(base, filename), os.path.join(base, "..", new_name), ) if base.endswith("__pycache__"): os.rmdir(base) for source_file in outfiles: if source_file.endswith(".py"): self.log.info("Removing %s", source_file) os.remove(source_file) class Install(install, BaseCommand): """Install Connector/Python implementation.""" description = "install MySQL Connector/Python" user_options = install.user_options + BaseCommand.user_options boolean_options = install.boolean_options + BaseCommand.boolean_options def initialize_options(self): """Initialize the options.""" install.initialize_options(self) BaseCommand.initialize_options(self) def finalize_options(self): """Finalize the options.""" BaseCommand.finalize_options(self) install.finalize_options(self) cmd_install_lib = self.distribution.get_command_obj("install_lib") cmd_install_lib.byte_code_only = self.byte_code_only