1# Copyright (c) 2020, 2021, Oracle and/or its affiliates.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License, version 2.0, as
5# published by the Free Software Foundation.
6#
7# This program is also distributed with certain software (including
8# but not limited to OpenSSL) that is licensed under separate terms,
9# as designated in a particular file or component or in included license
10# documentation.  The authors of MySQL hereby grant you an
11# additional permission to link the program and your derivative works
12# with the separately licensed software that they have included with
13# MySQL.
14#
15# Without limiting anything contained in the foregoing, this file,
16# which is part of MySQL Connector/Python, is also subject to the
17# Universal FOSS Exception, version 1.0, a copy of which can be found at
18# http://oss.oracle.com/licenses/universal-foss-exception.
19#
20# This program is distributed in the hope that it will be useful, but
21# WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
23# See the GNU General Public License, version 2.0, for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program; if not, write to the Free Software Foundation, Inc.,
27# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
28
29"""Connector/Python packaging system."""
30
31import logging
32import os
33import platform
34import shutil
35import sys
36import tempfile
37
38from glob import glob
39from setuptools.command.build_ext import build_ext
40from distutils import log
41from distutils.command.install import install
42from distutils.command.install_lib import install_lib
43from distutils.core import Command
44from distutils.dir_util import mkpath, remove_tree
45from distutils.sysconfig import get_config_vars, get_python_version
46from distutils.version import LooseVersion
47from subprocess import check_call, Popen, PIPE
48
49from .utils import (ARCH, ARCH_64BIT, mysql_c_api_info, write_info_src,
50                    write_info_bin)
51
52
53# Load version information
54VERSION = [999, 0, 0, "a", 0]
55VERSION_TEXT = "999.0.0"
56VERSION_EXTRA = ""
57EDITION = ""
58version_py = os.path.join("lib", "mysql", "connector", "version.py")
59with open(version_py, "rb") as fp:
60    exec(compile(fp.read(), version_py, "exec"))
61
62if "MACOSX_DEPLOYMENT_TARGET" in get_config_vars():
63    get_config_vars()["MACOSX_DEPLOYMENT_TARGET"] = "11.0"
64
65COMMON_USER_OPTIONS = [
66    ("byte-code-only", None,
67     "remove Python .py files; leave byte code .pyc only"),
68    ("edition=", None,
69     "Edition added in the package name after the version"),
70    ("label=", None,
71     "label added in the package name after the name"),
72    ("debug", None,
73     "turn debugging on"),
74    ("keep-temp", "k",
75     "keep the pseudo-installation tree around after creating the "
76     "distribution archive"),
77]
78
79CEXT_OPTIONS = [
80    ("with-mysql-capi=", None,
81     "location of MySQL C API installation or path to mysql_config"),
82    ("with-openssl-include-dir=", None,
83     "location of OpenSSL include directory"),
84    ("with-openssl-lib-dir=", None,
85     "location of OpenSSL library directory"),
86    ("with-protobuf-include-dir=", None,
87     "location of Protobuf include directory"),
88    ("with-protobuf-lib-dir=", None,
89     "location of Protobuf library directory"),
90    ("with-protoc=", None,
91     "location of Protobuf protoc binary"),
92    ("extra-compile-args=", None,
93     "extra compile args"),
94    ("extra-link-args=", None,
95     "extra link args")
96]
97
98LOGGER = logging.getLogger(__name__)
99handler = logging.StreamHandler()
100handler.setFormatter(
101    logging.Formatter("%(levelname)s[%(name)s]: %(message)s")
102)
103LOGGER.addHandler(handler)
104LOGGER.setLevel(logging.WARNING)
105
106
107class BaseCommand(Command):
108    """Base command class for Connector/Python."""
109
110    user_options = COMMON_USER_OPTIONS + CEXT_OPTIONS
111    boolean_options = ["debug", "byte_code_only", "keep-temp"]
112
113    with_mysql_capi = None
114    with_mysqlxpb_cext = False
115
116    with_openssl_include_dir = None
117    with_openssl_lib_dir = None
118
119    with_protobuf_include_dir = None
120    with_protobuf_lib_dir = None
121    with_protoc = None
122
123    extra_compile_args = None
124    extra_link_args = None
125
126    byte_code_only = False
127    edition = None
128    label = None
129    debug = False
130    keep_temp = False
131    build_base = None
132    log = LOGGER
133    vendor_folder = os.path.join("lib", "mysql", "vendor")
134
135    _mysql_info = {}
136    _build_mysql_lib_dir = None
137    _build_protobuf_lib_dir = None
138
139    def initialize_options(self):
140        """Initialize the options."""
141        self.with_mysql_capi = None
142        self.with_mysqlxpb_cext = False
143        self.with_openssl_include_dir = None
144        self.with_openssl_lib_dir = None
145        self.with_protobuf_include_dir = None
146        self.with_protobuf_lib_dir = None
147        self.with_protoc = None
148        self.extra_compile_args = None
149        self.extra_link_args = None
150        self.byte_code_only = False
151        self.edition = None
152        self.label = None
153        self.debug = False
154        self.keep_temp = False
155
156    def finalize_options(self):
157        """Finalize the options."""
158        if self.debug:
159            self.log.setLevel(logging.DEBUG)
160            log.set_threshold(1)  # Set Distutils logging level to DEBUG
161
162        cmd_build_ext = self.distribution.get_command_obj("build_ext")
163        cmd_build_ext.with_mysql_capi = (
164            self.with_mysql_capi or
165            os.environ.get("MYSQL_CAPI")
166        )
167        cmd_build_ext.with_openssl_include_dir = (
168            self.with_openssl_include_dir or
169            os.environ.get("OPENSSL_INCLUDE_DIR")
170        )
171        cmd_build_ext.with_openssl_lib_dir = (
172            self.with_openssl_lib_dir or
173            os.environ.get("OPENSSL_LIB_DIR")
174        )
175        cmd_build_ext.with_protobuf_include_dir = (
176            self.with_protobuf_include_dir or
177            os.environ.get("PROTOBUF_INCLUDE_DIR")
178        )
179        cmd_build_ext.with_protobuf_lib_dir = (
180            self.with_protobuf_lib_dir or
181            os.environ.get("PROTOBUF_LIB_DIR")
182        )
183        cmd_build_ext.with_protoc = (
184            self.with_protoc or
185            os.environ.get("PROTOC")
186        )
187        cmd_build_ext.extra_compile_args = (
188            self.extra_compile_args or
189            os.environ.get("EXTRA_COMPILE_ARGS")
190        )
191        cmd_build_ext.extra_link_args = (
192            self.extra_link_args or
193            os.environ.get("EXTRA_LINK_ARGS")
194        )
195        self._copy_vendor_libraries()
196
197    def remove_temp(self):
198        """Remove temporary build files."""
199        if not self.keep_temp:
200            cmd_build = self.get_finalized_command("build")
201            remove_tree(cmd_build.build_base, dry_run=self.dry_run)
202            vendor_folder = os.path.join(os.getcwd(), self.vendor_folder)
203            if os.path.exists(vendor_folder):
204                remove_tree(vendor_folder)
205            elif os.name == "nt":
206                if ARCH == "64-bit":
207                    libraries = ["libmysql.dll", "libssl-1_1-x64.dll",
208                                 "libcrypto-1_1-x64.dll"]
209                else:
210                    libraries = ["libmysql.dll", "libssl-1_1.dll",
211                                 "libcrypto-1_1.dll"]
212                for filename in libraries:
213                    dll_file = os.path.join(os.getcwd(), filename)
214                    if os.path.exists(dll_file):
215                        os.unlink(dll_file)
216
217    def _get_openssl_libs(self):
218        libssl = glob(os.path.join(
219            self.with_openssl_lib_dir, "libssl.*.*.*"))
220        libcrypto = glob(os.path.join(
221            self.with_openssl_lib_dir, "libcrypto.*.*.*"))
222        if not libssl or not libcrypto:
223            self.log.error("Unable to find OpenSSL libraries in '%s'",
224                           self.with_openssl_lib_dir)
225            sys.exit(1)
226        return (os.path.basename(libssl[0]), os.path.basename(libcrypto[0]))
227
228    def _copy_vendor_libraries(self):
229        vendor_libs = []
230
231        if os.name == "posix":
232            # Bundle OpenSSL libs
233            if self.with_openssl_lib_dir:
234                libssl, libcrypto = self._get_openssl_libs()
235                vendor_libs.append(
236                    (self.with_openssl_lib_dir, [libssl, libcrypto]))
237
238        # Plugins
239        bundle_plugin_libs = False
240        if self.with_mysql_capi:
241            plugin_ext = "dll" if os.name == "nt" else "so"
242            plugin_path = os.path.join(self.with_mysql_capi, "lib", "plugin")
243
244            # authentication_ldap_sasl_client
245            plugin_name = (
246                "authentication_ldap_sasl_client.{}".format(plugin_ext)
247            )
248            plugin_full_path = os.path.join(plugin_path, plugin_name)
249            self.log.debug("ldap plugin_path: '%s'", plugin_full_path)
250            if os.path.exists(plugin_full_path):
251                bundle_plugin_libs = True
252                vendor_libs.append(
253                    (plugin_path, [os.path.join("plugin", plugin_name)])
254                )
255
256            # authentication_kerberos_client
257            plugin_name = (
258                "authentication_kerberos_client.{}".format(plugin_ext)
259            )
260            plugin_full_path = os.path.join(plugin_path, plugin_name)
261            self.log.debug("kerberos plugin_path: '%s'", plugin_full_path)
262            if os.path.exists(plugin_full_path):
263                bundle_plugin_libs = True
264                vendor_libs.append(
265                    (plugin_path, [os.path.join("plugin", plugin_name)])
266                )
267
268            # authentication_oci_client
269            plugin_name = (
270                "authentication_oci_client.{}".format(plugin_ext)
271            )
272            plugin_full_path = os.path.join(plugin_path, plugin_name)
273            self.log.debug("OCI IAM plugin_path: '%s'", plugin_full_path)
274            if os.path.exists(plugin_full_path):
275                bundle_plugin_libs = True
276                vendor_libs.append(
277                    (plugin_path, [os.path.join("plugin", plugin_name)])
278                )
279
280            # vendor libraries
281            if bundle_plugin_libs and os.name == "nt":
282                plugin_libs = []
283                libs_path = os.path.join(self.with_mysql_capi, "bin")
284                for lib_name in ["libsasl.dll", "saslSCRAM.dll"]:
285                    if os.path.exists(os.path.join(libs_path, lib_name)):
286                        plugin_libs.append(lib_name)
287                if plugin_libs:
288                    vendor_libs.append((libs_path, plugin_libs))
289
290                if ARCH_64BIT:
291                    openssl_libs = ["libssl-1_1-x64.dll",
292                                    "libcrypto-1_1-x64.dll"]
293                else:
294                    openssl_libs = ["libssl-1_1.dll", "libcrypto-1_1.dll"]
295                if self.with_openssl_lib_dir:
296                    openssl_libs_path = os.path.abspath(self.with_openssl_lib_dir)
297                    if os.path.basename(openssl_libs_path) == "lib":
298                        openssl_libs_path = os.path.split(openssl_libs_path)[0]
299                    if os.path.exists(openssl_libs_path) and \
300                       os.path.exists(os.path.join(openssl_libs_path, "bin")):
301                        openssl_libs_path = os.path.join(openssl_libs_path, "bin")
302                    self.log.info("# openssl_libs_path: %s", openssl_libs_path)
303                else:
304                    openssl_libs_path = os.path.join(
305                        self.with_mysql_capi, "bin")
306                vendor_libs.append((openssl_libs_path, openssl_libs))
307
308        if not vendor_libs:
309            return
310
311        self.log.debug("# vendor_libs: %s", vendor_libs)
312
313        # mysql/vendor
314        if not os.path.exists(self.vendor_folder):
315            mkpath(os.path.join(os.getcwd(), self.vendor_folder))
316
317        # mysql/vendor/plugin
318        if not os.path.exists(os.path.join(self.vendor_folder, "plugin")):
319            mkpath(os.path.join(os.getcwd(), self.vendor_folder, "plugin"))
320
321        # mysql/vendor/private
322        if not os.path.exists(os.path.join(self.vendor_folder, "private")):
323            mkpath(os.path.join(os.getcwd(), self.vendor_folder, "private"))
324
325        # Copy vendor libraries to 'mysql/vendor' folder
326        self.log.info("Copying vendor libraries")
327        for src_folder, files in vendor_libs:
328            self.log.info("Copying folder: %s", src_folder)
329            for filepath in files:
330                dst_folder, filename = os.path.split(filepath)
331                src = os.path.join(src_folder, filename)
332                dst = os.path.join(os.getcwd(), self.vendor_folder, dst_folder)
333                self.log.info("copying %s -> %s", src, dst)
334                self.log.info("shutil res: %s", shutil.copy(src, dst))
335
336        if os.name == "nt":
337            self.distribution.package_data = {"mysql": ["vendor/plugin/*"]}
338            site_packages_files = [
339                os.path.join(openssl_libs_path, lib_n) for lib_n in openssl_libs
340            ]
341            site_packages_files.append(
342                os.path.join(self.with_mysql_capi, "lib", "libmysql.dll"))
343            self.distribution.data_files = [(
344                'lib\\site-packages\\', site_packages_files
345            )]
346            self.log.debug("# site_packages_files: %s",
347                           self.distribution.data_files)
348        elif bundle_plugin_libs:
349            # Bundle SASL libs
350            sasl_libs_path = os.path.join(self.with_mysql_capi, "lib",
351                                          "private")
352            if not os.path.exists(sasl_libs_path):
353                self.log.info("sasl2 llibraries not found at %s",
354                              sasl_libs_path)
355                self.distribution.package_data = {
356                    "mysql": ["vendor/*"]}
357            sasl_libs = []
358            sasl_plugin_libs_w = [
359                "libsasl2.*.*", "libgssapi_krb5.*.*", "libgssapi_krb5.*.*",
360                "libkrb5.*.*", "libk5crypto.*.*", "libkrb5support.*.*",
361                "libcrypto.*.*.*", "libssl.*.*.*", "libcom_err.*.*"]
362            sasl_plugin_libs = []
363            for sasl_lib in sasl_plugin_libs_w:
364                lib_path_entries = glob(os.path.join(
365                    sasl_libs_path, sasl_lib))
366                for lib_path_entry in lib_path_entries:
367                    sasl_plugin_libs.append(os.path.basename(lib_path_entry))
368            sasl_libs.append((sasl_libs_path, sasl_plugin_libs))
369
370            # Copy vendor libraries to 'mysql/vendor/private' folder
371            self.log.info("Copying vendor libraries")
372            for src_folder, files in sasl_libs:
373                self.log.info("Copying folder: %s", src_folder)
374                for filename in files:
375                    src = os.path.join(src_folder, filename)
376                    if not os.path.exists(src):
377                        self.log.warn("Library not found: %s", src)
378                        continue
379                    dst = os.path.join(
380                        os.getcwd(),
381                        self.vendor_folder,
382                        "private"
383                    )
384                    self.log.info("copying %s -> %s", src, dst)
385                    shutil.copy(src, dst)
386
387            # include sasl2 libs
388            sasl2_libs = []
389            sasl2_libs_path = os.path.join(self.with_mysql_capi, "lib",
390                                           "private", "sasl2")
391            if not os.path.exists(sasl2_libs_path):
392                self.log.info("sasl2 llibraries not found at %s",
393                              sasl2_libs_path)
394                self.distribution.package_data = {"mysql": ["vendor/*"]}
395                return
396            sasl2_libs_w = [
397                "libanonymous.*", "libcrammd5.*.*", "libdigestmd5.*.*.*.*",
398                "libgssapiv2.*", "libplain.*.*", "libscram.*.*.*.*",
399                "libanonymous.*.*", "libcrammd5.*.*.*.*", "libgs2.*",
400                "libgssapiv2.*.*", "libplain.*.*.*.*", "libanonymous.*.*.*.*",
401                "libdigestmd5.*", "libgs2.*.*", "libgssapiv2.*.*.*.*",
402                "libscram.*", "libcrammd5.*", "libdigestmd5.*.*",
403                "libgs2.*.*.*.*", "libplain.*", "libscram.*.*"]
404
405            sasl2_scram_libs = []
406            for sasl2_lib in sasl2_libs_w:
407                lib_path_entries = glob(os.path.join(
408                    sasl2_libs_path, sasl2_lib))
409                for lib_path_entry in lib_path_entries:
410                    sasl2_scram_libs.append(os.path.basename(lib_path_entry))
411
412            sasl2_libs.append((sasl2_libs_path, sasl2_scram_libs))
413
414            sasl2_libs_private_path = os.path.join(
415                self.vendor_folder, "private", "sasl2"
416            )
417            if not os.path.exists(sasl2_libs_private_path):
418                mkpath(sasl2_libs_private_path)
419
420            # Copy vendor libraries to 'mysql/vendor/private/sasl2' folder
421            self.log.info("Copying vendor libraries")
422            for src_folder, files in sasl2_libs:
423                self.log.info("Copying folder: %s", src_folder)
424                for filename in files:
425                    src = os.path.join(src_folder, filename)
426                    if not os.path.exists(src):
427                        self.log.warning("Library not found: %s", src)
428                        continue
429                    dst = os.path.join(os.getcwd(), sasl2_libs_private_path)
430                    self.log.info("copying %s -> %s", src, dst)
431                    shutil.copy(src, dst)
432
433        self.distribution.package_data = {
434            "mysql": [
435                "vendor/*",
436                "vendor/plugin/*",
437                "vendor/private/*",
438                "vendor/private/sasl2/*"
439            ]
440        }
441
442
443class BuildExt(build_ext, BaseCommand):
444    """Command class for building the Connector/Python C Extensions."""
445
446    description = "build MySQL Connector/Python C extensions"
447    user_options = build_ext.user_options + CEXT_OPTIONS
448    boolean_options = build_ext.boolean_options + BaseCommand.boolean_options
449
450    def _get_mysql_version(self):
451        if os.name == "posix" and self._mysql_info:
452            mysql_version = "{}.{}.{}".format(*self._mysql_info["version"][:3])
453        elif os.name == "nt" and os.path.isdir(self.with_mysql_capi):
454            mysql_version_h = os.path.join(self.with_mysql_capi,
455                                           "include",
456                                           "mysql_version.h")
457            with open(mysql_version_h, "rb") as fp:
458                for line in fp.readlines():
459                    if b"#define LIBMYSQL_VERSION" in line:
460                        mysql_version = LooseVersion(
461                            line.split()[2].replace(b'"', b"").decode()
462                        ).version
463                        break
464        else:
465            mysql_version = None
466        return mysql_version
467
468    def _finalize_mysql_capi(self):
469        self.log.info("Copying MySQL libraries")
470
471        if not os.path.exists(self._build_mysql_lib_dir):
472            os.makedirs(self._build_mysql_lib_dir)
473
474        libs = []
475
476        # Add libmysqlclient libraries to be copied
477        if "link_dirs" in self._mysql_info:
478            libs += glob(os.path.join(
479                self._mysql_info["link_dirs"][0], "libmysqlclient*"))
480
481        for lib in libs:
482            self.log.info("copying %s -> %s",
483                          lib, self._build_mysql_lib_dir)
484            shutil.copy(lib, self._build_mysql_lib_dir)
485
486        # Remove all but static libraries to force static linking
487        if os.name == "posix":
488            self.log.info("Removing non-static MySQL libraries from %s",
489                          self._build_mysql_lib_dir)
490            for lib in os.listdir(self._build_mysql_lib_dir):
491                lib_path = os.path.join(self._build_mysql_lib_dir, lib)
492                if os.path.isfile(lib_path) and not lib.endswith(".a"):
493                    os.unlink(os.path.join(self._build_mysql_lib_dir, lib))
494
495    def _finalize_protobuf(self):
496        if not self.with_protobuf_include_dir:
497            self.with_protobuf_include_dir = \
498                os.environ.get("MYSQLXPB_PROTOBUF_INCLUDE_DIR")
499
500        if not self.with_protobuf_lib_dir:
501            self.with_protobuf_lib_dir = \
502                os.environ.get("MYSQLXPB_PROTOBUF_LIB_DIR")
503
504        if not self.with_protoc:
505            self.with_protoc = os.environ.get("MYSQLXPB_PROTOC")
506
507        if self.with_protobuf_include_dir:
508            self.log.info("Protobuf include directory: %s",
509                          self.with_protobuf_include_dir)
510            if not os.path.isdir(self.with_protobuf_include_dir):
511                self.log.error("Protobuf include dir should be a directory")
512                sys.exit(1)
513        else:
514            self.log.error("Unable to find Protobuf include directory")
515            sys.exit(1)
516
517        if self.with_protobuf_lib_dir:
518            self.log.info("Protobuf library directory: %s",
519                          self.with_protobuf_lib_dir)
520            if not os.path.isdir(self.with_protobuf_lib_dir):
521                self.log.error("Protobuf library dir should be a directory")
522                sys.exit(1)
523        else:
524            self.log.error("Unable to find Protobuf library directory")
525            sys.exit(1)
526
527        if self.with_protoc:
528            self.log.info("Protobuf protoc binary: %s", self.with_protoc)
529            if not os.path.isfile(self.with_protoc):
530                self.log.error("Protobuf protoc binary is not valid")
531                sys.exit(1)
532        else:
533            self.log.error("Unable to find Protobuf protoc binary")
534            sys.exit(1)
535
536        if not os.path.exists(self._build_protobuf_lib_dir):
537            os.makedirs(self._build_protobuf_lib_dir)
538
539        self.log.info("Copying Protobuf libraries")
540
541        libs = glob(os.path.join(self.with_protobuf_lib_dir, "libprotobuf*"))
542        for lib in libs:
543            if os.path.isfile(lib):
544                self.log.info("copying %s -> %s",
545                              lib, self._build_protobuf_lib_dir)
546                shutil.copy2(lib, self._build_protobuf_lib_dir)
547
548        # Remove all but static libraries to force static linking
549        if os.name == "posix":
550            self.log.info("Removing non-static Protobuf libraries from %s",
551                          self._build_protobuf_lib_dir)
552            for lib in os.listdir(self._build_protobuf_lib_dir):
553                lib_path = os.path.join(self._build_protobuf_lib_dir, lib)
554                if os.path.isfile(lib_path) and \
555                   not lib.endswith((".a", ".dylib",)):
556                    os.unlink(os.path.join(self._build_protobuf_lib_dir, lib))
557
558    def _run_protoc(self):
559        base_path = os.path.join(os.getcwd(), "src", "mysqlxpb", "mysqlx")
560        command = [self.with_protoc, "-I"]
561        command.append(os.path.join(base_path, "protocol"))
562        command.extend(glob(os.path.join(base_path, "protocol", "*.proto")))
563        command.append("--cpp_out={0}".format(base_path))
564        self.log.info("Running protoc command: %s", " ".join(command))
565        check_call(command)
566
567    def initialize_options(self):
568        """Initialize the options."""
569        build_ext.initialize_options(self)
570        BaseCommand.initialize_options(self)
571
572    def finalize_options(self):
573        """Finalize the options."""
574        build_ext.finalize_options(self)
575        BaseCommand.finalize_options(self)
576
577        self.log.info("Python architecture: %s", ARCH)
578
579        self._build_mysql_lib_dir = os.path.join(
580            self.build_temp, "capi", "lib")
581        self._build_protobuf_lib_dir = os.path.join(
582            self.build_temp, "protobuf", "lib")
583        if self.with_mysql_capi:
584            self._mysql_info = mysql_c_api_info(self.with_mysql_capi)
585            self._finalize_mysql_capi()
586
587        self.with_mysqlxpb_cext = any((self.with_protobuf_include_dir,
588                                       self.with_protobuf_lib_dir,
589                                       self.with_protoc))
590        if self.with_mysqlxpb_cext:
591            self._finalize_protobuf()
592
593    def run(self):
594        """Run the command."""
595        # Generate docs/INFO_SRC
596        write_info_src(VERSION_TEXT)
597
598        disabled = []  # Extensions to be disabled
599        for ext in self.extensions:
600            # Add Protobuf include and library dirs
601            if ext.name == "_mysqlxpb":
602                if not self.with_mysqlxpb_cext:
603                    self.log.warning(
604                        "The '_mysqlxpb' C extension will not be built")
605                    disabled.append(ext)
606                    continue
607                if platform.system() == "Darwin":
608                    symbol_file = tempfile.NamedTemporaryFile()
609                    ext.extra_link_args.extend(
610                        ["-exported_symbols_list", symbol_file.name]
611                    )
612                    with open(symbol_file.name, "w") as fp:
613                        fp.write("_PyInit__mysqlxpb")
614                        fp.write("\n")
615                ext.include_dirs.append(self.with_protobuf_include_dir)
616                ext.library_dirs.append(self._build_protobuf_lib_dir)
617                ext.libraries.append(
618                    "libprotobuf" if os.name == "nt" else "protobuf")
619                # Add -std=c++11 needed for Protobuf 3.6.1
620                ext.extra_compile_args.append("-std=c++11")
621                self._run_protoc()
622            if ext.name == "_mysql_connector":
623                if not self.with_mysql_capi:
624                    self.log.warning(
625                        "The '_mysql_connector' C extension will not be built")
626                    disabled.append(ext)
627                    continue
628                # Add extra compile args
629                if self.extra_compile_args:
630                    ext.extra_compile_args.extend(
631                        self.extra_compile_args.split())
632                # Add extra link args
633                if self.extra_link_args:
634                    ext.extra_link_args.extend(self.extra_link_args.split())
635                # Add -rpath if the platform is Linux
636                if platform.system() == "Linux":
637                    ext.extra_link_args.extend([
638                        "-Wl,-rpath,$ORIGIN/mysql/vendor"])
639                # Add include dirs
640                if self.with_openssl_include_dir:
641                    ext.include_dirs.append(self.with_openssl_include_dir)
642                if "include_dirs" in self._mysql_info:
643                    ext.include_dirs.extend(self._mysql_info["include_dirs"])
644                # Add library dirs
645                ext.library_dirs.append(self._build_mysql_lib_dir)
646                if "library_dirs" in self._mysql_info:
647                    ext.library_dirs.extend(self._mysql_info["library_dirs"])
648                if self.with_openssl_lib_dir:
649                    ext.library_dirs.append(self.with_openssl_lib_dir)
650                # Add libraries
651                if "libraries" in self._mysql_info:
652                    ext.libraries.extend(self._mysql_info["libraries"])
653            # Suppress unknown pragmas
654            if os.name == "posix":
655                ext.extra_compile_args.append("-Wno-unknown-pragmas")
656
657        if os.name != "nt":
658            cmd_gcc_ver = ["cc", "-v"]
659            self.log.info("Executing: {0}"
660                          "".format(" ".join(cmd_gcc_ver)))
661            proc = Popen(cmd_gcc_ver, stdout=PIPE,
662                         universal_newlines=True)
663            self.log.info(proc.communicate())
664            cmd_gpp_ver = ["c++", "-v"]
665            self.log.info("Executing: {0}"
666                          "".format(" ".join(cmd_gcc_ver)))
667            proc = Popen(cmd_gpp_ver, stdout=PIPE,
668                         universal_newlines=True)
669            self.log.info(proc.communicate())
670
671        # Remove disabled extensions
672        for ext in disabled:
673            self.extensions.remove(ext)
674
675        build_ext.run(self)
676
677        # Change @loader_path if the platform is MacOS
678        if platform.system() == "Darwin" and self.with_openssl_lib_dir:
679            for ext in self.extensions:
680                if ext.name == "_mysql_connector":
681                    libssl, libcrypto = self._get_openssl_libs()
682                    cmd_libssl = [
683                        "install_name_tool", "-change", libssl,
684                        "@loader_path/mysql/vendor/{0}".format(libssl),
685                        build_ext.get_ext_fullpath(self, "_mysql_connector")
686                    ]
687                    self.log.info("Executing: {0}"
688                                  "".format(" ".join(cmd_libssl)))
689                    proc = Popen(cmd_libssl, stdout=PIPE,
690                                 universal_newlines=True)
691                    stdout, _ = proc.communicate()
692                    cmd_libcrypto = [
693                        "install_name_tool", "-change", libcrypto,
694                        "@loader_path/mysql/vendor/{0}".format(libcrypto),
695                        build_ext.get_ext_fullpath(self, "_mysql_connector")
696                    ]
697                    self.log.info("Executing: {0}"
698                                  "".format(" ".join(cmd_libcrypto)))
699                    proc = Popen(cmd_libcrypto, stdout=PIPE,
700                                 universal_newlines=True)
701                    stdout, _ = proc.communicate()
702
703        # Generate docs/INFO_BIN
704        if self.with_mysql_capi:
705            mysql_version = self._get_mysql_version()
706            compiler = self.compiler.compiler_so[0] \
707                if hasattr(self.compiler, "compiler_so") else None
708            write_info_bin(mysql_version, compiler)
709
710
711class InstallLib(install_lib):
712    """InstallLib Connector/Python implementation."""
713
714    user_options = install_lib.user_options + [
715        ("byte-code-only", None,
716         "remove Python .py files; leave byte code .pyc only"),
717    ]
718    boolean_options = ["byte-code-only"]
719    log = LOGGER
720
721    def initialize_options(self):
722        """Initialize the options."""
723        install_lib.initialize_options(self)
724        self.byte_code_only = False
725
726    def finalize_options(self):
727        """Finalize the options."""
728        install_lib.finalize_options(self)
729        self.set_undefined_options("install",
730                                   ("byte_code_only", "byte_code_only"))
731        self.set_undefined_options("build", ("build_base", "build_dir"))
732
733    def run(self):
734        """Run the command."""
735        if not os.path.exists(self.build_dir):
736            mkpath(self.build_dir)
737
738        self.build()
739
740        outfiles = self.install()
741
742        # (Optionally) compile .py to .pyc
743        if outfiles is not None and self.distribution.has_pure_modules():
744            self.byte_compile(outfiles)
745
746        if self.byte_code_only:
747            if get_python_version().startswith("3"):
748                for base, _, files in os.walk(self.install_dir):
749                    for filename in files:
750                        if filename.endswith(".pyc"):
751                            new_name = "{0}.pyc".format(filename.split(".")[0])
752                            os.rename(os.path.join(base, filename),
753                                      os.path.join(base, "..", new_name))
754                    if base.endswith("__pycache__"):
755                        os.rmdir(base)
756            for source_file in outfiles:
757                if source_file.endswith(".py"):
758                    self.log.info("Removing %s", source_file)
759                    os.remove(source_file)
760
761
762class Install(install, BaseCommand):
763    """Install Connector/Python implementation."""
764
765    description = "install MySQL Connector/Python"
766    user_options = install.user_options + BaseCommand.user_options
767    boolean_options = install.boolean_options + BaseCommand.boolean_options
768
769    def initialize_options(self):
770        """Initialize the options."""
771        install.initialize_options(self)
772        BaseCommand.initialize_options(self)
773
774    def finalize_options(self):
775        """Finalize the options."""
776        BaseCommand.finalize_options(self)
777        install.finalize_options(self)
778        cmd_install_lib = self.distribution.get_command_obj("install_lib")
779        cmd_install_lib.byte_code_only = self.byte_code_only
780