mirror of
				https://github.com/python/cpython.git
				synced 2025-10-30 01:47:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			543 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			543 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
| #!./python
 | |
| """Run Python tests against multiple installations of OpenSSL and LibreSSL
 | |
| 
 | |
| The script
 | |
| 
 | |
|   (1) downloads OpenSSL / LibreSSL tar bundle
 | |
|   (2) extracts it to ./src
 | |
|   (3) compiles OpenSSL / LibreSSL
 | |
|   (4) installs OpenSSL / LibreSSL into ../multissl/$LIB/$VERSION/
 | |
|   (5) forces a recompilation of Python modules using the
 | |
|       header and library files from ../multissl/$LIB/$VERSION/
 | |
|   (6) runs Python's test suite
 | |
| 
 | |
| The script must be run with Python's build directory as current working
 | |
| directory.
 | |
| 
 | |
| 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 and clang.
 | |
| 
 | |
| Please keep this script compatible with Python 2.7, and 3.4 to 3.7.
 | |
| 
 | |
| (c) 2013-2017 Christian Heimes <christian@python.org>
 | |
| """
 | |
| from __future__ import print_function
 | |
| 
 | |
| import argparse
 | |
| from datetime import datetime
 | |
| import logging
 | |
| import os
 | |
| try:
 | |
|     from urllib.request import urlopen
 | |
|     from urllib.error import HTTPError
 | |
| except ImportError:
 | |
|     from urllib2 import urlopen, HTTPError
 | |
| import re
 | |
| import shutil
 | |
| import subprocess
 | |
| import sys
 | |
| import tarfile
 | |
| 
 | |
| 
 | |
| log = logging.getLogger("multissl")
 | |
| 
 | |
| OPENSSL_OLD_VERSIONS = [
 | |
| ]
 | |
| 
 | |
| OPENSSL_RECENT_VERSIONS = [
 | |
|     "1.1.1w",
 | |
|     "3.0.13",
 | |
|     "3.1.5",
 | |
|     "3.2.1",
 | |
| ]
 | |
| 
 | |
| LIBRESSL_OLD_VERSIONS = [
 | |
| ]
 | |
| 
 | |
| LIBRESSL_RECENT_VERSIONS = [
 | |
| ]
 | |
| 
 | |
| # store files in ../multissl
 | |
| HERE = os.path.dirname(os.path.abspath(__file__))
 | |
| PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
 | |
| MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
 | |
| 
 | |
| 
 | |
| parser = argparse.ArgumentParser(
 | |
|     prog='multissl',
 | |
|     description=(
 | |
|         "Run CPython tests with multiple OpenSSL and LibreSSL "
 | |
|         "versions."
 | |
|     )
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--debug',
 | |
|     action='store_true',
 | |
|     help="Enable debug logging",
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--disable-ancient',
 | |
|     action='store_true',
 | |
|     help="Don't test OpenSSL and LibreSSL versions without upstream support",
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--openssl',
 | |
|     nargs='+',
 | |
|     default=(),
 | |
|     help=(
 | |
|         "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
 | |
|         "OpenSSL and LibreSSL versions are given."
 | |
|     ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--libressl',
 | |
|     nargs='+',
 | |
|     default=(),
 | |
|     help=(
 | |
|         "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
 | |
|         "OpenSSL and LibreSSL versions are given."
 | |
|     ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--tests',
 | |
|     nargs='*',
 | |
|     default=(),
 | |
|     help="Python tests to run, defaults to all SSL related tests.",
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--base-directory',
 | |
|     default=MULTISSL_DIR,
 | |
|     help="Base directory for OpenSSL / LibreSSL sources and builds."
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--no-network',
 | |
|     action='store_false',
 | |
|     dest='network',
 | |
|     help="Disable network tests."
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--steps',
 | |
|     choices=['library', 'modules', 'tests'],
 | |
|     default='tests',
 | |
|     help=(
 | |
|         "Which steps to perform. 'library' downloads and compiles OpenSSL "
 | |
|         "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
 | |
|         "all and runs the test suite."
 | |
|     )
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--system',
 | |
|     default='',
 | |
|     help="Override the automatic system type detection."
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--force',
 | |
|     action='store_true',
 | |
|     dest='force',
 | |
|     help="Force build and installation."
 | |
| )
 | |
| parser.add_argument(
 | |
|     '--keep-sources',
 | |
|     action='store_true',
 | |
|     dest='keep_sources',
 | |
|     help="Keep original sources for debugging."
 | |
| )
 | |
| 
 | |
| 
 | |
| class AbstractBuilder(object):
 | |
|     library = None
 | |
|     url_templates = None
 | |
|     src_template = None
 | |
|     build_template = None
 | |
|     depend_target = None
 | |
|     install_target = 'install'
 | |
|     if hasattr(os, 'process_cpu_count'):
 | |
|         jobs = os.process_cpu_count()
 | |
|     else:
 | |
|         jobs = os.cpu_count()
 | |
| 
 | |
|     module_files = (
 | |
|         os.path.join(PYTHONROOT, "Modules/_ssl.c"),
 | |
|         os.path.join(PYTHONROOT, "Modules/_hashopenssl.c"),
 | |
|     )
 | |
|     module_libs = ("_ssl", "_hashlib")
 | |
| 
 | |
|     def __init__(self, version, args):
 | |
|         self.version = version
 | |
|         self.args = args
 | |
|         # installation directory
 | |
|         self.install_dir = os.path.join(
 | |
|             os.path.join(args.base_directory, self.library.lower()), version
 | |
|         )
 | |
|         # source file
 | |
|         self.src_dir = os.path.join(args.base_directory, 'src')
 | |
|         self.src_file = os.path.join(
 | |
|             self.src_dir, self.src_template.format(version))
 | |
|         # build directory (removed after install)
 | |
|         self.build_dir = os.path.join(
 | |
|             self.src_dir, self.build_template.format(version))
 | |
|         self.system = args.system
 | |
| 
 | |
|     def __str__(self):
 | |
|         return "<{0.__class__.__name__} for {0.version}>".format(self)
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         if not isinstance(other, AbstractBuilder):
 | |
|             return NotImplemented
 | |
|         return (
 | |
|             self.library == other.library
 | |
|             and self.version == other.version
 | |
|         )
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return hash((self.library, self.version))
 | |
| 
 | |
|     @property
 | |
|     def short_version(self):
 | |
|         """Short version for OpenSSL download URL"""
 | |
|         return None
 | |
| 
 | |
|     @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'"""
 | |
|         cmd = [self.openssl_cli, "version"]
 | |
|         return self._subprocess_output(cmd)
 | |
| 
 | |
|     @property
 | |
|     def pyssl_version(self):
 | |
|         """Value of ssl.OPENSSL_VERSION"""
 | |
|         cmd = [
 | |
|             sys.executable,
 | |
|             '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
 | |
|         ]
 | |
|         return self._subprocess_output(cmd)
 | |
| 
 | |
|     @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, env=None, **kwargs):
 | |
|         log.debug("Call '{}'".format(" ".join(cmd)))
 | |
|         return subprocess.check_call(cmd, env=env, **kwargs)
 | |
| 
 | |
|     def _subprocess_output(self, cmd, env=None, **kwargs):
 | |
|         log.debug("Call '{}'".format(" ".join(cmd)))
 | |
|         if env is None:
 | |
|             env = os.environ.copy()
 | |
|             env["LD_LIBRARY_PATH"] = self.lib_dir
 | |
|         out = subprocess.check_output(cmd, env=env, **kwargs)
 | |
|         return out.strip().decode("utf-8")
 | |
| 
 | |
|     def _download_src(self):
 | |
|         """Download sources"""
 | |
|         src_dir = os.path.dirname(self.src_file)
 | |
|         if not os.path.isdir(src_dir):
 | |
|             os.makedirs(src_dir)
 | |
|         data = None
 | |
|         for url_template in self.url_templates:
 | |
|             url = url_template.format(v=self.version, s=self.short_version)
 | |
|             log.info("Downloading from {}".format(url))
 | |
|             try:
 | |
|                 req = urlopen(url)
 | |
|                 # KISS, read all, write all
 | |
|                 data = req.read()
 | |
|             except HTTPError as e:
 | |
|                 log.error(
 | |
|                     "Download from {} has from failed: {}".format(url, e)
 | |
|                 )
 | |
|             else:
 | |
|                 log.info("Successfully downloaded from {}".format(url))
 | |
|                 break
 | |
|         if data is None:
 | |
|             raise ValueError("All download URLs have failed")
 | |
|         log.info("Storing {}".format(self.src_file))
 | |
|         with open(self.src_file, "wb") as f:
 | |
|             f.write(data)
 | |
| 
 | |
|     def _unpack_src(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)
 | |
|         name = self.build_template.format(self.version)
 | |
|         base = name + '/'
 | |
|         # force extraction into build dir
 | |
|         members = tf.getmembers()
 | |
|         for member in list(members):
 | |
|             if member.name == name:
 | |
|                 members.remove(member)
 | |
|             elif not member.name.startswith(base):
 | |
|                 raise ValueError(member.name, base)
 | |
|             member.name = member.name[len(base):].lstrip('/')
 | |
|         log.info("Unpacking files to {}".format(self.build_dir))
 | |
|         tf.extractall(self.build_dir, members)
 | |
| 
 | |
|     def _build_src(self, config_args=()):
 | |
|         """Now build openssl"""
 | |
|         log.info("Running build in {}".format(self.build_dir))
 | |
|         cwd = self.build_dir
 | |
|         cmd = [
 | |
|             "./config", *config_args,
 | |
|             "shared", "--debug",
 | |
|             "--prefix={}".format(self.install_dir)
 | |
|         ]
 | |
|         # cmd.extend(["no-deprecated", "--api=1.1.0"])
 | |
|         env = os.environ.copy()
 | |
|         # set rpath
 | |
|         env["LD_RUN_PATH"] = self.lib_dir
 | |
|         if self.system:
 | |
|             env['SYSTEM'] = self.system
 | |
|         self._subprocess_call(cmd, cwd=cwd, env=env)
 | |
|         if self.depend_target:
 | |
|             self._subprocess_call(
 | |
|                 ["make", "-j1", self.depend_target], cwd=cwd, env=env
 | |
|             )
 | |
|         self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
 | |
| 
 | |
|     def _make_install(self):
 | |
|         self._subprocess_call(
 | |
|             ["make", "-j1", self.install_target],
 | |
|             cwd=self.build_dir
 | |
|         )
 | |
|         self._post_install()
 | |
|         if not self.args.keep_sources:
 | |
|             shutil.rmtree(self.build_dir)
 | |
| 
 | |
|     def _post_install(self):
 | |
|         pass
 | |
| 
 | |
|     def install(self):
 | |
|         log.info(self.openssl_cli)
 | |
|         if not self.has_openssl or self.args.force:
 | |
|             if not self.has_src:
 | |
|                 self._download_src()
 | |
|             else:
 | |
|                 log.debug("Already has src {}".format(self.src_file))
 | |
|             self._unpack_src()
 | |
|             self._build_src()
 | |
|             self._make_install()
 | |
|         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 recompile_pymods(self):
 | |
|         log.warning("Using build from {}".format(self.build_dir))
 | |
|         # force a rebuild of all modules that use OpenSSL APIs
 | |
|         for fname in self.module_files:
 | |
|             os.utime(fname, None)
 | |
|         # remove all build artefacts
 | |
|         for root, dirs, files in os.walk('build'):
 | |
|             for filename in files:
 | |
|                 if filename.startswith(self.module_libs):
 | |
|                     os.unlink(os.path.join(root, filename))
 | |
| 
 | |
|         # 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")
 | |
|         cmd = ["make", "sharedmods", "checksharedmods"]
 | |
|         self._subprocess_call(cmd, env=env)
 | |
|         self.check_imports()
 | |
| 
 | |
|     def check_imports(self):
 | |
|         cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
 | |
|         self._subprocess_call(cmd)
 | |
| 
 | |
|     def check_pyssl(self):
 | |
|         version = self.pyssl_version
 | |
|         if self.version not in version:
 | |
|             raise ValueError(version)
 | |
| 
 | |
|     def run_python_tests(self, tests, network=True):
 | |
|         if not tests:
 | |
|             cmd = [
 | |
|                 sys.executable,
 | |
|                 os.path.join(PYTHONROOT, 'Lib/test/ssltests.py'),
 | |
|                 '-j0'
 | |
|             ]
 | |
|         elif sys.version_info < (3, 3):
 | |
|             cmd = [sys.executable, '-m', 'test.regrtest']
 | |
|         else:
 | |
|             cmd = [sys.executable, '-m', 'test', '-j0']
 | |
|         if network:
 | |
|             cmd.extend(['-u', 'network', '-u', 'urlfetch'])
 | |
|         cmd.extend(['-w', '-r'])
 | |
|         cmd.extend(tests)
 | |
|         self._subprocess_call(cmd, stdout=None)
 | |
| 
 | |
| 
 | |
| class BuildOpenSSL(AbstractBuilder):
 | |
|     library = "OpenSSL"
 | |
|     url_templates = (
 | |
|         "https://www.openssl.org/source/openssl-{v}.tar.gz",
 | |
|         "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
 | |
|     )
 | |
|     src_template = "openssl-{}.tar.gz"
 | |
|     build_template = "openssl-{}"
 | |
|     # only install software, skip docs
 | |
|     install_target = 'install_sw'
 | |
|     depend_target = 'depend'
 | |
| 
 | |
|     def _post_install(self):
 | |
|         if self.version.startswith("3."):
 | |
|             self._post_install_3xx()
 | |
| 
 | |
|     def _build_src(self, config_args=()):
 | |
|         if self.version.startswith("3."):
 | |
|             config_args += ("enable-fips",)
 | |
|         super()._build_src(config_args)
 | |
| 
 | |
|     def _post_install_3xx(self):
 | |
|         # create ssl/ subdir with example configs
 | |
|         # Install FIPS module
 | |
|         self._subprocess_call(
 | |
|             ["make", "-j1", "install_ssldirs", "install_fips"],
 | |
|             cwd=self.build_dir
 | |
|         )
 | |
|         if not os.path.isdir(self.lib_dir):
 | |
|             # 3.0.0-beta2 uses lib64 on 64 bit platforms
 | |
|             lib64 = self.lib_dir + "64"
 | |
|             os.symlink(lib64, self.lib_dir)
 | |
| 
 | |
|     @property
 | |
|     def short_version(self):
 | |
|         """Short version for OpenSSL download URL"""
 | |
|         mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
 | |
|         parsed = tuple(int(m) for m in mo.groups())
 | |
|         if parsed < (1, 0, 0):
 | |
|             return "0.9.x"
 | |
|         if parsed >= (3, 0, 0):
 | |
|             # OpenSSL 3.0.0 -> /old/3.0/
 | |
|             parsed = parsed[:2]
 | |
|         return ".".join(str(i) for i in parsed)
 | |
| 
 | |
| class BuildLibreSSL(AbstractBuilder):
 | |
|     library = "LibreSSL"
 | |
|     url_templates = (
 | |
|         "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
 | |
|     )
 | |
|     src_template = "libressl-{}.tar.gz"
 | |
|     build_template = "libressl-{}"
 | |
| 
 | |
| 
 | |
| def configure_make():
 | |
|     if not os.path.isfile('Makefile'):
 | |
|         log.info('Running ./configure')
 | |
|         subprocess.check_call([
 | |
|             './configure', '--config-cache', '--quiet',
 | |
|             '--with-pydebug'
 | |
|         ])
 | |
| 
 | |
|     log.info('Running make')
 | |
|     subprocess.check_call(['make', '--quiet'])
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     args = parser.parse_args()
 | |
|     if not args.openssl and not args.libressl:
 | |
|         args.openssl = list(OPENSSL_RECENT_VERSIONS)
 | |
|         args.libressl = list(LIBRESSL_RECENT_VERSIONS)
 | |
|         if not args.disable_ancient:
 | |
|             args.openssl.extend(OPENSSL_OLD_VERSIONS)
 | |
|             args.libressl.extend(LIBRESSL_OLD_VERSIONS)
 | |
| 
 | |
|     logging.basicConfig(
 | |
|         level=logging.DEBUG if args.debug else logging.INFO,
 | |
|         format="*** %(levelname)s %(message)s"
 | |
|     )
 | |
| 
 | |
|     start = datetime.now()
 | |
| 
 | |
|     if args.steps in {'modules', 'tests'}:
 | |
|         for name in ['Makefile.pre.in', 'Modules/_ssl.c']:
 | |
|             if not os.path.isfile(os.path.join(PYTHONROOT, name)):
 | |
|                 parser.error(
 | |
|                     "Must be executed from CPython build dir"
 | |
|                 )
 | |
|         if not os.path.samefile('python', sys.executable):
 | |
|             parser.error(
 | |
|                 "Must be executed with ./python from CPython build dir"
 | |
|             )
 | |
|         # check for configure and run make
 | |
|         configure_make()
 | |
| 
 | |
|     # download and register builder
 | |
|     builds = []
 | |
| 
 | |
|     for version in args.openssl:
 | |
|         build = BuildOpenSSL(
 | |
|             version,
 | |
|             args
 | |
|         )
 | |
|         build.install()
 | |
|         builds.append(build)
 | |
| 
 | |
|     for version in args.libressl:
 | |
|         build = BuildLibreSSL(
 | |
|             version,
 | |
|             args
 | |
|         )
 | |
|         build.install()
 | |
|         builds.append(build)
 | |
| 
 | |
|     if args.steps in {'modules', 'tests'}:
 | |
|         for build in builds:
 | |
|             try:
 | |
|                 build.recompile_pymods()
 | |
|                 build.check_pyssl()
 | |
|                 if args.steps == 'tests':
 | |
|                     build.run_python_tests(
 | |
|                         tests=args.tests,
 | |
|                         network=args.network,
 | |
|                     )
 | |
|             except Exception as e:
 | |
|                 log.exception("%s failed", build)
 | |
|                 print("{} failed: {}".format(build, e), file=sys.stderr)
 | |
|                 sys.exit(2)
 | |
| 
 | |
|     log.info("\n{} finished in {}".format(
 | |
|             args.steps.capitalize(),
 | |
|             datetime.now() - start
 | |
|         ))
 | |
|     print('Python: ', sys.version)
 | |
|     if args.steps == 'tests':
 | |
|         if args.tests:
 | |
|             print('Executed Tests:', ' '.join(args.tests))
 | |
|         else:
 | |
|             print('Executed all SSL tests.')
 | |
| 
 | |
|     print('OpenSSL / LibreSSL versions:')
 | |
|     for build in builds:
 | |
|         print("    * {0.library} {0.version}".format(build))
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 | 
