blob: 75874cfca4d085965f00b469a94d8860392b0ca1 [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 Heimesced9cb52018-01-16 21:02:26 +010044 # "0.9.8zh",
45 # "1.0.1u",
Christian Heimesd3b9f972017-09-06 18:59:22 -070046]
47
48OPENSSL_RECENT_VERSIONS = [
49 "1.0.2",
Christian Heimes0d2c6452017-11-02 17:38:11 +010050 "1.0.2m",
51 "1.1.0g",
Christian Heimesd3b9f972017-09-06 18:59:22 -070052]
53
54LIBRESSL_OLD_VERSIONS = [
Christian Heimesced9cb52018-01-16 21:02:26 +010055 # "2.3.10",
56 # "2.4.5",
Christian Heimesd3b9f972017-09-06 18:59:22 -070057]
58
59LIBRESSL_RECENT_VERSIONS = [
60 "2.5.3",
61 "2.5.5",
62]
63
64# store files in ../multissl
Christian Heimesced9cb52018-01-16 21:02:26 +010065HERE = os.path.dirname(os.path.abspath(__file__))
66PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
67MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
68
Christian Heimesd3b9f972017-09-06 18:59:22 -070069
70parser = argparse.ArgumentParser(
71 prog='multissl',
72 description=(
73 "Run CPython tests with multiple OpenSSL and LibreSSL "
74 "versions."
75 )
76)
77parser.add_argument(
78 '--debug',
79 action='store_true',
80 help="Enable debug mode",
81)
82parser.add_argument(
83 '--disable-ancient',
84 action='store_true',
85 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.",
86)
87parser.add_argument(
88 '--openssl',
89 nargs='+',
90 default=(),
91 help=(
92 "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
93 "OpenSSL and LibreSSL versions are given."
94 ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
95)
96parser.add_argument(
97 '--libressl',
98 nargs='+',
99 default=(),
100 help=(
101 "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
102 "OpenSSL and LibreSSL versions are given."
103 ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
104)
105parser.add_argument(
106 '--tests',
107 nargs='*',
108 default=(),
109 help="Python tests to run, defaults to all SSL related tests.",
110)
111parser.add_argument(
112 '--base-directory',
113 default=MULTISSL_DIR,
114 help="Base directory for OpenSSL / LibreSSL sources and builds."
115)
116parser.add_argument(
117 '--no-network',
118 action='store_false',
119 dest='network',
120 help="Disable network tests."
121)
122parser.add_argument(
Christian Heimesced9cb52018-01-16 21:02:26 +0100123 '--steps',
124 choices=['library', 'modules', 'tests'],
125 default='tests',
126 help=(
127 "Which steps to perform. 'library' downloads and compiles OpenSSL "
128 "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
129 "all and runs the test suite."
130 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700131)
132
133
134class AbstractBuilder(object):
135 library = None
136 url_template = None
137 src_template = None
138 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100139 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700140
141 module_files = ("Modules/_ssl.c",
142 "Modules/_hashopenssl.c")
143 module_libs = ("_ssl", "_hashlib")
144
Christian Heimesced9cb52018-01-16 21:02:26 +0100145 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700146 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100147 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700148 # installation directory
149 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100150 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700151 )
152 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100153 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700154 self.src_file = os.path.join(
155 self.src_dir, self.src_template.format(version))
156 # build directory (removed after install)
157 self.build_dir = os.path.join(
158 self.src_dir, self.build_template.format(version))
159
160 def __str__(self):
161 return "<{0.__class__.__name__} for {0.version}>".format(self)
162
163 def __eq__(self, other):
164 if not isinstance(other, AbstractBuilder):
165 return NotImplemented
166 return (
167 self.library == other.library
168 and self.version == other.version
169 )
170
171 def __hash__(self):
172 return hash((self.library, self.version))
173
174 @property
175 def openssl_cli(self):
176 """openssl CLI binary"""
177 return os.path.join(self.install_dir, "bin", "openssl")
178
179 @property
180 def openssl_version(self):
181 """output of 'bin/openssl version'"""
182 cmd = [self.openssl_cli, "version"]
183 return self._subprocess_output(cmd)
184
185 @property
186 def pyssl_version(self):
187 """Value of ssl.OPENSSL_VERSION"""
188 cmd = [
189 sys.executable,
190 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
191 ]
192 return self._subprocess_output(cmd)
193
194 @property
195 def include_dir(self):
196 return os.path.join(self.install_dir, "include")
197
198 @property
199 def lib_dir(self):
200 return os.path.join(self.install_dir, "lib")
201
202 @property
203 def has_openssl(self):
204 return os.path.isfile(self.openssl_cli)
205
206 @property
207 def has_src(self):
208 return os.path.isfile(self.src_file)
209
210 def _subprocess_call(self, cmd, env=None, **kwargs):
211 log.debug("Call '{}'".format(" ".join(cmd)))
212 return subprocess.check_call(cmd, env=env, **kwargs)
213
214 def _subprocess_output(self, cmd, env=None, **kwargs):
215 log.debug("Call '{}'".format(" ".join(cmd)))
216 if env is None:
217 env = os.environ.copy()
218 env["LD_LIBRARY_PATH"] = self.lib_dir
219 out = subprocess.check_output(cmd, env=env, **kwargs)
220 return out.strip().decode("utf-8")
221
222 def _download_src(self):
223 """Download sources"""
224 src_dir = os.path.dirname(self.src_file)
225 if not os.path.isdir(src_dir):
226 os.makedirs(src_dir)
227 url = self.url_template.format(self.version)
228 log.info("Downloading from {}".format(url))
229 req = urlopen(url)
230 # KISS, read all, write all
231 data = req.read()
232 log.info("Storing {}".format(self.src_file))
233 with open(self.src_file, "wb") as f:
234 f.write(data)
235
236 def _unpack_src(self):
237 """Unpack tar.gz bundle"""
238 # cleanup
239 if os.path.isdir(self.build_dir):
240 shutil.rmtree(self.build_dir)
241 os.makedirs(self.build_dir)
242
243 tf = tarfile.open(self.src_file)
244 name = self.build_template.format(self.version)
245 base = name + '/'
246 # force extraction into build dir
247 members = tf.getmembers()
248 for member in list(members):
249 if member.name == name:
250 members.remove(member)
251 elif not member.name.startswith(base):
252 raise ValueError(member.name, base)
253 member.name = member.name[len(base):].lstrip('/')
254 log.info("Unpacking files to {}".format(self.build_dir))
255 tf.extractall(self.build_dir, members)
256
257 def _build_src(self):
258 """Now build openssl"""
259 log.info("Running build in {}".format(self.build_dir))
260 cwd = self.build_dir
261 cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)]
Christian Heimesd3b9f972017-09-06 18:59:22 -0700262 self._subprocess_call(cmd, cwd=cwd)
263 # Old OpenSSL versions do not support parallel builds.
264 self._subprocess_call(["make", "-j1"], cwd=cwd)
265
266 def _make_install(self, remove=True):
Christian Heimesced9cb52018-01-16 21:02:26 +0100267 self._subprocess_call(
268 ["make", "-j1", self.install_target],
269 cwd=self.build_dir
270 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700271 if remove:
272 shutil.rmtree(self.build_dir)
273
274 def install(self):
275 log.info(self.openssl_cli)
276 if not self.has_openssl:
277 if not self.has_src:
278 self._download_src()
279 else:
280 log.debug("Already has src {}".format(self.src_file))
281 self._unpack_src()
282 self._build_src()
283 self._make_install()
284 else:
285 log.info("Already has installation {}".format(self.install_dir))
286 # validate installation
287 version = self.openssl_version
288 if self.version not in version:
289 raise ValueError(version)
290
291 def recompile_pymods(self):
292 log.warning("Using build from {}".format(self.build_dir))
293 # force a rebuild of all modules that use OpenSSL APIs
294 for fname in self.module_files:
295 os.utime(fname, None)
296 # remove all build artefacts
297 for root, dirs, files in os.walk('build'):
298 for filename in files:
299 if filename.startswith(self.module_libs):
300 os.unlink(os.path.join(root, filename))
301
302 # overwrite header and library search paths
303 env = os.environ.copy()
304 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
305 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
306 # set rpath
307 env["LD_RUN_PATH"] = self.lib_dir
308
309 log.info("Rebuilding Python modules")
310 cmd = [sys.executable, "setup.py", "build"]
311 self._subprocess_call(cmd, env=env)
312 self.check_imports()
313
314 def check_imports(self):
315 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
316 self._subprocess_call(cmd)
317
318 def check_pyssl(self):
319 version = self.pyssl_version
320 if self.version not in version:
321 raise ValueError(version)
322
323 def run_python_tests(self, tests, network=True):
324 if not tests:
325 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
326 elif sys.version_info < (3, 3):
327 cmd = [sys.executable, '-m', 'test.regrtest']
328 else:
329 cmd = [sys.executable, '-m', 'test', '-j0']
330 if network:
331 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
332 cmd.extend(['-w', '-r'])
333 cmd.extend(tests)
334 self._subprocess_call(cmd, stdout=None)
335
336
337class BuildOpenSSL(AbstractBuilder):
338 library = "OpenSSL"
339 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
340 src_template = "openssl-{}.tar.gz"
341 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100342 # only install software, skip docs
343 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700344
345
346class BuildLibreSSL(AbstractBuilder):
347 library = "LibreSSL"
348 url_template = (
349 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
350 src_template = "libressl-{}.tar.gz"
351 build_template = "libressl-{}"
352
353
354def configure_make():
355 if not os.path.isfile('Makefile'):
356 log.info('Running ./configure')
357 subprocess.check_call([
358 './configure', '--config-cache', '--quiet',
359 '--with-pydebug'
360 ])
361
362 log.info('Running make')
363 subprocess.check_call(['make', '--quiet'])
364
365
366def main():
367 args = parser.parse_args()
368 if not args.openssl and not args.libressl:
369 args.openssl = list(OPENSSL_RECENT_VERSIONS)
370 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
371 if not args.disable_ancient:
372 args.openssl.extend(OPENSSL_OLD_VERSIONS)
373 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
374
375 logging.basicConfig(
376 level=logging.DEBUG if args.debug else logging.INFO,
377 format="*** %(levelname)s %(message)s"
378 )
379
380 start = datetime.now()
381
Christian Heimesced9cb52018-01-16 21:02:26 +0100382 if args.steps in {'modules', 'tests'}:
383 for name in ['setup.py', 'Modules/_ssl.c']:
384 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
385 parser.error(
386 "Must be executed from CPython build dir"
387 )
388 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700389 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100390 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700391 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100392 # check for configure and run make
393 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700394
395 # download and register builder
396 builds = []
397
398 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100399 build = BuildOpenSSL(
400 version,
401 args
402 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700403 build.install()
404 builds.append(build)
405
406 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100407 build = BuildLibreSSL(
408 version,
409 args
410 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700411 build.install()
412 builds.append(build)
413
Christian Heimesced9cb52018-01-16 21:02:26 +0100414 if args.steps in {'modules', 'tests'}:
415 for build in builds:
416 try:
417 build.recompile_pymods()
418 build.check_pyssl()
419 if args.steps == 'tests':
420 build.run_python_tests(
421 tests=args.tests,
422 network=args.network,
423 )
424 except Exception as e:
425 log.exception("%s failed", build)
426 print("{} failed: {}".format(build, e), file=sys.stderr)
427 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700428
Christian Heimesced9cb52018-01-16 21:02:26 +0100429 log.info("\n{} finished in {}".format(
430 args.steps.capitalize(),
431 datetime.now() - start
432 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700433 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100434 if args.steps == 'tests':
435 if args.tests:
436 print('Executed Tests:', ' '.join(args.tests))
437 else:
438 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700439
440 print('OpenSSL / LibreSSL versions:')
441 for build in builds:
442 print(" * {0.library} {0.version}".format(build))
443
444
445if __name__ == "__main__":
446 main()