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"""Implements the Distutils commands for creating Debian packages.""" 30 31import os 32import re 33import subprocess 34import sys 35 36from datetime import datetime 37 38from distutils.file_util import copy_file, move_file 39 40from . import BaseCommand, EDITION, VERSION, VERSION_EXTRA 41from .utils import unarchive_targz, linux_distribution 42 43 44DEBIAN_ROOT = os.path.join("cpydist", "data", "deb") 45DPKG_MAKER = "dpkg-buildpackage" 46LINUX_DIST = linux_distribution() 47VERSION_TEXT_SHORT = "{0}.{1}.{2}".format(*VERSION[0:3]) 48 49GPL_LIC_TEXT = """\ 50This is a release of MySQL Connector/Python, Oracle's dual- 51 license Python Driver for MySQL. For the avoidance of 52 doubt, this particular copy of the software is released 53 under the version 2 of the GNU General Public License. 54 MySQL Connector/Python is brought to you by Oracle. 55""" 56 57class DistDeb(BaseCommand): 58 """Create a Debian distribution.""" 59 60 description = "create a Debian distribution" 61 debian_files = [ 62 "changelog", 63 "compat", 64 "control", 65 "copyright", 66 "docs", 67 "mysql-connector-python-py3.postinst", 68 "mysql-connector-python-py3.postrm", 69 "mysql-connector-python.postinst", 70 "mysql-connector-python.postrm", 71 "rules", 72 ] 73 user_options = BaseCommand.user_options + [ 74 ("dist-dir=", "d", 75 "directory to put final built distributions in"), 76 ("platform=", "p", 77 "name of the platform in resulting files " 78 "(default '%s')" % LINUX_DIST[0].lower()), 79 ("sign", None, 80 "sign the Debian package"), 81 ] 82 boolean_options = BaseCommand.boolean_options + ["sign"] 83 84 dist_dir = None 85 with_cext = False 86 87 def initialize_options(self): 88 """Initialize the options.""" 89 BaseCommand.initialize_options(self) 90 91 self.platform = LINUX_DIST[0].lower() 92 if "debian" in self.platform: 93 # For Debian we only use the first part of the version, Ubuntu two 94 self.platform_version = LINUX_DIST[1].split(".", 2)[0] 95 else: 96 self.platform_version = ".".join(LINUX_DIST[1].split(".", 2)[0:2]) 97 self.sign = False 98 self.debian_support_dir = DEBIAN_ROOT 99 self.edition = EDITION 100 self.codename = linux_distribution()[2].lower() 101 self.version_extra = "-{0}".format(VERSION_EXTRA) \ 102 if VERSION_EXTRA else "" 103 104 def finalize_options(self): 105 """Finalize the options.""" 106 BaseCommand.finalize_options(self) 107 108 cmd_build = self.get_finalized_command("build") 109 self.build_base = cmd_build.build_base 110 if not self.dist_dir: 111 self.dist_dir = "dist" 112 self.with_cext = any((self.with_mysql_capi, 113 self.with_protobuf_include_dir, 114 self.with_protobuf_lib_dir, self.with_protoc)) 115 116 @property 117 def _have_python3(self): 118 """Check whether this distribution has Python 3 support.""" 119 try: 120 devnull = open(os.devnull, "w") 121 subprocess.Popen(["py3versions"], 122 stdin=devnull, 123 stdout=devnull, 124 stderr=devnull) 125 except OSError: 126 return False 127 128 return True 129 130 def _get_orig_name(self): 131 """Return name for tarball according to Debian's policies.""" 132 return "%(name)s%(label)s_%(version)s%(version_extra)s.orig" % { 133 "name": self.distribution.get_name(), 134 "label": "-{}".format(self.label) if self.label else "", 135 "version": self.distribution.get_version(), 136 "version_extra": self.version_extra 137 } 138 139 def _get_changes(self): 140 """Get changes from CHANGES.txt.""" 141 log_lines = [] 142 found_version = False 143 found_items = False 144 with open("CHANGES.txt", "r") as fp: 145 for line in fp.readlines(): 146 line = line.rstrip() 147 if line.endswith(VERSION_TEXT_SHORT): 148 found_version = True 149 if not line.strip() and found_items: 150 break 151 elif found_version and line.startswith("- "): 152 log_lines.append(" " * 2 + "* " + line[2:]) 153 found_items = True 154 155 return log_lines 156 157 def _populate_debian(self): 158 """Copy and make files ready in the debian/ folder.""" 159 for afile in self.debian_files: 160 copy_file(os.path.join(self.debian_support_dir, afile), 161 self.debian_base) 162 163 copy_file(os.path.join(self.debian_support_dir, "source", "format"), 164 os.path.join(self.debian_base, "source")) 165 166 # Update the version and log in the Debian changelog 167 changelog_file = os.path.join(self.debian_base, "changelog") 168 with open(changelog_file, "r") as fp: 169 changelog = fp.readlines() 170 self.log.info("changing changelog '%s' version and log", 171 changelog_file) 172 173 log_lines = self._get_changes() 174 if not log_lines: 175 self.log.error("Failed reading change history from CHANGES.txt") 176 log_lines.append(" * (change history missing)") 177 178 new_changelog = [] 179 first_line = True 180 regex = re.compile(r".*\((\d+\.\d+.\d+-1)\).*") 181 for line in changelog: 182 line = line.rstrip() 183 match = regex.match(line) 184 if match: 185 version = match.groups()[0] 186 line = line.replace(version, 187 "{0}.{1}.{2}-1".format(*VERSION[0:3])) 188 if first_line: 189 if self.codename == "": 190 proc = subprocess.Popen(["lsb_release", "-c"], 191 stdout=subprocess.PIPE, 192 stderr=subprocess.STDOUT) 193 codename = proc.stdout.read().split()[-1] 194 self.codename = codename.decode() \ 195 if sys.version_info[0] == 3 else codename 196 if self.label: 197 line = line.replace( 198 "mysql-connector-python", 199 "mysql-connector-python-{}".format(self.label)) 200 line = line.replace("UNRELEASED", self.codename) 201 line = line.replace("-1", 202 "{version_extra}-1{platform}{version}" 203 .format(platform=self.platform, 204 version=self.platform_version, 205 version_extra=self.version_extra)) 206 first_line = False 207 if "* Changes here." in line: 208 for change in log_lines: 209 new_changelog.append(change) 210 elif line.startswith(" --") and "@" in line: 211 utcnow = datetime.utcnow().strftime( 212 "%a, %d %b %Y %H:%M:%S +0000") 213 line = re.sub(r"( -- .* <.*@.*> ).*", r"\1" + utcnow, line) 214 new_changelog.append(line + "\n") 215 else: 216 new_changelog.append(line) 217 218 with open(changelog_file, "w") as changelog: 219 changelog.write("\n".join(new_changelog)) 220 221 control_file = os.path.join(self.debian_base, "control") 222 if self.label: 223 # Update the Source, Package and Conflicts fields 224 # in control file, if self.label is present 225 with open(control_file, "r") as fp: 226 control = fp.readlines() 227 228 self.log.info("changing control '%s' Source, Package and Conflicts fields", 229 control_file) 230 231 new_control = [] 232 add_label_regex = re.compile(r"^((?:Source|Package): mysql-connector-python)") 233 remove_label_regex = re.compile("^(Conflicts: .*?)-{}".format(self.label)) 234 for line in control: 235 line = line.rstrip() 236 237 match = add_label_regex.match(line) 238 if match: 239 line = add_label_regex.sub(r"\1-{}".format(self.label), line) 240 241 match = remove_label_regex.match(line) 242 if match: 243 line = remove_label_regex.sub(r"\1", line) 244 245 new_control.append(line) 246 247 with open(control_file, "w") as fp: 248 fp.write("\n".join(new_control)) 249 250 lic_text = GPL_LIC_TEXT 251 252 if self.byte_code_only: 253 copyright_file = os.path.join(self.debian_base, "copyright") 254 self.log.info("Reading license text from copyright file ({})" 255 "".format(copyright_file)) 256 with open(copyright_file, "r") as fp: 257 # Skip to line just before the text we want to copy 258 while True: 259 line = fp.readline() 260 if not line: 261 break 262 if line.startswith("License: Commercial"): 263 # Read the rest of the text 264 lic_text = fp.read() 265 266 with open(control_file, "r") as fp: 267 control_text = fp.read() 268 269 self.log.info("Updating license text in control file") 270 new_control = re.sub(r"@LICENSE@", lic_text, control_text) 271 with open(control_file, "w") as fp: 272 fp.write(new_control) 273 274 def _prepare(self, tarball=None, base=None): 275 """Prepare Debian files.""" 276 # Rename tarball to conform Debian's Policy 277 if tarball: 278 self.orig_tarball = os.path.join( 279 os.path.dirname(tarball), 280 self._get_orig_name()) + ".tar.gz" 281 move_file(tarball, self.orig_tarball) 282 283 unarchive_targz(self.orig_tarball) 284 self.debian_base = os.path.join( 285 tarball.replace(".tar.gz", ""), "debian") 286 elif base: 287 self.debian_base = os.path.join(base, "debian") 288 289 self.mkpath(self.debian_base) 290 self.mkpath(os.path.join(self.debian_base, "source")) 291 self._populate_debian() 292 293 def _make_dpkg(self): 294 """Create Debian package in the source distribution folder.""" 295 self.log.info("creating Debian package using '%s'", DPKG_MAKER) 296 297 orig_pwd = os.getcwd() 298 os.chdir(os.path.join(self.build_base, 299 self.distribution.get_fullname())) 300 cmd = [DPKG_MAKER, "-uc"] 301 302 if not self.sign: 303 cmd.append("-us") 304 305 success = True 306 env = os.environ.copy() 307 env["MYSQL_CAPI"] = self.with_mysql_capi or "" 308 env["OPENSSL_INCLUDE_DIR"] = self.with_openssl_include_dir or "" 309 env["OPENSSL_LIB_DIR"] = self.with_openssl_lib_dir or "" 310 env["MYSQLXPB_PROTOBUF_INCLUDE_DIR"] = \ 311 self.with_protobuf_include_dir or "" 312 env["MYSQLXPB_PROTOBUF_LIB_DIR"] = self.with_protobuf_lib_dir or "" 313 env["MYSQLXPB_PROTOC"] = self.with_protoc or "" 314 env["WITH_CEXT"] = "1" if self.with_cext else "" 315 env["EXTRA_COMPILE_ARGS"] = self.extra_compile_args or "" 316 env["EXTRA_LINK_ARGS"] = self.extra_link_args or "" 317 env["LABEL"] = self.label if self.label else "0" 318 env["BYTE_CODE_ONLY"] = "1" if self.byte_code_only else "" 319 proc = subprocess.Popen(cmd, 320 stdout=subprocess.PIPE, 321 stderr=subprocess.PIPE, 322 universal_newlines=True, 323 env=env) 324 stdout, stderr = proc.communicate() 325 for line in stdout.split("\n"): 326 if self.debug: 327 self.log.info(line) 328 if "error:" in line or "E: " in line: 329 if not self.debug: 330 self.log.info(line) 331 success = False 332 333 if stderr: 334 for line in stderr.split("\n"): 335 if self.debug: 336 self.log.info(line) 337 if "error:" in line or "E: " in line: 338 if not self.debug: 339 self.log.info(line) 340 success = False 341 342 os.chdir(orig_pwd) 343 return success 344 345 def _move_to_dist(self): 346 """Move *.deb files to dist/ (dist_dir) folder.""" 347 for base, dirs, files in os.walk(self.build_base): 348 for filename in files: 349 if "-py3" in filename and not self._have_python3: 350 continue 351 if not self.with_mysql_capi and "cext" in filename: 352 continue 353 if filename.endswith(".deb"): 354 filepath = os.path.join(base, filename) 355 copy_file(filepath, self.dist_dir) 356 357 def run(self): 358 """Run the command.""" 359 self.mkpath(self.dist_dir) 360 361 sdist = self.reinitialize_command("sdist") 362 sdist.dist_dir = self.build_base 363 sdist.formats = ["gztar"] 364 sdist.label = self.label 365 sdist.ensure_finalized() 366 sdist.run() 367 368 self._prepare(sdist.archive_files[0]) 369 success = self._make_dpkg() 370 371 if not success: 372 self.log.error("Building Debian package failed") 373 else: 374 self._move_to_dist() 375 376 self.remove_temp() 377