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