diff --git a/Makefile b/Makefile
index 92a75922e0307b3067dc2f55ead7e474d3c34f6e..60b68907e60725246cecafda343f6ad431526af7 100644
--- a/Makefile
+++ b/Makefile
@@ -10,46 +10,10 @@ _relocatabledir =
 endif
 
 install:
-	rm -fr relocatable-install
-	$(MAKE) install-amd64 install-i386
-	install pressure-vessel-unruntime relocatable-install/bin/
-	install pressure-vessel-wrap relocatable-install/bin/
-	mkdir -p relocatable-install/sources
-	install -m644 THIRD-PARTY.md relocatable-install/sources/README.txt
-	install -m644 libcapsule/debian/copyright relocatable-install/sources/capsule-capture-libs.txt
-	install -m644 /usr/share/doc/zlib1g/copyright relocatable-install/sources/libz.txt
-	install -m644 /usr/share/doc/libelf1/copyright relocatable-install/sources/libelf.txt
-	dcmd install -m644 libcapsule*.dsc relocatable-install/sources/
-	cd relocatable-install/sources; \
-	apt-get --download-only source elfutils zlib
-
-install-%:
-	mkdir -p relocatable-install/bin
-	set -eu; \
-	dhm="$$(dpkg-architecture -a"$*" -qDEB_HOST_MULTIARCH)"; \
-	mkdir -p "relocatable-install/lib/$${dhm}"; \
-	if [ -n "$(_relocatabledir)" ]; then \
-		install $(_relocatabledir)/$${dhm}-capsule-capture-libs _build/; \
-		install $(_relocatabledir)/$${dhm}-capsule-symbols _build/; \
-	else \
-		$(MAKE) -f Makefile.libcapsule $*; \
-		install "_build/$*/libcapsule/capsule-capture-libs" "_build/$${dhm}-capsule-capture-libs"; \
-		install "_build/$*/libcapsule/capsule-symbols" "_build/$${dhm}-capsule-symbols"; \
-		chrpath -r "\$${ORIGIN}/../lib/$${dhm}" "_build/$${dhm}"-*; \
-		install "_build/$${dhm}-capsule-capture-libs" relocatable-install/bin; \
-		install "_build/$${dhm}-capsule-symbols" relocatable-install/bin; \
-	fi; \
-	mkdir -p _build/$*/lib; \
-	"relocatable-install/bin/$${dhm}-capsule-capture-libs" \
-	    --dest=_build/$*/lib \
-	    --no-glibc \
-	    "soname:libelf.so.1" \
-	    "soname:libz.so.1" \
-	    $(NULL); \
-	cp -a --dereference _build/$*/lib/*.so.* "relocatable-install/lib/$${dhm}"
+	./build-relocatable-install.py $(_relocatabledir)
 
 check:
-	prove -v t/
+	prove -v t/*.sh
 
 binary:
 	$(MAKE) install
diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md
index ae8f8e05f4031a516d27927ba03a979f7c8abee1..2cdb3204d59625f300d1019839666fc109820b06 100644
--- a/THIRD-PARTY.md
+++ b/THIRD-PARTY.md
@@ -3,7 +3,7 @@ Third-party modules
 
 pressure-vessel includes the capsule-capture-libs utility from libcapsule,
 which is licensed under [LGPL-2.1-or-later][]. See
-capsule-capture-libs.txt. Complete source code can be obtained from the
+libcapsule.txt. Complete source code can be obtained from the
 same place as pressure-vessel, or from
 <https://gitlab.collabora.com/vivek/libcapsule/>.
 
@@ -11,9 +11,9 @@ Binary releases of pressure-vessel include the following third-party
 libraries:
 
 - libelf.so.1 from Red Hat elfutils: [GPL-2.0-or-later][] with linking
-  exception. See libelf.txt.
+  exception. See elfutils.txt.
 
-- libz.so.1 from zlib: [Zlib][] license. See libz.txt.
+- libz.so.1 from zlib: [Zlib][] license. See zlib.txt.
 
 Complete source code can be obtained from the same place as
 pressure-vessel, or from
diff --git a/build-relocatable-install.py b/build-relocatable-install.py
new file mode 100755
index 0000000000000000000000000000000000000000..2e2a0e0e4263d962ad0e48bb5defd584365fa596
--- /dev/null
+++ b/build-relocatable-install.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+
+# Copyright © 2017-2019 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import argparse
+import glob
+import os
+import re
+import shutil
+import subprocess
+
+try:
+    import typing
+except ImportError:
+    pass
+else:
+    typing      # silence pyflakes
+
+
+# Debian architecture => Debian multiarch tuple
+ARCHS = {
+    'amd64': 'x86_64-linux-gnu',
+    'i386': 'i386-linux-gnu',
+}
+# package to install from => source package for copyright information
+DEPENDENCIES = {
+    'libcapsule-tools-relocatable': 'libcapsule',
+    'libelf1': 'elfutils',
+    'zlib1g': 'zlib',
+}
+DESTDIR = 'relocatable-install'
+SCRIPTS = ('pressure-vessel-unruntime', 'pressure-vessel-wrap')
+TOOLS = ('capsule-capture-libs', 'capsule-symbols')
+
+
+def install(src, dst, mode=0o644):
+    # type: (str, str, int) -> None
+    shutil.copy(src, dst)
+
+    if os.path.isdir(dst):
+        dst = os.path.join(dst, os.path.basename(src))
+
+    os.chmod(dst, mode)
+
+
+def install_exe(src, dst, mode=0o755):
+    # type: (str, str, int) -> None
+    install(src, dst, mode)
+
+
+def v_check_call(command, **kwargs):
+    print('# {}'.format(command))
+    subprocess.check_call(command, **kwargs)
+
+
+def v_check_output(command, **kwargs):
+    print('# {}'.format(command))
+    return subprocess.check_output(command, **kwargs)
+
+
+def main():
+    # type: () -> None
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('relocatabledir', default=None, nargs='?')
+    args = parser.parse_args()
+
+    if os.path.exists(DESTDIR):
+        shutil.rmtree(DESTDIR)
+
+    os.makedirs(os.path.join(DESTDIR, 'bin'), exist_ok=True)
+    os.makedirs(os.path.join(DESTDIR, 'sources'), exist_ok=True)
+
+    for ma in ARCHS.values():
+        os.makedirs(
+            os.path.join(DESTDIR, 'lib', ma),
+            exist_ok=True,
+        )
+
+    for script in SCRIPTS:
+        install_exe(script, os.path.join(DESTDIR, 'bin'))
+
+    install(
+        'THIRD-PARTY.md',
+        os.path.join(DESTDIR, 'sources', 'README.txt'),
+        0o644,
+    )
+
+    if args.relocatabledir is not None:
+        for arch, ma in ARCHS.items():
+            for tool in TOOLS:
+                install_exe(
+                    os.path.join(
+                        args.relocatabledir,
+                        '{}-{}'.format(ma, tool),
+                    ),
+                    os.path.join(DESTDIR, 'bin'),
+                )
+
+        install(
+            '/usr/share/doc/libcapsule-tools-relocatable/copyright',
+            os.path.join(DESTDIR, 'sources', 'libcapsule.txt'),
+        )
+    else:
+        v_check_call(['make', '-f', 'Makefile.libcapsule'])
+
+        for arch, ma in ARCHS.items():
+            for tool in TOOLS:
+                install_exe(
+                    os.path.join('_build', arch, 'libcapsule', tool),
+                    os.path.join('_build', '{}-{}'.format(ma, tool)),
+                )
+                v_check_call([
+                    'chrpath', '-r',
+                    '${ORIGIN}/../lib/' + ma,
+                    os.path.join('_build', '{}-{}'.format(ma, tool)),
+                ])
+                install_exe(
+                    os.path.join('_build', '{}-{}'.format(ma, tool)),
+                    os.path.join(DESTDIR, 'bin'),
+                )
+
+            install(
+                os.path.join('libcapsule', 'debian', 'copyright'),
+                os.path.join(DESTDIR, 'sources', 'libcapsule.txt'),
+            )
+
+            for dsc in glob.glob('libcapsule*.dsc'):
+                v_check_call([
+                    'dcmd', 'install', '-m644', dsc,
+                    os.path.join(DESTDIR, 'sources'),
+                ])
+
+    for arch, ma in ARCHS.items():
+        os.makedirs(os.path.join('_build', arch, 'lib'), exist_ok=True)
+        v_check_call([
+            '{}/bin/{}-capsule-capture-libs'.format(DESTDIR, ma),
+            '--dest=_build/{}/lib'.format(arch),
+            '--no-glibc',
+            'soname:libelf.so.1',
+            'soname:libz.so.1',
+        ])
+
+        for so in glob.glob(
+            os.path.join('_build', arch, 'lib', '*.so.*'),
+        ):
+            install(so, os.path.join(DESTDIR, 'lib', 'ma'))
+
+    source_to_download = set()      # type: typing.Set[str]
+
+    for package, source in DEPENDENCIES.items():
+        if args.relocatabledir is None and source == 'libcapsule':
+            continue
+
+        install(
+            '/usr/share/doc/{}/copyright'.format(package),
+            os.path.join(DESTDIR, 'sources', '{}.txt'.format(source)),
+        )
+        install(
+            '/usr/share/doc/{}/copyright'.format(package),
+            os.path.join(DESTDIR, 'sources', '{}.txt'.format(source)),
+        )
+
+        for expr in set(
+            v_check_output([
+                'dpkg-query',
+                '-W',
+                '-f', '${source:Package}=${source:Version}\n',
+                package,
+            ], universal_newlines=True).splitlines()
+        ):
+            source_to_download.add(re.sub(r'[+]srt[0-9a-z.]+$', '', expr))
+
+    v_check_call(
+        [
+            'apt-get',
+            '--download-only',
+            '--only-source',
+            'source',
+        ] + list(source_to_download),
+        cwd=os.path.join(DESTDIR, 'sources'),
+    )
+
+
+if __name__ == '__main__':
+    main()
+
+# vim:set sw=4 sts=4 et:
diff --git a/t/mypy.sh b/t/mypy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..64238232c5d8d8bbc7354defa93b4f059ae2d32e
--- /dev/null
+++ b/t/mypy.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Copyright © 2016-2018 Simon McVittie
+# Copyright © 2018 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+# (See build-relocatable-install.py)
+
+set -e
+set -u
+
+export MYPYPATH="${PYTHONPATH:=$(pwd)}"
+
+i=0
+for script in \
+    ./*.py \
+; do
+    i=$((i + 1))
+    if [ "x${MYPY:="$(command -v mypy || echo false)"}" = xfalse ]; then
+        echo "ok $i - $script # SKIP mypy not found"
+    elif "${MYPY}" \
+            --python-executable="${PYTHON:=python3}" \
+            --follow-imports=skip \
+            --ignore-missing-imports \
+            "$script"; then
+        echo "ok $i - $script"
+    else
+        echo "not ok $i - $script # TODO mypy issues reported"
+    fi
+done
+echo "1..$i"
+
+# vim:set sw=4 sts=4 et:
diff --git a/t/pycodestyle.sh b/t/pycodestyle.sh
new file mode 100755
index 0000000000000000000000000000000000000000..04209a490165d2ea7db1de7a7e76bfbda5a5c3b4
--- /dev/null
+++ b/t/pycodestyle.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Copyright © 2016-2018 Simon McVittie
+# Copyright © 2018 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+# (See build-relocatable-install.py)
+
+set -e
+set -u
+
+if [ "x${PYCODESTYLE:=pycodestyle}" = xfalse ] || \
+        [ -z "$(command -v "$PYCODESTYLE")" ]; then
+    echo "1..0 # SKIP pycodestyle not found"
+    exit 0
+fi
+
+echo "1..1"
+
+if "${PYCODESTYLE}" \
+    ./*.py \
+    >&2; then
+    echo "ok 1 - $PYCODESTYLE reported no issues"
+else
+    echo "not ok 1 # TODO $PYCODESTYLE issues reported"
+fi
+
+# vim:set sw=4 sts=4 et:
diff --git a/t/pyflakes.sh b/t/pyflakes.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f90936a6ab89eb5db7a0860de7d22853423b91ef
--- /dev/null
+++ b/t/pyflakes.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+# Copyright © 2016-2018 Simon McVittie
+# Copyright © 2018 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+# (See build-relocatable-install.py)
+
+set -e
+set -u
+
+if [ "x${PYFLAKES:=pyflakes3}" = xfalse ] || \
+        [ -z "$(command -v "$PYFLAKES")" ]; then
+    echo "1..0 # SKIP pyflakes3 not found"
+elif "${PYFLAKES}" \
+    ./*.py \
+    >&2; then
+    echo "1..1"
+    echo "ok 1 - $PYFLAKES reported no issues"
+else
+    echo "1..1"
+    echo "not ok 1 # TODO $PYFLAKES issues reported"
+fi
+
+# vim:set sw=4 sts=4 et:
diff --git a/t/shellcheck.t b/t/shellcheck.sh
similarity index 94%
rename from t/shellcheck.t
rename to t/shellcheck.sh
index 4fe7fcb3d57eeacdfc26ef0ab893eda1021cfd6b..663b73dd5335dd4e8ee7f6bd783a404b81b27961 100755
--- a/t/shellcheck.t
+++ b/t/shellcheck.sh
@@ -36,9 +36,7 @@ cd "$TOP_SRCDIR"
 
 n=0
 for shell_script in \
-        pressure-vessel-wrap \
-        sysroot/*.sh \
-        t/shellcheck.t \
+        t/*.sh \
         ; do
     n=$((n + 1))