blob: 70913c7203b3cb8d904db8aadd6713c21c8cf917 [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 Heimes05d9fe32018-02-27 08:55:39 +010048 "1.0.2n",
49 "1.1.0g",
50 "1.1.1-pre1",
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 Heimes05d9fe32018-02-27 08:55:39 +010057 # "2.6.5",
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',
76 help="Enable debug mode",
77)
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)
128
129
130class AbstractBuilder(object):
131 library = None
132 url_template = None
133 src_template = None
134 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100135 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700136
137 module_files = ("Modules/_ssl.c",
138 "Modules/_hashopenssl.c")
139 module_libs = ("_ssl", "_hashlib")
140
Christian Heimesced9cb52018-01-16 21:02:26 +0100141 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700142 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100143 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700144 # installation directory
145 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100146 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700147 )
148 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100149 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700150 self.src_file = os.path.join(
151 self.src_dir, self.src_template.format(version))
152 # build directory (removed after install)
153 self.build_dir = os.path.join(
154 self.src_dir, self.build_template.format(version))
155
156 def __str__(self):
157 return "<{0.__class__.__name__} for {0.version}>".format(self)
158
159 def __eq__(self, other):
160 if not isinstance(other, AbstractBuilder):
161 return NotImplemented
162 return (
163 self.library == other.library
164 and self.version == other.version
165 )
166
167 def __hash__(self):
168 return hash((self.library, self.version))
169
170 @property
171 def openssl_cli(self):
172 """openssl CLI binary"""
173 return os.path.join(self.install_dir, "bin", "openssl")
174
175 @property
176 def openssl_version(self):
177 """output of 'bin/openssl version'"""
178 cmd = [self.openssl_cli, "version"]
179 return self._subprocess_output(cmd)
180
181 @property
182 def pyssl_version(self):
183 """Value of ssl.OPENSSL_VERSION"""
184 cmd = [
185 sys.executable,
186 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
187 ]
188 return self._subprocess_output(cmd)
189
190 @property
191 def include_dir(self):
192 return os.path.join(self.install_dir, "include")
193
194 @property
195 def lib_dir(self):
196 return os.path.join(self.install_dir, "lib")
197
198 @property
199 def has_openssl(self):
200 return os.path.isfile(self.openssl_cli)
201
202 @property
203 def has_src(self):
204 return os.path.isfile(self.src_file)
205
206 def _subprocess_call(self, cmd, env=None, **kwargs):
207 log.debug("Call '{}'".format(" ".join(cmd)))
208 return subprocess.check_call(cmd, env=env, **kwargs)
209
210 def _subprocess_output(self, cmd, env=None, **kwargs):
211 log.debug("Call '{}'".format(" ".join(cmd)))
212 if env is None:
213 env = os.environ.copy()
214 env["LD_LIBRARY_PATH"] = self.lib_dir
215 out = subprocess.check_output(cmd, env=env, **kwargs)
216 return out.strip().decode("utf-8")
217
218 def _download_src(self):
219 """Download sources"""
220 src_dir = os.path.dirname(self.src_file)
221 if not os.path.isdir(src_dir):
222 os.makedirs(src_dir)
223 url = self.url_template.format(self.version)
224 log.info("Downloading from {}".format(url))
225 req = urlopen(url)
226 # KISS, read all, write all
227 data = req.read()
228 log.info("Storing {}".format(self.src_file))
229 with open(self.src_file, "wb") as f:
230 f.write(data)
231
232 def _unpack_src(self):
233 """Unpack tar.gz bundle"""
234 # cleanup
235 if os.path.isdir(self.build_dir):
236 shutil.rmtree(self.build_dir)
237 os.makedirs(self.build_dir)
238
239 tf = tarfile.open(self.src_file)
240 name = self.build_template.format(self.version)
241 base = name + '/'
242 # force extraction into build dir
243 members = tf.getmembers()
244 for member in list(members):
245 if member.name == name:
246 members.remove(member)
247 elif not member.name.startswith(base):
248 raise ValueError(member.name, base)
249 member.name = member.name[len(base):].lstrip('/')
250 log.info("Unpacking files to {}".format(self.build_dir))
251 tf.extractall(self.build_dir, members)
252
253 def _build_src(self):
254 """Now build openssl"""
255 log.info("Running build in {}".format(self.build_dir))
256 cwd = self.build_dir
257 cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)]
Christian Heimesd3b9f972017-09-06 18:59:22 -0700258 self._subprocess_call(cmd, cwd=cwd)
259 # Old OpenSSL versions do not support parallel builds.
260 self._subprocess_call(["make", "-j1"], cwd=cwd)
261
262 def _make_install(self, remove=True):
Christian Heimesced9cb52018-01-16 21:02:26 +0100263 self._subprocess_call(
264 ["make", "-j1", self.install_target],
265 cwd=self.build_dir
266 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700267 if remove:
268 shutil.rmtree(self.build_dir)
269
270 def install(self):
271 log.info(self.openssl_cli)
272 if not self.has_openssl:
273 if not self.has_src:
274 self._download_src()
275 else:
276 log.debug("Already has src {}".format(self.src_file))
277 self._unpack_src()
278 self._build_src()
279 self._make_install()
280 else:
281 log.info("Already has installation {}".format(self.install_dir))
282 # validate installation
283 version = self.openssl_version
284 if self.version not in version:
285 raise ValueError(version)
286
287 def recompile_pymods(self):
288 log.warning("Using build from {}".format(self.build_dir))
289 # force a rebuild of all modules that use OpenSSL APIs
290 for fname in self.module_files:
291 os.utime(fname, None)
292 # remove all build artefacts
293 for root, dirs, files in os.walk('build'):
294 for filename in files:
295 if filename.startswith(self.module_libs):
296 os.unlink(os.path.join(root, filename))
297
298 # overwrite header and library search paths
299 env = os.environ.copy()
300 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
301 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
302 # set rpath
303 env["LD_RUN_PATH"] = self.lib_dir
304
305 log.info("Rebuilding Python modules")
306 cmd = [sys.executable, "setup.py", "build"]
307 self._subprocess_call(cmd, env=env)
308 self.check_imports()
309
310 def check_imports(self):
311 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
312 self._subprocess_call(cmd)
313
314 def check_pyssl(self):
315 version = self.pyssl_version
316 if self.version not in version:
317 raise ValueError(version)
318
319 def run_python_tests(self, tests, network=True):
320 if not tests:
321 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
322 elif sys.version_info < (3, 3):
323 cmd = [sys.executable, '-m', 'test.regrtest']
324 else:
325 cmd = [sys.executable, '-m', 'test', '-j0']
326 if network:
327 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
328 cmd.extend(['-w', '-r'])
329 cmd.extend(tests)
330 self._subprocess_call(cmd, stdout=None)
331
332
333class BuildOpenSSL(AbstractBuilder):
334 library = "OpenSSL"
335 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
336 src_template = "openssl-{}.tar.gz"
337 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100338 # only install software, skip docs
339 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700340
341
342class BuildLibreSSL(AbstractBuilder):
343 library = "LibreSSL"
344 url_template = (
345 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
346 src_template = "libressl-{}.tar.gz"
347 build_template = "libressl-{}"
348
349
350def configure_make():
351 if not os.path.isfile('Makefile'):
352 log.info('Running ./configure')
353 subprocess.check_call([
354 './configure', '--config-cache', '--quiet',
355 '--with-pydebug'
356 ])
357
358 log.info('Running make')
359 subprocess.check_call(['make', '--quiet'])
360
361
362def main():
363 args = parser.parse_args()
364 if not args.openssl and not args.libressl:
365 args.openssl = list(OPENSSL_RECENT_VERSIONS)
366 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
367 if not args.disable_ancient:
368 args.openssl.extend(OPENSSL_OLD_VERSIONS)
369 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
370
371 logging.basicConfig(
372 level=logging.DEBUG if args.debug else logging.INFO,
373 format="*** %(levelname)s %(message)s"
374 )
375
376 start = datetime.now()
377
Christian Heimesced9cb52018-01-16 21:02:26 +0100378 if args.steps in {'modules', 'tests'}:
379 for name in ['setup.py', 'Modules/_ssl.c']:
380 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
381 parser.error(
382 "Must be executed from CPython build dir"
383 )
384 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700385 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100386 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700387 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100388 # check for configure and run make
389 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700390
391 # download and register builder
392 builds = []
393
394 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100395 build = BuildOpenSSL(
396 version,
397 args
398 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700399 build.install()
400 builds.append(build)
401
402 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100403 build = BuildLibreSSL(
404 version,
405 args
406 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700407 build.install()
408 builds.append(build)
409
Christian Heimesced9cb52018-01-16 21:02:26 +0100410 if args.steps in {'modules', 'tests'}:
411 for build in builds:
412 try:
413 build.recompile_pymods()
414 build.check_pyssl()
415 if args.steps == 'tests':
416 build.run_python_tests(
417 tests=args.tests,
418 network=args.network,
419 )
420 except Exception as e:
421 log.exception("%s failed", build)
422 print("{} failed: {}".format(build, e), file=sys.stderr)
423 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700424
Christian Heimesced9cb52018-01-16 21:02:26 +0100425 log.info("\n{} finished in {}".format(
426 args.steps.capitalize(),
427 datetime.now() - start
428 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700429 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100430 if args.steps == 'tests':
431 if args.tests:
432 print('Executed Tests:', ' '.join(args.tests))
433 else:
434 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700435
436 print('OpenSSL / LibreSSL versions:')
437 for build in builds:
438 print(" * {0.library} {0.version}".format(build))
439
440
441if __name__ == "__main__":
442 main()