blob: bbc5c66520989c6bee222306f8828eb62acc3f80 [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",
50 "1.1.1-pre6",
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',
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)
Steve Dowere5f41d22018-05-16 17:50:29 -0400128parser.add_argument(
129 '--system',
130 default='',
131 help="Override the automatic system type detection."
132)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700133
134
135class AbstractBuilder(object):
136 library = None
137 url_template = None
138 src_template = None
139 build_template = None
Christian Heimesced9cb52018-01-16 21:02:26 +0100140 install_target = 'install'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700141
142 module_files = ("Modules/_ssl.c",
143 "Modules/_hashopenssl.c")
144 module_libs = ("_ssl", "_hashlib")
145
Christian Heimesced9cb52018-01-16 21:02:26 +0100146 def __init__(self, version, args):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700147 self.version = version
Christian Heimesced9cb52018-01-16 21:02:26 +0100148 self.args = args
Christian Heimesd3b9f972017-09-06 18:59:22 -0700149 # installation directory
150 self.install_dir = os.path.join(
Christian Heimesced9cb52018-01-16 21:02:26 +0100151 os.path.join(args.base_directory, self.library.lower()), version
Christian Heimesd3b9f972017-09-06 18:59:22 -0700152 )
153 # source file
Christian Heimesced9cb52018-01-16 21:02:26 +0100154 self.src_dir = os.path.join(args.base_directory, 'src')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700155 self.src_file = os.path.join(
156 self.src_dir, self.src_template.format(version))
157 # build directory (removed after install)
158 self.build_dir = os.path.join(
159 self.src_dir, self.build_template.format(version))
Steve Dowere5f41d22018-05-16 17:50:29 -0400160 self.system = args.system
Christian Heimesd3b9f972017-09-06 18:59:22 -0700161
162 def __str__(self):
163 return "<{0.__class__.__name__} for {0.version}>".format(self)
164
165 def __eq__(self, other):
166 if not isinstance(other, AbstractBuilder):
167 return NotImplemented
168 return (
169 self.library == other.library
170 and self.version == other.version
171 )
172
173 def __hash__(self):
174 return hash((self.library, self.version))
175
176 @property
177 def openssl_cli(self):
178 """openssl CLI binary"""
179 return os.path.join(self.install_dir, "bin", "openssl")
180
181 @property
182 def openssl_version(self):
183 """output of 'bin/openssl version'"""
184 cmd = [self.openssl_cli, "version"]
185 return self._subprocess_output(cmd)
186
187 @property
188 def pyssl_version(self):
189 """Value of ssl.OPENSSL_VERSION"""
190 cmd = [
191 sys.executable,
192 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
193 ]
194 return self._subprocess_output(cmd)
195
196 @property
197 def include_dir(self):
198 return os.path.join(self.install_dir, "include")
199
200 @property
201 def lib_dir(self):
202 return os.path.join(self.install_dir, "lib")
203
204 @property
205 def has_openssl(self):
206 return os.path.isfile(self.openssl_cli)
207
208 @property
209 def has_src(self):
210 return os.path.isfile(self.src_file)
211
212 def _subprocess_call(self, cmd, env=None, **kwargs):
213 log.debug("Call '{}'".format(" ".join(cmd)))
214 return subprocess.check_call(cmd, env=env, **kwargs)
215
216 def _subprocess_output(self, cmd, env=None, **kwargs):
217 log.debug("Call '{}'".format(" ".join(cmd)))
218 if env is None:
219 env = os.environ.copy()
220 env["LD_LIBRARY_PATH"] = self.lib_dir
221 out = subprocess.check_output(cmd, env=env, **kwargs)
222 return out.strip().decode("utf-8")
223
224 def _download_src(self):
225 """Download sources"""
226 src_dir = os.path.dirname(self.src_file)
227 if not os.path.isdir(src_dir):
228 os.makedirs(src_dir)
229 url = self.url_template.format(self.version)
230 log.info("Downloading from {}".format(url))
231 req = urlopen(url)
232 # KISS, read all, write all
233 data = req.read()
234 log.info("Storing {}".format(self.src_file))
235 with open(self.src_file, "wb") as f:
236 f.write(data)
237
238 def _unpack_src(self):
239 """Unpack tar.gz bundle"""
240 # cleanup
241 if os.path.isdir(self.build_dir):
242 shutil.rmtree(self.build_dir)
243 os.makedirs(self.build_dir)
244
245 tf = tarfile.open(self.src_file)
246 name = self.build_template.format(self.version)
247 base = name + '/'
248 # force extraction into build dir
249 members = tf.getmembers()
250 for member in list(members):
251 if member.name == name:
252 members.remove(member)
253 elif not member.name.startswith(base):
254 raise ValueError(member.name, base)
255 member.name = member.name[len(base):].lstrip('/')
256 log.info("Unpacking files to {}".format(self.build_dir))
257 tf.extractall(self.build_dir, members)
258
259 def _build_src(self):
260 """Now build openssl"""
261 log.info("Running build in {}".format(self.build_dir))
262 cwd = self.build_dir
263 cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)]
Steve Dowere5f41d22018-05-16 17:50:29 -0400264 env = None
265 if self.system:
266 env = os.environ.copy()
267 env['SYSTEM'] = self.system
268 self._subprocess_call(cmd, cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700269 # Old OpenSSL versions do not support parallel builds.
Steve Dowere5f41d22018-05-16 17:50:29 -0400270 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700271
272 def _make_install(self, remove=True):
Christian Heimesced9cb52018-01-16 21:02:26 +0100273 self._subprocess_call(
274 ["make", "-j1", self.install_target],
275 cwd=self.build_dir
276 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700277 if remove:
278 shutil.rmtree(self.build_dir)
279
280 def install(self):
281 log.info(self.openssl_cli)
282 if not self.has_openssl:
283 if not self.has_src:
284 self._download_src()
285 else:
286 log.debug("Already has src {}".format(self.src_file))
287 self._unpack_src()
288 self._build_src()
289 self._make_install()
290 else:
291 log.info("Already has installation {}".format(self.install_dir))
292 # validate installation
293 version = self.openssl_version
294 if self.version not in version:
295 raise ValueError(version)
296
297 def recompile_pymods(self):
298 log.warning("Using build from {}".format(self.build_dir))
299 # force a rebuild of all modules that use OpenSSL APIs
300 for fname in self.module_files:
301 os.utime(fname, None)
302 # remove all build artefacts
303 for root, dirs, files in os.walk('build'):
304 for filename in files:
305 if filename.startswith(self.module_libs):
306 os.unlink(os.path.join(root, filename))
307
308 # overwrite header and library search paths
309 env = os.environ.copy()
310 env["CPPFLAGS"] = "-I{}".format(self.include_dir)
311 env["LDFLAGS"] = "-L{}".format(self.lib_dir)
312 # set rpath
313 env["LD_RUN_PATH"] = self.lib_dir
314
315 log.info("Rebuilding Python modules")
316 cmd = [sys.executable, "setup.py", "build"]
317 self._subprocess_call(cmd, env=env)
318 self.check_imports()
319
320 def check_imports(self):
321 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
322 self._subprocess_call(cmd)
323
324 def check_pyssl(self):
325 version = self.pyssl_version
326 if self.version not in version:
327 raise ValueError(version)
328
329 def run_python_tests(self, tests, network=True):
330 if not tests:
331 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0']
332 elif sys.version_info < (3, 3):
333 cmd = [sys.executable, '-m', 'test.regrtest']
334 else:
335 cmd = [sys.executable, '-m', 'test', '-j0']
336 if network:
337 cmd.extend(['-u', 'network', '-u', 'urlfetch'])
338 cmd.extend(['-w', '-r'])
339 cmd.extend(tests)
340 self._subprocess_call(cmd, stdout=None)
341
342
343class BuildOpenSSL(AbstractBuilder):
344 library = "OpenSSL"
345 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz"
346 src_template = "openssl-{}.tar.gz"
347 build_template = "openssl-{}"
Christian Heimesced9cb52018-01-16 21:02:26 +0100348 # only install software, skip docs
349 install_target = 'install_sw'
Christian Heimesd3b9f972017-09-06 18:59:22 -0700350
351
352class BuildLibreSSL(AbstractBuilder):
353 library = "LibreSSL"
354 url_template = (
355 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz")
356 src_template = "libressl-{}.tar.gz"
357 build_template = "libressl-{}"
358
359
360def configure_make():
361 if not os.path.isfile('Makefile'):
362 log.info('Running ./configure')
363 subprocess.check_call([
364 './configure', '--config-cache', '--quiet',
365 '--with-pydebug'
366 ])
367
368 log.info('Running make')
369 subprocess.check_call(['make', '--quiet'])
370
371
372def main():
373 args = parser.parse_args()
374 if not args.openssl and not args.libressl:
375 args.openssl = list(OPENSSL_RECENT_VERSIONS)
376 args.libressl = list(LIBRESSL_RECENT_VERSIONS)
377 if not args.disable_ancient:
378 args.openssl.extend(OPENSSL_OLD_VERSIONS)
379 args.libressl.extend(LIBRESSL_OLD_VERSIONS)
380
381 logging.basicConfig(
382 level=logging.DEBUG if args.debug else logging.INFO,
383 format="*** %(levelname)s %(message)s"
384 )
385
386 start = datetime.now()
387
Christian Heimesced9cb52018-01-16 21:02:26 +0100388 if args.steps in {'modules', 'tests'}:
389 for name in ['setup.py', 'Modules/_ssl.c']:
390 if not os.path.isfile(os.path.join(PYTHONROOT, name)):
391 parser.error(
392 "Must be executed from CPython build dir"
393 )
394 if not os.path.samefile('python', sys.executable):
Christian Heimesd3b9f972017-09-06 18:59:22 -0700395 parser.error(
Christian Heimesced9cb52018-01-16 21:02:26 +0100396 "Must be executed with ./python from CPython build dir"
Christian Heimesd3b9f972017-09-06 18:59:22 -0700397 )
Christian Heimesced9cb52018-01-16 21:02:26 +0100398 # check for configure and run make
399 configure_make()
Christian Heimesd3b9f972017-09-06 18:59:22 -0700400
401 # download and register builder
402 builds = []
403
404 for version in args.openssl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100405 build = BuildOpenSSL(
406 version,
407 args
408 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700409 build.install()
410 builds.append(build)
411
412 for version in args.libressl:
Christian Heimesced9cb52018-01-16 21:02:26 +0100413 build = BuildLibreSSL(
414 version,
415 args
416 )
Christian Heimesd3b9f972017-09-06 18:59:22 -0700417 build.install()
418 builds.append(build)
419
Christian Heimesced9cb52018-01-16 21:02:26 +0100420 if args.steps in {'modules', 'tests'}:
421 for build in builds:
422 try:
423 build.recompile_pymods()
424 build.check_pyssl()
425 if args.steps == 'tests':
426 build.run_python_tests(
427 tests=args.tests,
428 network=args.network,
429 )
430 except Exception as e:
431 log.exception("%s failed", build)
432 print("{} failed: {}".format(build, e), file=sys.stderr)
433 sys.exit(2)
Christian Heimesd3b9f972017-09-06 18:59:22 -0700434
Christian Heimesced9cb52018-01-16 21:02:26 +0100435 log.info("\n{} finished in {}".format(
436 args.steps.capitalize(),
437 datetime.now() - start
438 ))
Christian Heimesd3b9f972017-09-06 18:59:22 -0700439 print('Python: ', sys.version)
Christian Heimesced9cb52018-01-16 21:02:26 +0100440 if args.steps == 'tests':
441 if args.tests:
442 print('Executed Tests:', ' '.join(args.tests))
443 else:
444 print('Executed all SSL tests.')
Christian Heimesd3b9f972017-09-06 18:59:22 -0700445
446 print('OpenSSL / LibreSSL versions:')
447 for build in builds:
448 print(" * {0.library} {0.version}".format(build))
449
450
451if __name__ == "__main__":
452 main()