blob: 7bdfd0b92bacf1e4053c0bdd8841f80046f1646a [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
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)2fe15db2021-09-08 01:25:58 -0700415 if not os.path.isdir(self.lib_dir):
416 # 3.0.0-beta2 uses lib64 on 64 bit platforms
417 lib64 = self.lib_dir + "64"
418 os.symlink(lib64, self.lib_dir)
Miss Islington (bot)f8778f92021-05-06 07:53:11 -0700419
Christian Heimes938717f2020-05-15 22:32:25 +0200420 @property
421 def short_version(self):
422 """Short version for OpenSSL download URL"""
Christian Heimes3c586ca2021-04-26 10:54:12 +0200423 mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
424 parsed = tuple(int(m) for m in mo.groups())
425 if parsed < (1, 0, 0):
426 return "0.9.x"
427 if parsed >= (3, 0, 0):
428 # OpenSSL 3.0.0 -> /old/3.0/
429 parsed = parsed[:2]
430 return ".".join(str(i) for i in parsed)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700431
432class BuildLibreSSL(AbstractBuilder):
433 library = "LibreSSL"
Christian Heimes938717f2020-05-15 22:32:25 +0200434 url_templates = (
435 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
436 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700437 src_template = "libressl-{}.tar.gz"
438 build_template = "libressl-{}"
439
440
441def configure_make():
442 if not os.path.isfile('Makefile'):
443 log.info('Running ./configure')
444 subprocess.check_call([
445 './configure', '--config-cache', '--quiet',
446 '--with-pydebug'
447 ])
448
449 log.info('Running make')
450 subprocess.check_call(['make', '--quiet'])
451
452
453def main():
454 args = parser.parse_args()
455 if not args.openssl and not args.libressl:
456 args.openssl = list(OPENSSL_RECENT_VERSIONS)
457 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
458 if not args.disable_ancient:
459 args.openssl.extend(OPENSSL_OLD_VERSIONS)
460 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
461
462 logging.basicConfig(
463 level=logging.DEBUG if args.debug else logging.INFO,
464 format="*** %(levelname)s %(message)s"
465 )
466
467 start = datetime.now()
468
Christian Heimesced9cb52018-01-16 21:02:26 +0100469 if args.steps in {'modules', 'tests'}:
470 for name in ['setup.py', 'Modules/_ssl.c']:
471 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
472 parser.error(
473 "Must be executed from CPython build dir"
474 )
475 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700476 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100477 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700478 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100479 # check for configure and run make
480 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700481
482 # download and register builder
483 builds = []
484
485 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100486 build = BuildOpenSSL(
487 version,
488 args
489 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700490 build.install()
491 builds.append(build)
492
493 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100494 build = BuildLibreSSL(
495 version,
496 args
497 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700498 build.install()
499 builds.append(build)
500
Christian Heimesced9cb52018-01-16 21:02:26 +0100501 if args.steps in {'modules', 'tests'}:
502 for build in builds:
503 try:
504 build.recompile_pymods()
505 build.check_pyssl()
506 if args.steps == 'tests':
507 build.run_python_tests(
508 tests=args.tests,
509 network=args.network,
510 )
511 except Exception as e:
512 log.exception("%s failed", build)
513 print("{} failed: {}".format(build, e), file=sys.stderr)
514 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700515
Christian Heimesced9cb52018-01-16 21:02:26 +0100516 log.info("\n{} finished in {}".format(
517 args.steps.capitalize(),
518 datetime.now() - start
519 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700520 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100521 if args.steps == 'tests':
522 if args.tests:
523 print('Executed Tests:', ' '.join(args.tests))
524 else:
525 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700526
527 print('OpenSSL / LibreSSL versions:')
528 for build in builds:
529 print(" * {0.library} {0.version}".format(build))
530
531
532if __name__ == "__main__":
533 main()