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