#./python """Run Python tests with multiple installations of OpenSSL The script (1) downloads OpenSSL tar bundle (2) extracts it to ../openssl/src/openssl-VERSION/ (3) compiles OpenSSL (4) installs OpenSSL into ../openssl/VERSION/ (5) forces a recompilation of Python modules using the header and library files from ../openssl/VERSION/ (6) runs Python's test suite The script must be run with Python's build directory as current working directory: ./python Tools/ssl/test_multiple_versions.py The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend search paths for header files and shared libraries. It's known to work on Linux with GCC 4.x. (c) 2013 Christian Heimes <christian@python.org> """ import logging import os import tarfile import shutil import subprocess import sys from urllib.request import urlopen log = logging.getLogger("multissl") OPENSSL_VERSIONS = [ "0.9.7m", "0.9.8i", "0.9.8l", "0.9.8m", "0.9.8y", "1.0.0k", "1.0.1e" ] FULL_TESTS = [ "test_asyncio", "test_ftplib", "test_hashlib", "test_httplib", "test_imaplib", "test_nntplib", "test_poplib", "test_smtplib", "test_smtpnet", "test_urllib2_localnet", "test_venv" ] MINIMAL_TESTS = ["test_ssl", "test_hashlib"] CADEFAULT = True HERE = os.path.abspath(os.getcwd()) DEST_DIR = os.path.abspath(os.path.join(HERE, os.pardir, "openssl")) class BuildSSL: url_template = "https://www.openssl.org/source/openssl-{}.tar.gz" module_files = ["Modules/_ssl.c", "Modules/socketmodule.c", "Modules/_hashopenssl.c"] def __init__(self, version, openssl_compile_args=(), destdir=DEST_DIR): self._check_python_builddir() self.version = version self.openssl_compile_args = openssl_compile_args # installation directory self.install_dir = os.path.join(destdir, version) # source file self.src_file = os.path.join(destdir, "src", "openssl-{}.tar.gz".format(version)) # build directory (removed after install) self.build_dir = os.path.join(destdir, "src", "openssl-{}".format(version)) @property def openssl_cli(self): """openssl CLI binary""" return os.path.join(self.install_dir, "bin", "openssl") @property def openssl_version(self): """output of 'bin/openssl version'""" env = os.environ.copy() env["LD_LIBRARY_PATH"] = self.lib_dir cmd = [self.openssl_cli, "version"] return self._subprocess_output(cmd, env=env) @property def pyssl_version(self): """Value of ssl.OPENSSL_VERSION""" env = os.environ.copy() env["LD_LIBRARY_PATH"] = self.lib_dir cmd = ["./python", "-c", "import ssl; print(ssl.OPENSSL_VERSION)"] return self._subprocess_output(cmd, env=env) @property def include_dir(self): return os.path.join(self.install_dir, "include") @property def lib_dir(self): return os.path.join(self.install_dir, "lib") @property def has_openssl(self): return os.path.isfile(self.openssl_cli) @property def has_src(self): return os.path.isfile(self.src_file) def _subprocess_call(self, cmd, stdout=subprocess.DEVNULL, env=None, **kwargs): log.debug("Call '{}'".format(" ".join(cmd))) return subprocess.check_call(cmd, stdout=stdout, env=env, **kwargs) def _subprocess_output(self, cmd, env=None, **kwargs): log.debug("Call '{}'".format(" ".join(cmd))) out = subprocess.check_output(cmd, env=env) return out.strip().decode("utf-8") def _check_python_builddir(self): if not os.path.isfile("python") or not os.path.isfile("setup.py"): raise ValueError("Script must be run in Python build directory") def _download_openssl(self): """Download OpenSSL source dist""" src_dir = os.path.dirname(self.src_file) if not os.path.isdir(src_dir): os.makedirs(src_dir) url = self.url_template.format(self.version) log.info("Downloading OpenSSL from {}".format(url)) req = urlopen(url, cadefault=CADEFAULT) # KISS, read all, write all data = req.read() log.info("Storing {}".format(self.src_file)) with open(self.src_file, "wb") as f: f.write(data) def _unpack_openssl(self): """Unpack tar.gz bundle""" # cleanup if os.path.isdir(self.build_dir): shutil.rmtree(self.build_dir) os.makedirs(self.build_dir) tf = tarfile.open(self.src_file) base = "openssl-{}/".format(self.version) # force extraction into build dir members = tf.getmembers() for member in members: if not member.name.startswith(base): raise ValueError(member.name) member.name = member.name[len(base):] log.info("Unpacking files to {}".format(self.build_dir)) tf.extractall(self.build_dir, members) def _build_openssl(self): """Now build openssl""" log.info("Running build in {}".format(self.install_dir)) cwd = self.build_dir cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)] cmd.extend(self.openssl_compile_args) self._subprocess_call(cmd, cwd=cwd) self._subprocess_call(["make"], cwd=cwd) def _install_openssl(self, remove=True): self._subprocess_call(["make", "install"], cwd=self.build_dir) if remove: shutil.rmtree(self.build_dir) def install_openssl(self): if not self.has_openssl: if not self.has_src: self._download_openssl() else: log.debug("Already has src {}".format(self.src_file)) self._unpack_openssl() self._build_openssl() self._install_openssl() else: log.info("Already has installation {}".format(self.install_dir)) # validate installation version = self.openssl_version if self.version not in version: raise ValueError(version) def touch_pymods(self): # force a rebuild of all modules that use OpenSSL APIs for fname in self.module_files: os.utime(fname) def recompile_pymods(self): log.info("Using OpenSSL build from {}".format(self.build_dir)) # overwrite header and library search paths env = os.environ.copy() env["CPPFLAGS"] = "-I{}".format(self.include_dir) env["LDFLAGS"] = "-L{}".format(self.lib_dir) # set rpath env["LD_RUN_PATH"] = self.lib_dir log.info("Rebuilding Python modules") self.touch_pymods() cmd = ["./python", "setup.py", "build"] self._subprocess_call(cmd, env=env) def check_pyssl(self): version = self.pyssl_version if self.version not in version: raise ValueError(version) def run_pytests(self, *args): cmd = ["./python", "-m", "test"] cmd.extend(args) self._subprocess_call(cmd, stdout=None) def run_python_tests(self, *args): self.recompile_pymods() self.check_pyssl() self.run_pytests(*args) def main(*args): builders = [] for version in OPENSSL_VERSIONS: if version in ("0.9.8i", "0.9.8l"): openssl_compile_args = ("no-asm",) else: openssl_compile_args = () builder = BuildSSL(version, openssl_compile_args) builder.install_openssl() builders.append(builder) for builder in builders: builder.run_python_tests(*args) # final touch builder.touch_pymods() if __name__ == "__main__": logging.basicConfig(level=logging.INFO, format="*** %(levelname)s %(message)s") args = sys.argv[1:] if not args: args = ["-unetwork", "-v"] args.extend(FULL_TESTS) main(*args)