1# Copyright (c) 2020, 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"""Implements the Distutils command 'bdist'.
30
31Creates a binary distribution.
32"""
33
34import os
35import logging
36
37from distutils import log
38from distutils.util import byte_compile
39from distutils.dir_util import remove_tree, mkpath, copy_tree
40from distutils.file_util import copy_file
41from distutils.sysconfig import get_python_version
42from distutils.command.bdist import bdist
43
44from . import COMMON_USER_OPTIONS, VERSION_TEXT, EDITION, LOGGER
45from .utils import add_docs, write_info_src, write_info_bin
46
47
48class DistBinary(bdist):
49    """Create a generic binary distribution.
50
51    DistBinary is meant to replace distutils.bdist.
52    """
53
54    description = "create a built (binary) distribution"
55    user_options = COMMON_USER_OPTIONS + [
56        ("bdist-dir=", "d",
57         "temporary directory for creating the distribution"),
58        ("dist-dir=", "d",
59         "directory to put final built distributions in"),
60    ]
61    boolean_options = ["debug", "byte-code-only", "keep-temp"]
62    log = LOGGER
63
64    def initialize_options(self):
65        """Initialize the options."""
66        bdist.initialize_options(self)
67        self.bdist_dir = None
68        self.byte_code_only = False
69        self.label = None
70        self.edition = EDITION
71        self.debug = False
72        self.keep_temp = False
73
74    def finalize_options(self):
75        """Finalize the options."""
76        bdist.finalize_options(self)
77
78        def _get_fullname():
79            label = "-{}".format(self.label) if self.label else ""
80            python_version = "-py{}".format(get_python_version()) \
81                if self.byte_code_only else ""
82            return "{name}{label}-{version}{edition}{pyver}".format(
83                name=self.distribution.get_name(),
84                label=label,
85                version=self.distribution.get_version(),
86                edition=self.edition or "",
87                pyver=python_version)
88
89        self.distribution.get_fullname = _get_fullname
90
91        if self.bdist_dir is None:
92            self.bdist_dir = os.path.join(self.dist_dir,
93                                          "bdist.{}".format(self.plat_name))
94        if self.debug:
95            self.log.setLevel(logging.DEBUG)
96            log.set_threshold(1)  # Set Distutils logging level to DEBUG
97
98    def _remove_sources(self):
99        """Remove Python source files from the build directory."""
100        for base, dirs, files in os.walk(self.bdist_dir):
101            for filename in files:
102                if filename.endswith(".py"):
103                    filepath = os.path.join(base, filename)
104                    self.log.info("Removing source '%s'", filepath)
105                    os.unlink(filepath)
106
107    def _copy_from_pycache(self, start_dir):
108        """Copy .py files from __pycache__."""
109        for base, dirs, files in os.walk(start_dir):
110            for filename in files:
111                if filename.endswith(".pyc"):
112                    filepath = os.path.join(base, filename)
113                    new_name = "{}.pyc".format(filename.split(".")[0])
114                    os.rename(filepath, os.path.join(base, "..", new_name))
115        for base, dirs, files in os.walk(start_dir):
116            if base.endswith("__pycache__"):
117                os.rmdir(base)
118
119    def run(self):
120        """Run the command."""
121        self.log.info("Installing library code to %s", self.bdist_dir)
122        self.log.info("Generating INFO_SRC and INFO_BIN files")
123        write_info_src(VERSION_TEXT)
124        write_info_bin()
125
126        dist_name = self.distribution.get_fullname()
127        self.dist_target = os.path.join(self.dist_dir, dist_name)
128        self.log.info("Distribution will be available as '%s'",
129                      self.dist_target)
130
131        # build command: just to get the build_base
132        cmdbuild = self.get_finalized_command("build")
133        self.build_base = cmdbuild.build_base
134
135        # install command
136        install = self.reinitialize_command("install_lib",
137                                            reinit_subcommands=1)
138        install.compile = False
139        install.warn_dir = 0
140        install.install_dir = self.bdist_dir
141
142        self.log.info("Installing to %s", self.bdist_dir)
143        self.run_command("install_lib")
144
145        # install_egg_info command
146        cmd_egginfo = self.get_finalized_command("install_egg_info")
147        cmd_egginfo.install_dir = self.bdist_dir
148        self.run_command("install_egg_info")
149
150        installed_files = install.get_outputs()
151
152        # compile and remove sources
153        if self.byte_code_only:
154            byte_compile(installed_files, optimize=0, force=True,
155                         prefix=install.install_dir)
156            self._remove_sources()
157            if get_python_version().startswith('3'):
158                self.log.info("Copying byte code from __pycache__")
159                self._copy_from_pycache(os.path.join(self.bdist_dir, "mysql"))
160                self._copy_from_pycache(os.path.join(self.bdist_dir, "mysqlx"))
161
162        # create distribution
163        info_files = [
164            ("README.txt", "README.txt"),
165            ("LICENSE.txt", "LICENSE.txt"),
166            ("README.rst", "README.rst"),
167            ("CONTRIBUTING.rst", "CONTRIBUTING.rst"),
168            ("docs/INFO_SRC", "INFO_SRC"),
169            ("docs/INFO_BIN", "INFO_BIN"),
170        ]
171
172        copy_tree(self.bdist_dir, self.dist_target)
173        mkpath(os.path.join(self.dist_target))
174        for src, dst in info_files:
175            if dst is None:
176                dest_name, _ = copy_file(src, self.dist_target)
177            else:
178                dest_name, _ = copy_file(src,
179                                         os.path.join(self.dist_target, dst))
180
181        add_docs(os.path.join(self.dist_target, "docs"))
182
183        if not self.keep_temp:
184            remove_tree(self.build_base, dry_run=self.dry_run)
185