blob: ba2663e9a399beaec98317b524354bc29a5814be [file] [log] [blame]
Christian Heimesd3b9f972017-09-06 18:59:22 -07001#!./python
2"""Run Python tests against multiple installations of OpenSSL and LibreSSL
3
4The script
5
6 (1) downloads OpenSSL / LibreSSL tar bundle
7 (2) extracts it to ./src
8 (3) compiles OpenSSL / LibreSSL
9 (4) installs OpenSSL / LibreSSL into ../multissl/$LIB/$VERSION/
10 (5) forces a recompilation of Python modules using the
11 header and library files from ../multissl/$LIB/$VERSION/
12 (6) runs Python's test suite
13
14The script must be run with Python's build directory as current working
15directory.
16
17The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend
18search paths for header files and shared libraries. It's known to work on
19Linux with GCC and clang.
20
21Please keep this script compatible with Python 2.7, and 3.4 to 3.7.
22
23(c) 2013-2017 Christian Heimes <christian@python.org>
24"""
25from __future__ import print_function
26
27import argparse
28from datetime import datetime
29import logging
30import os
31try:
32 from urllib.request import urlopen
Christian Heimes938717f2020-05-15 22:32:25 +020033 from urllib.error import HTTPError
Christian Heimesd3b9f972017-09-06 18:59:22 -070034except ImportError:
Christian Heimes938717f2020-05-15 22:32:25 +020035 from urllib2 import urlopen, HTTPError
Christian Heimes3c586ca2021-04-26 10:54:12 +020036import re
Christian Heimesd3b9f972017-09-06 18:59:22 -070037import shutil
Christian Heimes938717f2020-05-15 22:32:25 +020038import string
39import subprocess
Christian Heimesd3b9f972017-09-06 18:59:22 -070040import sys
41import tarfile
42
43
44log = logging.getLogger("multissl")
45
46OPENSSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070047]
48
49OPENSSL_RECENT_VERSIONS = [
Miss Islington (bot)dc46f4c2021-08-30 06:07:38 -070050 "1.1.1l",
Miss Islington (bot)2fe15db2021-09-08 01:25:58 -070051 "3.0.0"
Christian Heimesd3b9f972017-09-06 18:59:22 -070052]
53
54LIBRESSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070055]
56
57LIBRESSL_RECENT_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070058]
59
60# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010061HERE = os.path.dirname(os.path.abspath(__file__))
62PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
63MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
64
Christian Heimesd3b9f972017-09-06 18:59:22 -070065
66parser = argparse.ArgumentParser(
67 prog='multissl',
68 description=(
69 "Run CPython tests with multiple OpenSSL and LibreSSL "
70 "versions."
71 )
72)
73parser.add_argument(
74 '--debug',
75 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020076 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070077)
78parser.add_argument(
79 '--disable-ancient',
80 action='store_true',
Christian Heimes6e8cda92020-05-16 03:33:05 +020081 help="Don't test OpenSSL and LibreSSL versions without upstream support",
Christian Heimesd3b9f972017-09-06 18:59:22 -070082)
83parser.add_argument(
84 '--openssl',
85 nargs='+',
86 default=(),
87 help=(
88 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
89 "OpenSSL and LibreSSL versions are given."
90 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
91)
92parser.add_argument(
93 '--libressl',
94 nargs='+',
95 default=(),
96 help=(
97 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
98 "OpenSSL and LibreSSL versions are given."
99 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
100)
101parser.add_argument(
102 '--tests',
103 nargs='*',
104 default=(),
105 help="Python tests to run, defaults to all SSL related tests.",
106)
107parser.add_argument(
108 '--base-directory',
109 default=MULTISSL_DIR,
110 help="Base directory for OpenSSL / LibreSSL sources and builds."
111)
112parser.add_argument(
113 '--no-network',
114 action='store_false',
115 dest='network',
116 help="Disable network tests."
117)
118parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100119 '--steps',
120 choices=['library', 'modules', 'tests'],
121 default='tests',
122 help=(
123 "Which steps to perform. 'library' downloads and compiles OpenSSL "
124 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
125 "all and runs the test suite."
126 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700127)
Steve Dowere5f41d22018-05-16 17:50:29 -0400128parser.add_argument(
129 '--system',
130 default='',
131 help="Override the automatic system type detection."
132)
Christian Heimes529525f2018-05-23 22:24:45 +0200133parser.add_argument(
134 '--force',
135 action='store_true',
136 dest='force',
137 help="Force build and installation."
138)
139parser.add_argument(
140 '--keep-sources',
141 action='store_true',
142 dest='keep_sources',
143 help="Keep original sources for debugging."
144)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700145
146
147class AbstractBuilder(object):
148 library = None
Christian Heimes938717f2020-05-15 22:32:25 +0200149 url_templates = None
Christian Heimesd3b9f972017-09-06 18:59:22 -0700150 src_template = None
151 build_template = None
Christian Heimes32eba612021-03-19 10:29:25 +0100152 depend_target = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100153 install_target = 'install'
Christian Heimes32eba612021-03-19 10:29:25 +0100154 jobs = os.cpu_count()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700155
Miss Islington (bot)9e7a2e42021-11-22 02:14:28 -0800156 module_files = (
157 os.path.join(PYTHONROOT, "Modules/_ssl.c"),
158 os.path.join(PYTHONROOT, "Modules/_hashopenssl.c"),
159 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700160 module_libs = ("_ssl", "_hashlib")
161
Christian Heimesced9cb52018-01-16 21:02:26 +0100162 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700163 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100164 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700165 # installation directory
166 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100167 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700168 )
169 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100170 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700171 self.src_file = os.path.join(
172 self.src_dir, self.src_template.format(version))
173 # build directory (removed after install)
174 self.build_dir = os.path.join(
175 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400176 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700177
178 def __str__(self):
179 return "<{0.__class__.__name__} for {0.version}>".format(self)
180
181 def __eq__(self, other):
182 if not isinstance(other, AbstractBuilder):
183 return NotImplemented
184 return (
185 self.library == other.library
186 and self.version == other.version
187 )
188
189 def __hash__(self):
190 return hash((self.library, self.version))
191
192 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200193 def short_version(self):
194 """Short version for OpenSSL download URL"""
195 return None
196
197 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700198 def openssl_cli(self):
199 """openssl CLI binary"""
200 return os.path.join(self.install_dir, "bin", "openssl")
201
202 @property
203 def openssl_version(self):
204 """output of 'bin/openssl version'"""
205 cmd = [self.openssl_cli, "version"]
206 return self._subprocess_output(cmd)
207
208 @property
209 def pyssl_version(self):
210 """Value of ssl.OPENSSL_VERSION"""
211 cmd = [
212 sys.executable,
213 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
214 ]
215 return self._subprocess_output(cmd)
216
217 @property
218 def include_dir(self):
219 return os.path.join(self.install_dir, "include")
220
221 @property
222 def lib_dir(self):
223 return os.path.join(self.install_dir, "lib")
224
225 @property
226 def has_openssl(self):
227 return os.path.isfile(self.openssl_cli)
228
229 @property
230 def has_src(self):
231 return os.path.isfile(self.src_file)
232
233 def _subprocess_call(self, cmd, env=None, **kwargs):
234 log.debug("Call '{}'".format(" ".join(cmd)))
235 return subprocess.check_call(cmd, env=env, **kwargs)
236
237 def _subprocess_output(self, cmd, env=None, **kwargs):
238 log.debug("Call '{}'".format(" ".join(cmd)))
239 if env is None:
240 env = os.environ.copy()
241 env["LD_LIBRARY_PATH"] = self.lib_dir
242 out = subprocess.check_output(cmd, env=env, **kwargs)
243 return out.strip().decode("utf-8")
244
245 def _download_src(self):
246 """Download sources"""
247 src_dir = os.path.dirname(self.src_file)
248 if not os.path.isdir(src_dir):
249 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200250 data = None
251 for url_template in self.url_templates:
252 url = url_template.format(v=self.version, s=self.short_version)
253 log.info("Downloading from {}".format(url))
254 try:
255 req = urlopen(url)
256 # KISS, read all, write all
257 data = req.read()
258 except HTTPError as e:
259 log.error(
260 "Download from {} has from failed: {}".format(url, e)
261 )
262 else:
263 log.info("Successfully downloaded from {}".format(url))
264 break
265 if data is None:
266 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700267 log.info("Storing {}".format(self.src_file))
268 with open(self.src_file, "wb") as f:
269 f.write(data)
270
271 def _unpack_src(self):
272 """Unpack tar.gz bundle"""
273 # cleanup
274 if os.path.isdir(self.build_dir):
275 shutil.rmtree(self.build_dir)
276 os.makedirs(self.build_dir)
277
278 tf = tarfile.open(self.src_file)
279 name = self.build_template.format(self.version)
280 base = name + '/'
281 # force extraction into build dir
282 members = tf.getmembers()
283 for member in list(members):
284 if member.name == name:
285 members.remove(member)
286 elif not member.name.startswith(base):
287 raise ValueError(member.name, base)
288 member.name = member.name[len(base):].lstrip('/')
289 log.info("Unpacking files to {}".format(self.build_dir))
290 tf.extractall(self.build_dir, members)
291
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700292 def _build_src(self, config_args=()):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700293 """Now build openssl"""
294 log.info("Running build in {}".format(self.build_dir))
295 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200296 cmd = [
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700297 "./config", *config_args,
Christian Heimes529525f2018-05-23 22:24:45 +0200298 "shared", "--debug",
299 "--prefix={}".format(self.install_dir)
300 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200301 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200302 env = os.environ.copy()
303 # set rpath
304 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400305 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400306 env['SYSTEM'] = self.system
307 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimes32eba612021-03-19 10:29:25 +0100308 if self.depend_target:
309 self._subprocess_call(
310 ["make", "-j1", self.depend_target], cwd=cwd, env=env
311 )
312 self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700313
Christian Heimes529525f2018-05-23 22:24:45 +0200314 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100315 self._subprocess_call(
316 ["make", "-j1", self.install_target],
317 cwd=self.build_dir
318 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200319 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200320 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700321 shutil.rmtree(self.build_dir)
322
Christian Heimes62d618c2020-05-15 18:48:25 +0200323 def _post_install(self):
324 pass
325
Christian Heimesd3b9f972017-09-06 18:59:22 -0700326 def install(self):
327 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200328 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700329 if not self.has_src:
330 self._download_src()
331 else:
332 log.debug("Already has src {}".format(self.src_file))
333 self._unpack_src()
334 self._build_src()
335 self._make_install()
336 else:
337 log.info("Already has installation {}".format(self.install_dir))
338 # validate installation
339 version = self.openssl_version
340 if self.version not in version:
341 raise ValueError(version)
342
343 def recompile_pymods(self):
344 log.warning("Using build from {}".format(self.build_dir))
345 # force a rebuild of all modules that use OpenSSL APIs
346 for fname in self.module_files:
347 os.utime(fname, None)
348 # remove all build artefacts
349 for root, dirs, files in os.walk('build'):
350 for filename in files:
351 if filename.startswith(self.module_libs):
352 os.unlink(os.path.join(root, filename))
353
354 # overwrite header and library search paths
355 env = os.environ.copy()
356 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
357 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
358 # set rpath
359 env["LD_RUN_PATH"] = self.lib_dir
360
361 log.info("Rebuilding Python modules")
Miss Islington (bot)9e7a2e42021-11-22 02:14:28 -0800362 cmd = [sys.executable, os.path.join(PYTHONROOT, "setup.py"), "build"]
Christian Heimesd3b9f972017-09-06 18:59:22 -0700363 self._subprocess_call(cmd, env=env)
364 self.check_imports()
365
366 def check_imports(self):
367 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
368 self._subprocess_call(cmd)
369
370 def check_pyssl(self):
371 version = self.pyssl_version
372 if self.version not in version:
373 raise ValueError(version)
374
375 def run_python_tests(self, tests, network=True):
376 if not tests:
Miss Islington (bot)9e7a2e42021-11-22 02:14:28 -0800377 cmd = [
378 sys.executable,
379 os.path.join(PYTHONROOT, 'Lib/test/ssltests.py'),
380 '-j0'
381 ]
Christian Heimesd3b9f972017-09-06 18:59:22 -0700382 elif sys.version_info < (3, 3):
383 cmd = [sys.executable, '-m', 'test.regrtest']
384 else:
385 cmd = [sys.executable, '-m', 'test', '-j0']
386 if network:
387 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
388 cmd.extend(['-w', '-r'])
389 cmd.extend(tests)
390 self._subprocess_call(cmd, stdout=None)
391
392
393class BuildOpenSSL(AbstractBuilder):
394 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200395 url_templates = (
396 "https://www.openssl.org/source/openssl-{v}.tar.gz",
397 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
398 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700399 src_template = "openssl-{}.tar.gz"
400 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100401 # only install software, skip docs
402 install_target = 'install_sw'
Christian Heimes32eba612021-03-19 10:29:25 +0100403 depend_target = 'depend'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700404
Christian Heimes62d618c2020-05-15 18:48:25 +0200405 def _post_install(self):
406 if self.version.startswith("3.0"):
407 self._post_install_300()
408
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700409 def _build_src(self, config_args=()):
410 if self.version.startswith("3.0"):
411 config_args += ("enable-fips",)
412 super()._build_src(config_args)
413
Christian Heimes62d618c2020-05-15 18:48:25 +0200414 def _post_install_300(self):
415 # create ssl/ subdir with example configs
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700416 # Install FIPS module
Christian Heimes62d618c2020-05-15 18:48:25 +0200417 self._subprocess_call(
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700418 ["make", "-j1", "install_ssldirs", "install_fips"],
Christian Heimes62d618c2020-05-15 18:48:25 +0200419 cwd=self.build_dir
420 )
Miss Islington (bot)2fe15db2021-09-08 01:25:58 -0700421 if not os.path.isdir(self.lib_dir):
422 # 3.0.0-beta2 uses lib64 on 64 bit platforms
423 lib64 = self.lib_dir + "64"
424 os.symlink(lib64, self.lib_dir)
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700425
Christian Heimes938717f2020-05-15 22:32:25 +0200426 @property
427 def short_version(self):
428 """Short version for OpenSSL download URL"""
Christian Heimes3c586ca2021-04-26 10:54:12 +0200429 mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
430 parsed = tuple(int(m) for m in mo.groups())
431 if parsed < (1, 0, 0):
432 return "0.9.x"
433 if parsed >= (3, 0, 0):
434 # OpenSSL 3.0.0 -> /old/3.0/
435 parsed = parsed[:2]
436 return ".".join(str(i) for i in parsed)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700437
438class BuildLibreSSL(AbstractBuilder):
439 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200440 url_templates = (
441 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
442 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700443 src_template = "libressl-{}.tar.gz"
444 build_template = "libressl-{}"
445
446
447def configure_make():
448 if not os.path.isfile('Makefile'):
449 log.info('Running ./configure')
450 subprocess.check_call([
451 './configure', '--config-cache', '--quiet',
452 '--with-pydebug'
453 ])
454
455 log.info('Running make')
456 subprocess.check_call(['make', '--quiet'])
457
458
459def main():
460 args = parser.parse_args()
461 if not args.openssl and not args.libressl:
462 args.openssl = list(OPENSSL_RECENT_VERSIONS)
463 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
464 if not args.disable_ancient:
465 args.openssl.extend(OPENSSL_OLD_VERSIONS)
466 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
467
468 logging.basicConfig(
469 level=logging.DEBUG if args.debug else logging.INFO,
470 format="*** %(levelname)s %(message)s"
471 )
472
473 start = datetime.now()
474
Christian Heimesced9cb52018-01-16 21:02:26 +0100475 if args.steps in {'modules', 'tests'}:
476 for name in ['setup.py', 'Modules/_ssl.c']:
477 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
478 parser.error(
479 "Must be executed from CPython build dir"
480 )
481 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700482 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100483 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700484 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100485 # check for configure and run make
486 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700487
488 # download and register builder
489 builds = []
490
491 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100492 build = BuildOpenSSL(
493 version,
494 args
495 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700496 build.install()
497 builds.append(build)
498
499 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100500 build = BuildLibreSSL(
501 version,
502 args
503 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700504 build.install()
505 builds.append(build)
506
Christian Heimesced9cb52018-01-16 21:02:26 +0100507 if args.steps in {'modules', 'tests'}:
508 for build in builds:
509 try:
510 build.recompile_pymods()
511 build.check_pyssl()
512 if args.steps == 'tests':
513 build.run_python_tests(
514 tests=args.tests,
515 network=args.network,
516 )
517 except Exception as e:
518 log.exception("%s failed", build)
519 print("{} failed: {}".format(build, e), file=sys.stderr)
520 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700521
Christian Heimesced9cb52018-01-16 21:02:26 +0100522 log.info("\n{} finished in {}".format(
523 args.steps.capitalize(),
524 datetime.now() - start
525 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700526 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100527 if args.steps == 'tests':
528 if args.tests:
529 print('Executed Tests:', ' '.join(args.tests))
530 else:
531 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700532
533 print('OpenSSL / LibreSSL versions:')
534 for build in builds:
535 print(" * {0.library} {0.version}".format(build))
536
537
538if __name__ == "__main__":
539 main()