blob: 8d1132f4a95846369a88fe0eba923a6f9c218e60 [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 = [
Christian Heimesa54fc682021-03-30 02:00:34 +020050 "1.1.1k",
Miss Islington (bot)f8778f92021-05-06 07:53:11 -070051 "3.0.0-alpha16"
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
156 module_files = ("Modules/_ssl.c",
157 "Modules/_hashopenssl.c")
158 module_libs = ("_ssl", "_hashlib")
159
Christian Heimesced9cb52018-01-16 21:02:26 +0100160 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700161 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100162 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700163 # installation directory
164 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100165 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700166 )
167 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100168 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700169 self.src_file = os.path.join(
170 self.src_dir, self.src_template.format(version))
171 # build directory (removed after install)
172 self.build_dir = os.path.join(
173 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400174 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700175
176 def __str__(self):
177 return "<{0.__class__.__name__} for {0.version}>".format(self)
178
179 def __eq__(self, other):
180 if not isinstance(other, AbstractBuilder):
181 return NotImplemented
182 return (
183 self.library == other.library
184 and self.version == other.version
185 )
186
187 def __hash__(self):
188 return hash((self.library, self.version))
189
190 @property
Christian Heimes938717f2020-05-15 22:32:25 +0200191 def short_version(self):
192 """Short version for OpenSSL download URL"""
193 return None
194
195 @property
Christian Heimesd3b9f972017-09-06 18:59:22 -0700196 def openssl_cli(self):
197 """openssl CLI binary"""
198 return os.path.join(self.install_dir, "bin", "openssl")
199
200 @property
201 def openssl_version(self):
202 """output of 'bin/openssl version'"""
203 cmd = [self.openssl_cli, "version"]
204 return self._subprocess_output(cmd)
205
206 @property
207 def pyssl_version(self):
208 """Value of ssl.OPENSSL_VERSION"""
209 cmd = [
210 sys.executable,
211 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
212 ]
213 return self._subprocess_output(cmd)
214
215 @property
216 def include_dir(self):
217 return os.path.join(self.install_dir, "include")
218
219 @property
220 def lib_dir(self):
221 return os.path.join(self.install_dir, "lib")
222
223 @property
224 def has_openssl(self):
225 return os.path.isfile(self.openssl_cli)
226
227 @property
228 def has_src(self):
229 return os.path.isfile(self.src_file)
230
231 def _subprocess_call(self, cmd, env=None, **kwargs):
232 log.debug("Call '{}'".format(" ".join(cmd)))
233 return subprocess.check_call(cmd, env=env, **kwargs)
234
235 def _subprocess_output(self, cmd, env=None, **kwargs):
236 log.debug("Call '{}'".format(" ".join(cmd)))
237 if env is None:
238 env = os.environ.copy()
239 env["LD_LIBRARY_PATH"] = self.lib_dir
240 out = subprocess.check_output(cmd, env=env, **kwargs)
241 return out.strip().decode("utf-8")
242
243 def _download_src(self):
244 """Download sources"""
245 src_dir = os.path.dirname(self.src_file)
246 if not os.path.isdir(src_dir):
247 os.makedirs(src_dir)
Christian Heimes938717f2020-05-15 22:32:25 +0200248 data = None
249 for url_template in self.url_templates:
250 url = url_template.format(v=self.version, s=self.short_version)
251 log.info("Downloading from {}".format(url))
252 try:
253 req = urlopen(url)
254 # KISS, read all, write all
255 data = req.read()
256 except HTTPError as e:
257 log.error(
258 "Download from {} has from failed: {}".format(url, e)
259 )
260 else:
261 log.info("Successfully downloaded from {}".format(url))
262 break
263 if data is None:
264 raise ValueError("All download URLs have failed")
Christian Heimesd3b9f972017-09-06 18:59:22 -0700265 log.info("Storing {}".format(self.src_file))
266 with open(self.src_file, "wb") as f:
267 f.write(data)
268
269 def _unpack_src(self):
270 """Unpack tar.gz bundle"""
271 # cleanup
272 if os.path.isdir(self.build_dir):
273 shutil.rmtree(self.build_dir)
274 os.makedirs(self.build_dir)
275
276 tf = tarfile.open(self.src_file)
277 name = self.build_template.format(self.version)
278 base = name + '/'
279 # force extraction into build dir
280 members = tf.getmembers()
281 for member in list(members):
282 if member.name == name:
283 members.remove(member)
284 elif not member.name.startswith(base):
285 raise ValueError(member.name, base)
286 member.name = member.name[len(base):].lstrip('/')
287 log.info("Unpacking files to {}".format(self.build_dir))
288 tf.extractall(self.build_dir, members)
289
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700290 def _build_src(self, config_args=()):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700291 """Now build openssl"""
292 log.info("Running build in {}".format(self.build_dir))
293 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200294 cmd = [
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700295 "./config", *config_args,
Christian Heimes529525f2018-05-23 22:24:45 +0200296 "shared", "--debug",
297 "--prefix={}".format(self.install_dir)
298 ]
Christian Heimesa871f692020-06-01 08:58:14 +0200299 # cmd.extend(["no-deprecated", "--api=1.1.0"])
Christian Heimes529525f2018-05-23 22:24:45 +0200300 env = os.environ.copy()
301 # set rpath
302 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400303 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400304 env['SYSTEM'] = self.system
305 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimes32eba612021-03-19 10:29:25 +0100306 if self.depend_target:
307 self._subprocess_call(
308 ["make", "-j1", self.depend_target], cwd=cwd, env=env
309 )
310 self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700311
Christian Heimes529525f2018-05-23 22:24:45 +0200312 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100313 self._subprocess_call(
314 ["make", "-j1", self.install_target],
315 cwd=self.build_dir
316 )
Christian Heimes62d618c2020-05-15 18:48:25 +0200317 self._post_install()
Christian Heimes529525f2018-05-23 22:24:45 +0200318 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700319 shutil.rmtree(self.build_dir)
320
Christian Heimes62d618c2020-05-15 18:48:25 +0200321 def _post_install(self):
322 pass
323
Christian Heimesd3b9f972017-09-06 18:59:22 -0700324 def install(self):
325 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200326 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700327 if not self.has_src:
328 self._download_src()
329 else:
330 log.debug("Already has src {}".format(self.src_file))
331 self._unpack_src()
332 self._build_src()
333 self._make_install()
334 else:
335 log.info("Already has installation {}".format(self.install_dir))
336 # validate installation
337 version = self.openssl_version
338 if self.version not in version:
339 raise ValueError(version)
340
341 def recompile_pymods(self):
342 log.warning("Using build from {}".format(self.build_dir))
343 # force a rebuild of all modules that use OpenSSL APIs
344 for fname in self.module_files:
345 os.utime(fname, None)
346 # remove all build artefacts
347 for root, dirs, files in os.walk('build'):
348 for filename in files:
349 if filename.startswith(self.module_libs):
350 os.unlink(os.path.join(root, filename))
351
352 # overwrite header and library search paths
353 env = os.environ.copy()
354 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
355 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
356 # set rpath
357 env["LD_RUN_PATH"] = self.lib_dir
358
359 log.info("Rebuilding Python modules")
360 cmd = [sys.executable, "setup.py", "build"]
361 self._subprocess_call(cmd, env=env)
362 self.check_imports()
363
364 def check_imports(self):
365 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
366 self._subprocess_call(cmd)
367
368 def check_pyssl(self):
369 version = self.pyssl_version
370 if self.version not in version:
371 raise ValueError(version)
372
373 def run_python_tests(self, tests, network=True):
374 if not tests:
375 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
376 elif sys.version_info < (3, 3):
377 cmd = [sys.executable, '-m', 'test.regrtest']
378 else:
379 cmd = [sys.executable, '-m', 'test', '-j0']
380 if network:
381 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
382 cmd.extend(['-w', '-r'])
383 cmd.extend(tests)
384 self._subprocess_call(cmd, stdout=None)
385
386
387class BuildOpenSSL(AbstractBuilder):
388 library = "OpenSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200389 url_templates = (
390 "https://www.openssl.org/source/openssl-{v}.tar.gz",
391 "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
392 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700393 src_template = "openssl-{}.tar.gz"
394 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100395 # only install software, skip docs
396 install_target = 'install_sw'
Christian Heimes32eba612021-03-19 10:29:25 +0100397 depend_target = 'depend'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700398
Christian Heimes62d618c2020-05-15 18:48:25 +0200399 def _post_install(self):
400 if self.version.startswith("3.0"):
401 self._post_install_300()
402
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700403 def _build_src(self, config_args=()):
404 if self.version.startswith("3.0"):
405 config_args += ("enable-fips",)
406 super()._build_src(config_args)
407
Christian Heimes62d618c2020-05-15 18:48:25 +0200408 def _post_install_300(self):
409 # create ssl/ subdir with example configs
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700410 # Install FIPS module
Christian Heimes62d618c2020-05-15 18:48:25 +0200411 self._subprocess_call(
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700412 ["make", "-j1", "install_ssldirs", "install_fips"],
Christian Heimes62d618c2020-05-15 18:48:25 +0200413 cwd=self.build_dir
414 )
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700415
Christian Heimes938717f2020-05-15 22:32:25 +0200416 @property
417 def short_version(self):
418 """Short version for OpenSSL download URL"""
Christian Heimes3c586ca2021-04-26 10:54:12 +0200419 mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
420 parsed = tuple(int(m) for m in mo.groups())
421 if parsed < (1, 0, 0):
422 return "0.9.x"
423 if parsed >= (3, 0, 0):
424 # OpenSSL 3.0.0 -> /old/3.0/
425 parsed = parsed[:2]
426 return ".".join(str(i) for i in parsed)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700427
428class BuildLibreSSL(AbstractBuilder):
429 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200430 url_templates = (
431 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
432 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700433 src_template = "libressl-{}.tar.gz"
434 build_template = "libressl-{}"
435
436
437def configure_make():
438 if not os.path.isfile('Makefile'):
439 log.info('Running ./configure')
440 subprocess.check_call([
441 './configure', '--config-cache', '--quiet',
442 '--with-pydebug'
443 ])
444
445 log.info('Running make')
446 subprocess.check_call(['make', '--quiet'])
447
448
449def main():
450 args = parser.parse_args()
451 if not args.openssl and not args.libressl:
452 args.openssl = list(OPENSSL_RECENT_VERSIONS)
453 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
454 if not args.disable_ancient:
455 args.openssl.extend(OPENSSL_OLD_VERSIONS)
456 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
457
458 logging.basicConfig(
459 level=logging.DEBUG if args.debug else logging.INFO,
460 format="*** %(levelname)s %(message)s"
461 )
462
463 start = datetime.now()
464
Christian Heimesced9cb52018-01-16 21:02:26 +0100465 if args.steps in {'modules', 'tests'}:
466 for name in ['setup.py', 'Modules/_ssl.c']:
467 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
468 parser.error(
469 "Must be executed from CPython build dir"
470 )
471 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700472 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100473 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700474 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100475 # check for configure and run make
476 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700477
478 # download and register builder
479 builds = []
480
481 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100482 build = BuildOpenSSL(
483 version,
484 args
485 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700486 build.install()
487 builds.append(build)
488
489 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100490 build = BuildLibreSSL(
491 version,
492 args
493 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700494 build.install()
495 builds.append(build)
496
Christian Heimesced9cb52018-01-16 21:02:26 +0100497 if args.steps in {'modules', 'tests'}:
498 for build in builds:
499 try:
500 build.recompile_pymods()
501 build.check_pyssl()
502 if args.steps == 'tests':
503 build.run_python_tests(
504 tests=args.tests,
505 network=args.network,
506 )
507 except Exception as e:
508 log.exception("%s failed", build)
509 print("{} failed: {}".format(build, e), file=sys.stderr)
510 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700511
Christian Heimesced9cb52018-01-16 21:02:26 +0100512 log.info("\n{} finished in {}".format(
513 args.steps.capitalize(),
514 datetime.now() - start
515 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700516 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100517 if args.steps == 'tests':
518 if args.tests:
519 print('Executed Tests:', ' '.join(args.tests))
520 else:
521 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700522
523 print('OpenSSL / LibreSSL versions:')
524 for build in builds:
525 print(" * {0.library} {0.version}".format(build))
526
527
528if __name__ == "__main__":
529 main()