blob: c4ebe317797d2b4d393d366326d4ef6f422a6b2d [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 Heimese8eb6cb2018-05-22 22:50:12 +020048 "1.0.2o",
49 "1.1.0h",
Christian Heimes529525f2018-05-23 22:24:45 +020050 # "1.1.1-pre7",
Christian Heimesd3b9f972017-09-06 18:59:22 -070051]
52
53LIBRESSL_OLD_VERSIONS = [
Christian Heimesd3b9f972017-09-06 18:59:22 -070054]
55
56LIBRESSL_RECENT_VERSIONS = [
Christian Heimese8eb6cb2018-05-22 22:50:12 +020057 "2.7.3",
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',
81 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.",
82)
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
149 url_template = None
150 src_template = None
151 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100152 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700153
154 module_files = ("Modules/_ssl.c",
155 "Modules/_hashopenssl.c")
156 module_libs = ("_ssl", "_hashlib")
157
Christian Heimesced9cb52018-01-16 21:02:26 +0100158 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700159 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100160 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700161 # installation directory
162 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100163 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700164 )
165 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100166 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700167 self.src_file = os.path.join(
168 self.src_dir, self.src_template.format(version))
169 # build directory (removed after install)
170 self.build_dir = os.path.join(
171 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400172 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700173
174 def __str__(self):
175 return "<{0.__class__.__name__} for {0.version}>".format(self)
176
177 def __eq__(self, other):
178 if not isinstance(other, AbstractBuilder):
179 return NotImplemented
180 return (
181 self.library == other.library
182 and self.version == other.version
183 )
184
185 def __hash__(self):
186 return hash((self.library, self.version))
187
188 @property
189 def openssl_cli(self):
190 """openssl CLI binary"""
191 return os.path.join(self.install_dir, "bin", "openssl")
192
193 @property
194 def openssl_version(self):
195 """output of 'bin/openssl version'"""
196 cmd = [self.openssl_cli, "version"]
197 return self._subprocess_output(cmd)
198
199 @property
200 def pyssl_version(self):
201 """Value of ssl.OPENSSL_VERSION"""
202 cmd = [
203 sys.executable,
204 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
205 ]
206 return self._subprocess_output(cmd)
207
208 @property
209 def include_dir(self):
210 return os.path.join(self.install_dir, "include")
211
212 @property
213 def lib_dir(self):
214 return os.path.join(self.install_dir, "lib")
215
216 @property
217 def has_openssl(self):
218 return os.path.isfile(self.openssl_cli)
219
220 @property
221 def has_src(self):
222 return os.path.isfile(self.src_file)
223
224 def _subprocess_call(self, cmd, env=None, **kwargs):
225 log.debug("Call '{}'".format(" ".join(cmd)))
226 return subprocess.check_call(cmd, env=env, **kwargs)
227
228 def _subprocess_output(self, cmd, env=None, **kwargs):
229 log.debug("Call '{}'".format(" ".join(cmd)))
230 if env is None:
231 env = os.environ.copy()
232 env["LD_LIBRARY_PATH"] = self.lib_dir
233 out = subprocess.check_output(cmd, env=env, **kwargs)
234 return out.strip().decode("utf-8")
235
236 def _download_src(self):
237 """Download sources"""
238 src_dir = os.path.dirname(self.src_file)
239 if not os.path.isdir(src_dir):
240 os.makedirs(src_dir)
241 url = self.url_template.format(self.version)
242 log.info("Downloading from {}".format(url))
243 req = urlopen(url)
244 # KISS, read all, write all
245 data = req.read()
246 log.info("Storing {}".format(self.src_file))
247 with open(self.src_file, "wb") as f:
248 f.write(data)
249
250 def _unpack_src(self):
251 """Unpack tar.gz bundle"""
252 # cleanup
253 if os.path.isdir(self.build_dir):
254 shutil.rmtree(self.build_dir)
255 os.makedirs(self.build_dir)
256
257 tf = tarfile.open(self.src_file)
258 name = self.build_template.format(self.version)
259 base = name + '/'
260 # force extraction into build dir
261 members = tf.getmembers()
262 for member in list(members):
263 if member.name == name:
264 members.remove(member)
265 elif not member.name.startswith(base):
266 raise ValueError(member.name, base)
267 member.name = member.name[len(base):].lstrip('/')
268 log.info("Unpacking files to {}".format(self.build_dir))
269 tf.extractall(self.build_dir, members)
270
271 def _build_src(self):
272 """Now build openssl"""
273 log.info("Running build in {}".format(self.build_dir))
274 cwd = self.build_dir
Christian Heimes529525f2018-05-23 22:24:45 +0200275 cmd = [
276 "./config",
277 "shared", "--debug",
278 "--prefix={}".format(self.install_dir)
279 ]
280 env = os.environ.copy()
281 # set rpath
282 env["LD_RUN_PATH"] = self.lib_dir
Steve Dowere5f41d22018-05-16 17:50:29 -0400283 if self.system:
Steve Dowere5f41d22018-05-16 17:50:29 -0400284 env['SYSTEM'] = self.system
285 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700286 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400287 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700288
Christian Heimes529525f2018-05-23 22:24:45 +0200289 def _make_install(self):
Christian Heimesced9cb52018-01-16 21:02:26 +0100290 self._subprocess_call(
291 ["make", "-j1", self.install_target],
292 cwd=self.build_dir
293 )
Christian Heimes529525f2018-05-23 22:24:45 +0200294 if not self.args.keep_sources:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700295 shutil.rmtree(self.build_dir)
296
297 def install(self):
298 log.info(self.openssl_cli)
Christian Heimes529525f2018-05-23 22:24:45 +0200299 if not self.has_openssl or self.args.force:
Christian Heimesd3b9f972017-09-06 18:59:22 -0700300 if not self.has_src:
301 self._download_src()
302 else:
303 log.debug("Already has src {}".format(self.src_file))
304 self._unpack_src()
305 self._build_src()
306 self._make_install()
307 else:
308 log.info("Already has installation {}".format(self.install_dir))
309 # validate installation
310 version = self.openssl_version
311 if self.version not in version:
312 raise ValueError(version)
313
314 def recompile_pymods(self):
315 log.warning("Using build from {}".format(self.build_dir))
316 # force a rebuild of all modules that use OpenSSL APIs
317 for fname in self.module_files:
318 os.utime(fname, None)
319 # remove all build artefacts
320 for root, dirs, files in os.walk('build'):
321 for filename in files:
322 if filename.startswith(self.module_libs):
323 os.unlink(os.path.join(root, filename))
324
325 # overwrite header and library search paths
326 env = os.environ.copy()
327 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
328 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
329 # set rpath
330 env["LD_RUN_PATH"] = self.lib_dir
331
332 log.info("Rebuilding Python modules")
333 cmd = [sys.executable, "setup.py", "build"]
334 self._subprocess_call(cmd, env=env)
335 self.check_imports()
336
337 def check_imports(self):
338 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
339 self._subprocess_call(cmd)
340
341 def check_pyssl(self):
342 version = self.pyssl_version
343 if self.version not in version:
344 raise ValueError(version)
345
346 def run_python_tests(self, tests, network=True):
347 if not tests:
348 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
349 elif sys.version_info < (3, 3):
350 cmd = [sys.executable, '-m', 'test.regrtest']
351 else:
352 cmd = [sys.executable, '-m', 'test', '-j0']
353 if network:
354 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
355 cmd.extend(['-w', '-r'])
356 cmd.extend(tests)
357 self._subprocess_call(cmd, stdout=None)
358
359
360class BuildOpenSSL(AbstractBuilder):
361 library = "OpenSSL"
362 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
363 src_template = "openssl-{}.tar.gz"
364 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100365 # only install software, skip docs
366 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700367
368
369class BuildLibreSSL(AbstractBuilder):
370 library = "LibreSSL"
371 url_template = (
372 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
373 src_template = "libressl-{}.tar.gz"
374 build_template = "libressl-{}"
375
376
377def configure_make():
378 if not os.path.isfile('Makefile'):
379 log.info('Running ./configure')
380 subprocess.check_call([
381 './configure', '--config-cache', '--quiet',
382 '--with-pydebug'
383 ])
384
385 log.info('Running make')
386 subprocess.check_call(['make', '--quiet'])
387
388
389def main():
390 args = parser.parse_args()
391 if not args.openssl and not args.libressl:
392 args.openssl = list(OPENSSL_RECENT_VERSIONS)
393 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
394 if not args.disable_ancient:
395 args.openssl.extend(OPENSSL_OLD_VERSIONS)
396 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
397
398 logging.basicConfig(
399 level=logging.DEBUG if args.debug else logging.INFO,
400 format="*** %(levelname)s %(message)s"
401 )
402
403 start = datetime.now()
404
Christian Heimesced9cb52018-01-16 21:02:26 +0100405 if args.steps in {'modules', 'tests'}:
406 for name in ['setup.py', 'Modules/_ssl.c']:
407 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
408 parser.error(
409 "Must be executed from CPython build dir"
410 )
411 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700412 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100413 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700414 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100415 # check for configure and run make
416 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700417
418 # download and register builder
419 builds = []
420
421 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100422 build = BuildOpenSSL(
423 version,
424 args
425 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700426 build.install()
427 builds.append(build)
428
429 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100430 build = BuildLibreSSL(
431 version,
432 args
433 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700434 build.install()
435 builds.append(build)
436
Christian Heimesced9cb52018-01-16 21:02:26 +0100437 if args.steps in {'modules', 'tests'}:
438 for build in builds:
439 try:
440 build.recompile_pymods()
441 build.check_pyssl()
442 if args.steps == 'tests':
443 build.run_python_tests(
444 tests=args.tests,
445 network=args.network,
446 )
447 except Exception as e:
448 log.exception("%s failed", build)
449 print("{} failed: {}".format(build, e), file=sys.stderr)
450 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700451
Christian Heimesced9cb52018-01-16 21:02:26 +0100452 log.info("\n{} finished in {}".format(
453 args.steps.capitalize(),
454 datetime.now() - start
455 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700456 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100457 if args.steps == 'tests':
458 if args.tests:
459 print('Executed Tests:', ' '.join(args.tests))
460 else:
461 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700462
463 print('OpenSSL / LibreSSL versions:')
464 for build in builds:
465 print(" * {0.library} {0.version}".format(build))
466
467
468if __name__ == "__main__":
469 main()