blob: 7fda4df55a678be10361f2d74506169611f796a2 [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
33except ImportError:
34 from urllib2 import urlopen
35import subprocess
36import shutil
37import sys
38import tarfile
39
40
41log = logging.getLogger("multissl")
42
43OPENSSL_OLD_VERSIONS = [
Christian Heimes05d9fe32018-02-27 08:55:39 +010044 "1.0.2",
Christian Heimesd3b9f972017-09-06 18:59:22 -070045]
46
47OPENSSL_RECENT_VERSIONS = [
Christian Heimes06651ee2019-06-03 20:10:19 +020048 "1.0.2s",
49 "1.1.0k",
50 "1.1.1c",
Christian Heimesd3b9f972017-09-06 18:59:22 -070051]
52
53LIBRESSL_OLD_VERSIONS = [
Christian Heimese35d1ba2019-06-03 20:40:15 +020054 "2.9.2",
Christian Heimesd3b9f972017-09-06 18:59:22 -070055]
56
57LIBRESSL_RECENT_VERSIONS = [
Christian Heimese35d1ba2019-06-03 20:40:15 +020058 "2.8.3",
Christian Heimesd3b9f972017-09-06 18:59:22 -070059]
60
61# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010062HERE = os.path.dirname(os.path.abspath(__file__))
63PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
64MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
65
Christian Heimesd3b9f972017-09-06 18:59:22 -070066
67parser = argparse.ArgumentParser(
68 prog='multissl',
69 description=(
70 "Run CPython tests with multiple OpenSSL and LibreSSL "
71 "versions."
72 )
73)
74parser.add_argument(
75 '--debug',
76 action='store_true',
Christian Heimes529525f2018-05-23 22:24:45 +020077 help="Enable debug logging",
Christian Heimesd3b9f972017-09-06 18:59:22 -070078)
79parser.add_argument(
80 '--disable-ancient',
81 action='store_true',
82 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.",
83)
84parser.add_argument(
85 '--openssl',
86 nargs='+',
87 default=(),
88 help=(
89 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
90 "OpenSSL and LibreSSL versions are given."
91 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
92)
93parser.add_argument(
94 '--libressl',
95 nargs='+',
96 default=(),
97 help=(
98 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
99 "OpenSSL and LibreSSL versions are given."
100 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
101)
102parser.add_argument(
103 '--tests',
104 nargs='*',
105 default=(),
106 help="Python tests to run, defaults to all SSL related tests.",
107)
108parser.add_argument(
109 '--base-directory',
110 default=MULTISSL_DIR,
111 help="Base directory for OpenSSL / LibreSSL sources and builds."
112)
113parser.add_argument(
114 '--no-network',
115 action='store_false',
116 dest='network',
117 help="Disable network tests."
118)
119parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100120 '--steps',
121 choices=['library', 'modules', 'tests'],
122 default='tests',
123 help=(
124 "Which steps to perform. 'library' downloads and compiles OpenSSL "
125 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
126 "all and runs the test suite."
127 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700128)
Steve Dowere5f41d22018-05-16 17:50:29 -0400129parser.add_argument(
130 '--system',
131 default='',
132 help="Override the automatic system type detection."
133)
Christian Heimes529525f2018-05-23 22:24:45 +0200134parser.add_argument(
135 '--force',
136 action='store_true',
137 dest='force',
138 help="Force build and installation."
139)
140parser.add_argument(
141 '--keep-sources',
142 action='store_true',
143 dest='keep_sources',
144 help="Keep original sources for debugging."
145)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700146
147
148class AbstractBuilder(object):
149 library = None
150 url_template = None
151 src_template = None
152 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100153 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700154
155 module_files = ("Modules/_ssl.c",
156 "Modules/_hashopenssl.c")
157 module_libs = ("_ssl", "_hashlib")
158
Christian Heimesced9cb52018-01-16 21:02:26 +0100159 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700160 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100161 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700162 # installation directory
163 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100164 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700165 )
166 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100167 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700168 self.src_file = os.path.join(
169 self.src_dir, self.src_template.format(version))
170 # build directory (removed after install)
171 self.build_dir = os.path.join(
172 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400173 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700174
175 def __str__(self):
176 return "<{0.__class__.__name__} for {0.version}>".format(self)
177
178 def __eq__(self, other):
179 if not isinstance(other, AbstractBuilder):
180 return NotImplemented
181 return (
182 self.library == other.library
183 and self.version == other.version
184 )
185
186 def __hash__(self):
187 return hash((self.library, self.version))
188
189 @property
190 def openssl_cli(self):
191 """openssl CLI binary"""
192 return os.path.join(self.install_dir, "bin", "openssl")
193
194 @property
195 def openssl_version(self):
196 """output of 'bin/openssl version'"""
197 cmd = [self.openssl_cli, "version"]
198 return self._subprocess_output(cmd)
199
200 @property
201 def pyssl_version(self):
202 """Value of ssl.OPENSSL_VERSION"""
203 cmd = [
204 sys.executable,
205 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
206 ]
207 return self._subprocess_output(cmd)
208
209 @property
210 def include_dir(self):
211 return os.path.join(self.install_dir, "include")
212
213 @property
214 def lib_dir(self):
215 return os.path.join(self.install_dir, "lib")
216
217 @property
218 def has_openssl(self):
219 return os.path.isfile(self.openssl_cli)
220
221 @property
222 def has_src(self):
223 return os.path.isfile(self.src_file)
224
225 def _subprocess_call(self, cmd, env=None, **kwargs):
226 log.debug("Call '{}'".format(" ".join(cmd)))
227 return subprocess.check_call(cmd, env=env, **kwargs)
228
229 def _subprocess_output(self, cmd, env=None, **kwargs):
230 log.debug("Call '{}'".format(" ".join(cmd)))
231 if env is None:
232 env = os.environ.copy()
233 env["LD_LIBRARY_PATH"] = self.lib_dir
234 out = subprocess.check_output(cmd, env=env, **kwargs)
235 return out.strip().decode("utf-8")
236
237 def _download_src(self):
238 """Download sources"""
239 src_dir = os.path.dirname(self.src_file)
240 if not os.path.isdir(src_dir):
241 os.makedirs(src_dir)
242 url = self.url_template.format(self.version)
243 log.info("Downloading from {}".format(url))
244 req = urlopen(url)
245 # KISS, read all, write all
246 data = req.read()
247 log.info("Storing {}".format(self.src_file))
248 with open(self.src_file, "wb") as f:
249 f.write(data)
250
251 def _unpack_src(self):
252 """Unpack tar.gz bundle"""
253 # cleanup
254 if os.path.isdir(self.build_dir):
255 shutil.rmtree(self.build_dir)
256 os.makedirs(self.build_dir)
257
258 tf = tarfile.open(self.src_file)
259 name = self.build_template.format(self.version)
260 base = name + '/'
261 # force extraction into build dir
262 members = tf.getmembers()
263 for member in list(members):
264 if member.name == name:
265 members.remove(member)
266 elif not member.name.startswith(base):
267 raise ValueError(member.name, base)
268 member.name = member.name[len(base):].lstrip('/')
269 log.info("Unpacking files to {}".format(self.build_dir))
270 tf.extractall(self.build_dir, members)
271
272 def _build_src(self):
273 """Now build openssl"""
274 log.info("Running build in {}".format(self.build_dir))
275 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200276 cmd = [
277 "./config",
278 "shared", "--debug",
279 "--prefix={}".format(self.install_dir)
280 ]
281 env = os.environ.copy()
282 # set rpath
283 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400284 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400285 env['SYSTEM'] = self.system
286 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700287 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400288 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700289
Christian Heimes529525f2018-05-23 22:24:45 +0200290 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100291 self._subprocess_call(
292 ["make", "-j1", self.install_target],
293 cwd=self.build_dir
294 )
Christian Heimes529525f2018-05-23 22:24:45 +0200295 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700296 shutil.rmtree(self.build_dir)
297
298 def install(self):
299 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200300 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700301 if not self.has_src:
302 self._download_src()
303 else:
304 log.debug("Already has src {}".format(self.src_file))
305 self._unpack_src()
306 self._build_src()
307 self._make_install()
308 else:
309 log.info("Already has installation {}".format(self.install_dir))
310 # validate installation
311 version = self.openssl_version
312 if self.version not in version:
313 raise ValueError(version)
314
315 def recompile_pymods(self):
316 log.warning("Using build from {}".format(self.build_dir))
317 # force a rebuild of all modules that use OpenSSL APIs
318 for fname in self.module_files:
319 os.utime(fname, None)
320 # remove all build artefacts
321 for root, dirs, files in os.walk('build'):
322 for filename in files:
323 if filename.startswith(self.module_libs):
324 os.unlink(os.path.join(root, filename))
325
326 # overwrite header and library search paths
327 env = os.environ.copy()
328 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
329 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
330 # set rpath
331 env["LD_RUN_PATH"] = self.lib_dir
332
333 log.info("Rebuilding Python modules")
334 cmd = [sys.executable, "setup.py", "build"]
335 self._subprocess_call(cmd, env=env)
336 self.check_imports()
337
338 def check_imports(self):
339 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
340 self._subprocess_call(cmd)
341
342 def check_pyssl(self):
343 version = self.pyssl_version
344 if self.version not in version:
345 raise ValueError(version)
346
347 def run_python_tests(self, tests, network=True):
348 if not tests:
349 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
350 elif sys.version_info < (3, 3):
351 cmd = [sys.executable, '-m', 'test.regrtest']
352 else:
353 cmd = [sys.executable, '-m', 'test', '-j0']
354 if network:
355 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
356 cmd.extend(['-w', '-r'])
357 cmd.extend(tests)
358 self._subprocess_call(cmd, stdout=None)
359
360
361class BuildOpenSSL(AbstractBuilder):
362 library = "OpenSSL"
363 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
364 src_template = "openssl-{}.tar.gz"
365 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100366 # only install software, skip docs
367 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700368
369
370class BuildLibreSSL(AbstractBuilder):
371 library = "LibreSSL"
372 url_template = (
373 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
374 src_template = "libressl-{}.tar.gz"
375 build_template = "libressl-{}"
376
377
378def configure_make():
379 if not os.path.isfile('Makefile'):
380 log.info('Running ./configure')
381 subprocess.check_call([
382 './configure', '--config-cache', '--quiet',
383 '--with-pydebug'
384 ])
385
386 log.info('Running make')
387 subprocess.check_call(['make', '--quiet'])
388
389
390def main():
391 args = parser.parse_args()
392 if not args.openssl and not args.libressl:
393 args.openssl = list(OPENSSL_RECENT_VERSIONS)
394 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
395 if not args.disable_ancient:
396 args.openssl.extend(OPENSSL_OLD_VERSIONS)
397 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
398
399 logging.basicConfig(
400 level=logging.DEBUG if args.debug else logging.INFO,
401 format="*** %(levelname)s %(message)s"
402 )
403
404 start = datetime.now()
405
Christian Heimesced9cb52018-01-16 21:02:26 +0100406 if args.steps in {'modules', 'tests'}:
407 for name in ['setup.py', 'Modules/_ssl.c']:
408 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
409 parser.error(
410 "Must be executed from CPython build dir"
411 )
412 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700413 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100414 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700415 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100416 # check for configure and run make
417 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700418
419 # download and register builder
420 builds = []
421
422 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100423 build = BuildOpenSSL(
424 version,
425 args
426 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700427 build.install()
428 builds.append(build)
429
430 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100431 build = BuildLibreSSL(
432 version,
433 args
434 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700435 build.install()
436 builds.append(build)
437
Christian Heimesced9cb52018-01-16 21:02:26 +0100438 if args.steps in {'modules', 'tests'}:
439 for build in builds:
440 try:
441 build.recompile_pymods()
442 build.check_pyssl()
443 if args.steps == 'tests':
444 build.run_python_tests(
445 tests=args.tests,
446 network=args.network,
447 )
448 except Exception as e:
449 log.exception("%s failed", build)
450 print("{} failed: {}".format(build, e), file=sys.stderr)
451 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700452
Christian Heimesced9cb52018-01-16 21:02:26 +0100453 log.info("\n{} finished in {}".format(
454 args.steps.capitalize(),
455 datetime.now() - start
456 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700457 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100458 if args.steps == 'tests':
459 if args.tests:
460 print('Executed Tests:', ' '.join(args.tests))
461 else:
462 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700463
464 print('OpenSSL / LibreSSL versions:')
465 for build in builds:
466 print(" * {0.library} {0.version}".format(build))
467
468
469if __name__ == "__main__":
470 main()