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