diff --git a/Makefile b/Makefile
deleted file mode 100644
index 136e2e6e74c1bee4c1746181cf5215701da7282e..0000000000000000000000000000000000000000
--- a/Makefile
+++ /dev/null
@@ -1,30 +0,0 @@
-VERSION := $(shell ./build-aux/git-version-gen .tarball-version)
-PYTHON ?= python3
-relocatabledir = /usr/lib/libcapsule/relocatable
-
-all: binary
-
-ifeq (,$(wildcard libcapsule_*.dsc))
-_relocatabledir = $(relocatabledir)
-else
-_relocatabledir =
-endif
-
-install:
-	$(PYTHON) ./build-relocatable-install.py $(_relocatabledir)
-
-check:
-	prove -v t/*.sh
-
-binary:
-	$(MAKE) install
-	tar --transform='s,^relocatable-install,pressure-vessel-$(VERSION),' -zcvf pressure-vessel-$(VERSION)-bin+src.tar.gz.tmp relocatable-install
-	tar --transform='s,^relocatable-install,pressure-vessel-$(VERSION),' --exclude=sources -zcvf pressure-vessel-$(VERSION)-bin.tar.gz.tmp relocatable-install
-	mv pressure-vessel-$(VERSION)-bin+src.tar.gz.tmp pressure-vessel-$(VERSION)-bin+src.tar.gz
-	mv pressure-vessel-$(VERSION)-bin.tar.gz.tmp pressure-vessel-$(VERSION)-bin.tar.gz
-
-clean:
-	rm -fr pressure-vessel-[0-9]*.tar.gz
-	rm -fr _build
-	rm -fr relocatable-install
-	rm -fr libcapsule
diff --git a/Makefile.libcapsule b/Makefile.libcapsule
deleted file mode 100644
index 6e2334396123857ada5c507d58df7ec935a0d92d..0000000000000000000000000000000000000000
--- a/Makefile.libcapsule
+++ /dev/null
@@ -1,46 +0,0 @@
-all: amd64 i386
-amd64 i386: %: _build/%/build.stamp
-
-libcapsule/configure: $(wildcard libcapsule*.dsc)
-	rm -fr libcapsule
-	first=; \
-	for t in libcapsule*.dsc; do \
-		if [ -z "$$first" ]; then \
-			first="$$t"; \
-		else \
-			echo "Exactly one libcapsule*.dsc is required" >&2; \
-			exit 1; \
-		fi; \
-	done
-	dpkg-source -x libcapsule*.dsc
-	mv libcapsule-*/ libcapsule
-	set -e; cd libcapsule; NOCONFIGURE=1 ./autogen.sh
-
-_build/%/config.stamp: libcapsule/configure
-	mkdir -p _build/$*/libcapsule
-	set -eu; \
-	DEB_BUILD_ARCH="$$(dpkg-architecture -a"$*" -qDEB_BUILD_ARCH)"; \
-	DEB_HOST_ARCH="$$(dpkg-architecture -a"$*" -qDEB_HOST_ARCH)"; \
-	DEB_BUILD_GNU_TYPE="$$(dpkg-architecture -a"$*" -qDEB_BUILD_GNU_TYPE)"; \
-	DEB_HOST_GNU_TYPE="$$(dpkg-architecture -a"$*" -qDEB_HOST_GNU_TYPE)"; \
-	DEB_HOST_MULTIARCH="$$(dpkg-architecture -a"$*" -qDEB_HOST_MULTIARCH)"; \
-	case "$${DEB_BUILD_ARCH}/$${DEB_HOST_ARCH}" in \
-	    (amd64/i386) \
-	        export CC="cc -m32"; \
-	        ;; \
-	esac; \
-	cd _build/$*/libcapsule; \
-	../../../libcapsule/configure \
-	    --build="$${DEB_BUILD_GNU_TYPE}" \
-	    --host="$${DEB_HOST_GNU_TYPE}" \
-	    --enable-host-prefix="$${DEB_HOST_MULTIARCH}-" \
-	    --enable-tools-rpath="/_ORIGIN_/__/lib/$${DEB_HOST_MULTIARCH}" \
-	    --disable-shared \
-	    --disable-gtk-doc \
-	    --without-glib \
-	    $(NULL)
-	touch $@
-
-_build/%/build.stamp: _build/%/config.stamp
-	$(MAKE) -C _build/$*/libcapsule
-	touch $@
diff --git a/README.md b/README.md
index 66f9035b0f5118bc91e06558d3d86a0347472a27..4595e77a8a9cebdbf2c9a15169a72de081db2785 100644
--- a/README.md
+++ b/README.md
@@ -42,15 +42,35 @@ The most straightforward method is to have a prebuilt version of
 libcapsule-tools-relocatable:amd64 and libcapsule-tools-relocatable:i386
 in your build environment. For example, you could do the build in a
 SteamRT 1 'scout' or SteamRT 1.5 'heavy' Docker image that has those
-packages already. Then you can just do:
+packages already. Then you can do:
 
-    make
+    meson --prefix=$(pwd)/_build/relocatable-install _build
+    ninja -C _build
+    meson test -v -C _build             # optional
+    ninja -C _build install
 
-Or, if your build environment has those packages available but
-uninstalled, you can unpack them with `dpkg-deb -x` and use something
+For more convenient use on a development system, if you have a
+SteamRT 1 'scout' SDK tarball or an unpacked sysroot, you can place a
+tarball at `_build/sysroot.tar.gz` or unpack a sysroot into
+`_build/sysroot`, and prefix those commands with
+`./sysroot/run-in-sysroot.py`:
+
+    ./sysroot/run-in-sysroot.py meson ...
+
+(Or put them in different locations and pass the `--sysroot` and
+`--tarball` options to `./sysroot/run-in-sysroot.py`.)
+
+If your build environment has `libcapsule-tools-relocatable` available
+but uninstalled, you can unpack them with `dpkg-deb -x` and use something
 like:
 
-    make relocatabledir=./usr/lib/libcapsule/relocatable
+    meson \
+        --prefix=$(pwd)/_build/relocatable-install \
+        -Drelocatabledir=./usr/lib/libcapsule/relocatable \
+        _build
+    ninja -C _build
+    meson test -v -C _build             # optional
+    ninja -C _build install
 
 Alternatively, build a Debian source package (`.dsc`, `.debian.tar.*`,
 `.orig.tar.*` for libcapsule 0.20190402.0 or later, which will require
@@ -59,41 +79,12 @@ Put it in the top-level directory of `pressure-vessel`, for example:
 
     dcmd cp ../build-area/libcapsule_0.20190402.0-0co1.dsc .
 
-After that, the simplest way is (again) to do the build already inside
-a suitable container:
-
-    make
-
-Alternatively, you can use `bubblewrap` to enter a sysroot prepared
-using [debos][] and [qemu-system-x86_64][qemu]. If
-you have all those, you can just run:
-
-    make -C sysroot
-
-which will automatically build and use the sysroot. (This sysroot
-won't have libcapsule-tools-relocatable, so you will need a
-`libcapsule_*.dsc` or the `relocatabledir` variable, as above.)
-
-Alternatively, prepare a Debian jessie sysroot with `deb`
-and `deb-src` sources and an `i386` foreign architecture (see
-`sysroot/configure-sources.sh`), all the packages listed in
-`sysroot/install-dependencies.sh`, and optionally
-libcapsule-tools-relocatable. Then use:
-
-    make -C sysroot sysroot=/path/to/sysroot
-
-or compress a similar chroot into a gzipped tar file (containing `./etc`,
-`./usr` and so on, as used by [lxc][] and [pbuilder][]) and use:
-
-    make -C sysroot tarball=/path/to/tarball.tar.gz
-
-Whichever method you use, binaries and source code end up in
-`relocatable-install/` which can be copied to wherever you want.
+There is experimental support in `sysroot/` for building a container
+for the build using debos, but it requires an unusual container
+(Debian jessie or SteamRT 1.5 'heavy' with a backport of meson) and
+so is not currently recommended.
 
-[debos]: https://github.com/go-debos/debos
-[lxc]: https://github.com/lxc/lxc
-[pbuilder]: https://pbuilder.alioth.debian.org/
-[qemu]: https://www.qemu.org/
+The relocatable install ends up in `_build/relocatable-install`.
 
 Instructions for testing
 ------------------------
diff --git a/build-relocatable-install.py b/build-relocatable-install.py
index e48f46ef1b80211bd505433ca8ba66f9302970d9..80550123a6bbe12e29cd797d9d3d63a7ccc5a80e 100755
--- a/build-relocatable-install.py
+++ b/build-relocatable-install.py
@@ -27,7 +27,6 @@ import argparse
 import glob
 import os
 import re
-import shlex
 import shutil
 import subprocess
 
@@ -81,7 +80,6 @@ PRIMARY_ARCH_DEPENDENCIES = {
     'libselinux1': 'libselinux',
     'libpcre3': 'pcre3',
 }
-DESTDIR = 'relocatable-install'
 SCRIPTS = [
     'pressure-vessel-unruntime',
     'pressure-vessel-wrap'
@@ -121,31 +119,57 @@ def main():
     # type: () -> None
 
     parser = argparse.ArgumentParser()
-    parser.add_argument('relocatabledir', default=None, nargs='?')
+    parser.add_argument(
+        '--destdir', default=os.getenv('DESTDIR', ''))
+    parser.add_argument(
+        '--srcdir', default=os.getenv('MESON_SOURCE_ROOT', '.'))
+    parser.add_argument(
+        '--builddir', default=os.getenv('MESON_BUILD_ROOT', '.'))
+    parser.add_argument(
+        '--prefix',
+        default=os.getenv(
+            'MESON_INSTALL_PREFIX',
+            os.path.abspath('relocatable-build'),
+        ),
+    )
+    parser.add_argument(
+        '--archive', default=os.getenv('MESON_BUILD_ROOT', '.'))
+    parser.add_argument('--relocatabledir', default='')
+    parser.add_argument('--set-version', dest='version', default='unknown')
     args = parser.parse_args()
 
-    if os.path.exists(DESTDIR):
-        shutil.rmtree(DESTDIR)
+    os.chdir(args.builddir)
+
+    destdir_prefix = os.getenv(
+        'MESON_INSTALL_DESTDIR_PREFIX',
+        args.destdir + args.prefix,
+    )
 
-    os.makedirs(os.path.join(DESTDIR, 'bin'), exist_ok=True)
-    os.makedirs(os.path.join(DESTDIR, 'sources'), exist_ok=True)
+    if os.path.exists(destdir_prefix):
+        shutil.rmtree(destdir_prefix)
+
+    os.makedirs(os.path.join(destdir_prefix, 'bin'), exist_ok=True)
+    os.makedirs(os.path.join(destdir_prefix, 'sources'), exist_ok=True)
 
     for arch in ARCHS:
         os.makedirs(
-            os.path.join(DESTDIR, 'lib', arch.multiarch),
+            os.path.join(destdir_prefix, 'lib', arch.multiarch),
             exist_ok=True,
         )
 
     for script in SCRIPTS:
-        install_exe(script, os.path.join(DESTDIR, 'bin'))
+        install_exe(
+            os.path.join(args.srcdir, script),
+            os.path.join(destdir_prefix, 'bin'),
+        )
 
     install(
-        'THIRD-PARTY.md',
-        os.path.join(DESTDIR, 'sources', 'README.txt'),
+        os.path.join(args.srcdir, 'THIRD-PARTY.md'),
+        os.path.join(destdir_prefix, 'sources', 'README.txt'),
         0o644,
     )
 
-    if args.relocatabledir is not None:
+    if args.relocatabledir:
         for arch in ARCHS:
             for tool in TOOLS:
                 install_exe(
@@ -153,22 +177,25 @@ def main():
                         args.relocatabledir,
                         '{}-{}'.format(arch.multiarch, tool),
                     ),
-                    os.path.join(DESTDIR, 'bin'),
+                    os.path.join(destdir_prefix, 'bin'),
                 )
 
         install(
             '/usr/share/doc/libcapsule-tools-relocatable/copyright',
-            os.path.join(DESTDIR, 'sources', 'libcapsule.txt'),
+            os.path.join(destdir_prefix, 'sources', 'libcapsule.txt'),
         )
     else:
-        v_check_call(['make', '-f', 'Makefile.libcapsule'])
-
         for arch in ARCHS:
             for tool in TOOLS:
                 install_exe(
-                    os.path.join('_build', arch.name, 'libcapsule', tool),
                     os.path.join(
-                        '_build',
+                        'build-relocatable',
+                        arch.name,
+                        'libcapsule',
+                        tool,
+                    ),
+                    os.path.join(
+                        'build-relocatable',
                         '{}-{}'.format(arch.multiarch, tool),
                     ),
                 )
@@ -176,27 +203,34 @@ def main():
                     'chrpath', '-r',
                     '${ORIGIN}/../lib/' + arch.multiarch,
                     os.path.join(
-                        '_build',
+                        'build-relocatable',
                         '{}-{}'.format(arch.multiarch, tool),
                     ),
                 ])
                 install_exe(
                     os.path.join(
-                        '_build',
+                        'build-relocatable',
                         '{}-{}'.format(arch.multiarch, tool),
                     ),
-                    os.path.join(DESTDIR, 'bin'),
+                    os.path.join(destdir_prefix, 'bin'),
                 )
 
             install(
-                os.path.join('libcapsule', 'debian', 'copyright'),
-                os.path.join(DESTDIR, 'sources', 'libcapsule.txt'),
+                os.path.join(
+                    args.builddir,
+                    'libcapsule',
+                    'debian',
+                    'copyright',
+                ),
+                os.path.join(destdir_prefix, 'sources', 'libcapsule.txt'),
             )
 
-            for dsc in glob.glob('libcapsule*.dsc'):
+            for dsc in glob.glob(
+                os.path.join(args.srcdir, 'libcapsule*.dsc')
+            ):
                 v_check_call([
                     'dcmd', 'install', '-m644', dsc,
-                    os.path.join(DESTDIR, 'sources'),
+                    os.path.join(destdir_prefix, 'sources'),
                 ])
 
     primary_architecture = subprocess.check_output([
@@ -204,11 +238,17 @@ def main():
     ]).decode('utf-8').strip()
 
     for arch in ARCHS:
-        os.makedirs(os.path.join('_build', arch.name, 'lib'), exist_ok=True)
+        os.makedirs(
+            os.path.join('build-relocatable', arch.name, 'lib'),
+            exist_ok=True,
+        )
 
         v_check_call([
-            '{}/bin/{}-capsule-capture-libs'.format(DESTDIR, arch.multiarch),
-            '--dest=_build/{}/lib'.format(arch.name),
+            '{}/bin/{}-capsule-capture-libs'.format(
+                destdir_prefix,
+                arch.multiarch,
+            ),
+            '--dest=build-relocatable/{}/lib'.format(arch.name),
             '--no-glibc',
             'soname:libelf.so.1',
             'soname:libz.so.1',
@@ -217,10 +257,10 @@ def main():
         if arch.name == primary_architecture:
             v_check_call([
                 '{}/bin/{}-capsule-capture-libs'.format(
-                    DESTDIR,
+                    destdir_prefix,
                     arch.multiarch,
                 ),
-                '--dest=_build/{}/lib'.format(arch.name),
+                '--dest=build-relocatable/{}/lib'.format(arch.name),
                 '--no-glibc',
                 'soname:libcap.so.2',
                 'soname:libpcre.so.3',
@@ -228,9 +268,9 @@ def main():
             ])
 
         for so in glob.glob(
-            os.path.join('_build', arch.name, 'lib', '*.so.*'),
+            os.path.join('build-relocatable', arch.name, 'lib', '*.so.*'),
         ):
-            install(so, os.path.join(DESTDIR, 'lib', arch.multiarch))
+            install(so, os.path.join(destdir_prefix, 'lib', arch.multiarch))
 
     # For bwrap (and possibly other programs in future) we don't have
     # a relocatable version with a RPATH/RUNPATH, so we wrap a script
@@ -246,10 +286,12 @@ def main():
                 package,
             ])
             v_check_call(
-                'dpkg-deb -x {}_*.deb _build'.format(shlex.quote(package)),
+                'dpkg-deb -x {}_*.deb build-relocatable'.format(
+                    shlex.quote(package),
+                ),
                 shell=True,
             )
-            path = '_build/usr/bin/{}'.format(exe)
+            path = 'build-relocatable/usr/bin/{}'.format(exe)
 
         for arch in ARCHS:
             if arch.name != primary_architecture:
@@ -257,11 +299,11 @@ def main():
 
             install_exe(
                 path,
-                os.path.join(DESTDIR, 'bin', exe + '.bin'),
+                os.path.join(destdir_prefix, 'bin', exe + '.bin'),
             )
 
             with open(
-                os.path.join('_build', arch.name, exe),
+                os.path.join('build-relocatable', arch.name, exe),
                 'w',
             ) as writer:
                 writer.write('#!/bin/sh\n')
@@ -277,8 +319,8 @@ def main():
                 )
 
             install_exe(
-                os.path.join('_build', arch.name, exe),
-                os.path.join(DESTDIR, 'bin', exe),
+                os.path.join('build-relocatable', arch.name, exe),
+                os.path.join(destdir_prefix, 'bin', exe),
             )
 
     source_to_download = set()      # type: typing.Set[str]
@@ -286,13 +328,17 @@ def main():
     for package, source in (
         list(DEPENDENCIES.items()) + list(PRIMARY_ARCH_DEPENDENCIES.items())
     ):
-        if args.relocatabledir is None and source == 'libcapsule':
+        if not args.relocatabledir and source == 'libcapsule':
             continue
 
         if os.path.exists('/usr/share/doc/{}/copyright'.format(package)):
             install(
                 '/usr/share/doc/{}/copyright'.format(package),
-                os.path.join(DESTDIR, 'sources', '{}.txt'.format(source)),
+                os.path.join(
+                    destdir_prefix,
+                    'sources',
+                    '{}.txt'.format(source),
+                ),
             )
 
             for expr in set(
@@ -306,8 +352,12 @@ def main():
                 source_to_download.add(re.sub(r'[+]srt[0-9a-z.]+$', '', expr))
         else:
             install(
-                '_build/usr/share/doc/{}/copyright'.format(package),
-                os.path.join(DESTDIR, 'sources', '{}.txt'.format(source)),
+                'build-relocatable/usr/share/doc/{}/copyright'.format(package),
+                os.path.join(
+                    destdir_prefix,
+                    'sources',
+                    '{}.txt'.format(source),
+                ),
             )
             source_to_download.add(source)
 
@@ -318,9 +368,36 @@ def main():
             '--only-source',
             'source',
         ] + list(source_to_download),
-        cwd=os.path.join(DESTDIR, 'sources'),
+        cwd=os.path.join(destdir_prefix, 'sources'),
     )
 
+    bin_tar = 'pressure-vessel-{}-bin.tar.gz'.format(args.version)
+    src_tar = 'pressure-vessel-{}-bin+src.tar.gz'.format(args.version)
+
+    subprocess.check_call([
+        'tar',
+        r'--transform=s,^\(\.\(/\|$\)\)\?,pressure-vessel-{}/,'.format(
+            args.version,
+        ),
+        '-zcvf', src_tar + '.tmp',
+        '-C', destdir_prefix,
+        '.',
+    ])
+    subprocess.check_call([
+        'tar',
+        r'--transform=s,^\(\.\(/\|$\)\)\?,pressure-vessel-{}/,'.format(
+            args.version,
+        ),
+        '--exclude=sources',
+        '-zcvf', bin_tar + '.tmp',
+        '-C', destdir_prefix,
+        '.',
+    ])
+    os.rename(bin_tar + '.tmp', bin_tar)
+    os.rename(src_tar + '.tmp', src_tar)
+    print('Generated {}'.format(os.path.abspath(bin_tar)))
+    print('Generated {}'.format(os.path.abspath(src_tar)))
+
 
 if __name__ == '__main__':
     main()
diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile
index c0d9a2261a86a3865eb2befb781aa5dcc522257f..270f34e91c7bdcafd79b84f966b551f8daf2e30a 100644
--- a/ci/Jenkinsfile
+++ b/ci/Jenkinsfile
@@ -62,8 +62,11 @@ pipeline {
             docker.image("${env.CI_DOCKER_REGISTRY}/steamrt/sdk:scout").inside('--mount type=tmpfs,destination=/var/cache/apt/archives') {
               sh '''
               set -eu
-              make -C src PYTHON=python3.5
-              make -C src check PYTHON=python3.5
+              cd src
+              meson --prefix="${WORKSPACE}/src/_build/relocatable-install" -Dpython=python3.5 _build
+              ninja -C _build
+              ninja -C _build install
+              meson test --verbose -C _build
               '''
             }
           }
@@ -73,14 +76,18 @@ pipeline {
           docker.image('debian:buster-slim').inside() {
             sh '''
             set -eu
+            export G_TEST_SRCDIR="${WORKSPACE}/src"
+            export G_TEST_BUILDDIR="${WORKSPACE}/src/_build"
             cd src
             ./t/relocatable-install.sh
             '''
           }
         }
 
-        archiveArtifacts 'src/pressure-vessel-*-bin.tar.gz'
-        archiveArtifacts 'src/pressure-vessel-*-bin+src.tar.gz'
+        dir('src/_build') {
+          archiveArtifacts 'pressure-vessel-*-bin.tar.gz'
+          archiveArtifacts 'pressure-vessel-*-bin+src.tar.gz'
+        }
       }
     }
   }
diff --git a/configure-libcapsule.sh b/configure-libcapsule.sh
new file mode 100644
index 0000000000000000000000000000000000000000..435cfb8affe2697d22c36616f8759a219388870c
--- /dev/null
+++ b/configure-libcapsule.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+#
+# Copyright © 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.
+
+set -eu
+NULL=
+
+DEB_BUILD_ARCH="$(dpkg-architecture -a"$1" -qDEB_BUILD_ARCH)"
+DEB_HOST_ARCH="$(dpkg-architecture -a"$1" -qDEB_HOST_ARCH)"
+DEB_BUILD_GNU_TYPE="$(dpkg-architecture -a"$1" -qDEB_BUILD_GNU_TYPE)"
+DEB_HOST_GNU_TYPE="$(dpkg-architecture -a"$1" -qDEB_HOST_GNU_TYPE)"
+DEB_HOST_MULTIARCH="$(dpkg-architecture -a"$1" -qDEB_HOST_MULTIARCH)"
+
+case "${DEB_BUILD_ARCH}/${DEB_HOST_ARCH}" in
+    (amd64/i386)
+        export CC="cc -m32"
+        ;;
+esac
+
+mkdir -p "build-relocatable/$1/libcapsule"
+
+(
+  cd "build-relocatable/$1/libcapsule"
+  ../../../libcapsule/configure \
+      --build="${DEB_BUILD_GNU_TYPE}" \
+      --host="${DEB_HOST_GNU_TYPE}" \
+      --enable-host-prefix="${DEB_HOST_MULTIARCH}-" \
+      --enable-tools-rpath="/_ORIGIN_/__/lib/${DEB_HOST_MULTIARCH}" \
+      --disable-shared \
+      --disable-gtk-doc \
+      --without-glib \
+      ${NULL}
+)
+
+touch "$2"
+
+# vim:set sw=2 sts=2 et:
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..0d3c7e0478475aa64036547569a386a28bbf34c1
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,166 @@
+# Copyright © 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.
+
+project('pressure-vessel', 'c')
+
+prove = find_program('prove', required : false)
+python = find_program(get_option('python'), required : true)
+sh = find_program('sh', required : true)
+
+version = get_option('version')
+if version == 'auto'
+  git_version_gen = run_command(
+    sh,
+    files('build-aux/git-version-gen'),
+    join_paths(meson.current_source_dir(), '.tarball-version'),
+    check : true,
+  )
+  version = git_version_gen.stdout().strip()
+endif
+
+scripts = [
+  'pressure-vessel-wrap',
+  'pressure-vessel-unruntime',
+]
+
+tests = [
+  'mypy',
+  'pycodestyle',
+  'pyflakes',
+  'relocatable-install',
+  'shellcheck',
+]
+
+test_env = environment()
+test_env.set('G_TEST_SRCDIR', meson.current_source_dir())
+test_env.set('G_TEST_BUILDDIR', meson.current_build_dir())
+
+foreach test_name : tests
+  if prove.found()
+    test(
+      test_name, prove,
+      args : ['-v', files('t/' + test_name + '.sh')],
+      env : test_env,
+    )
+  endif
+endforeach
+
+foreach script : scripts
+  install_data(
+    script,
+    install_dir : get_option('bindir'),
+  )
+endforeach
+
+relocatabledir = get_option('relocatabledir')
+
+if relocatabledir == 'auto'
+  dsc_exists = run_command(
+    sh,
+    '-euc',
+    '''
+      cd "$MESON_SOURCE_ROOT"
+      if test -e libcapsule_*.dsc
+        then echo libcapsule_*.dsc
+      else
+        exit 1
+      fi
+    ''',
+  )
+  if dsc_exists.returncode() == 0
+    libcapsule_dsc = dsc_exists.stdout().strip()
+    relocatabledir = ''
+  else
+    libcapsule_dsc = ''
+    relocatabledir = '/usr/lib/libcapsule/relocatable'
+  endif
+endif
+
+if libcapsule_dsc != ''
+  libcapsule_configure = custom_target(
+    'libcapsule_configure',
+    command: [
+      sh,
+      '-euc',
+      '''
+        rm -fr libcapsule
+        dpkg-source -x "$1"
+        mv libcapsule-* libcapsule
+        ( cd libcapsule; NOCONFIGURE=1 ./autogen.sh )
+        touch "$2"
+      ''',
+      'sh',
+      '@INPUT@',
+      '@OUTPUT@',
+    ],
+    input : files(libcapsule_dsc),
+    output : 'libcapsule_configure.stamp',
+    build_by_default : true,
+  )
+
+  foreach arch : ['amd64', 'i386']
+    config_stamp = custom_target(
+      'build-relocatable_' + arch + '_config.stamp',
+      command : [
+        sh,
+        files('configure-libcapsule.sh'),
+        arch,
+        '@OUTPUT@',
+      ],
+      depends : libcapsule_configure,
+      output : 'build-relocatable_' + arch + '_config.stamp',
+      build_by_default : true,
+    )
+
+    custom_target(
+      'build-relocatable_' + arch + '_build.stamp',
+      command: [
+        sh,
+        '-euc',
+        '''
+          make -C "build-relocatable/$1/libcapsule"
+          touch "$2"
+        ''',
+        'sh',
+        arch,
+        '@OUTPUT@',
+      ],
+      depends : config_stamp,
+      output : 'build-relocatable_' + arch + '_build.stamp',
+      build_by_default : true,
+    )
+  endforeach
+endif
+
+meson.add_install_script(
+  python.path(),
+  join_paths(meson.current_source_dir(), 'build-relocatable-install.py'),
+  '--srcdir', meson.current_source_dir(),
+  '--builddir', meson.current_build_dir(),
+  '--relocatabledir', relocatabledir,
+  '--prefix', get_option('prefix'),
+  '--archive', meson.current_build_dir(),
+  '--set-version', version,
+)
+
+# vim:set sw=2 sts=2 et:
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fef87a65d198f3bfb9eb4b851a5c3c1181740bcc
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,3 @@
+option('python', type : 'string', value : 'python3')
+option('relocatabledir', type : 'string', value : 'auto')
+option('version', type : 'string', value : 'auto')
diff --git a/sysroot/Makefile b/sysroot/Makefile
deleted file mode 100644
index 48c712070e38ddd1bc176304e1860d4adc2cc117..0000000000000000000000000000000000000000
--- a/sysroot/Makefile
+++ /dev/null
@@ -1,47 +0,0 @@
-top_srcdir = $(dir $(CURDIR))
-top_builddir = $(dir $(CURDIR))
-
-all:
-
-mirror = http://deb.debian.org/debian
-security_mirror = http://security.debian.org/debian-security
-tarball = $(top_builddir)/_build/sysroot.tar.gz
-sysroot = $(top_builddir)/_build/sysroot
-
-$(top_builddir)/_build/sysroot/etc/debian_version: $(tarball)
-	rm -fr $(top_builddir)/_build/sysroot
-	mkdir -p $(top_builddir)/_build/sysroot
-	tar -zxf $(tarball) --exclude="./dev/*" -C $(top_builddir)/_build/sysroot
-	touch $@
-
-$(top_builddir)/_build/sysroot.tar.gz: $(wildcard *)
-	mkdir -p $(dir $@)
-	set -e; \
-	debos -t mirror:$(mirror) -t security_mirror:$(security_mirror) -t ospack:sysroot.tar.gz --artifactdir=$(top_builddir)/_build debos.yaml
-
-ifeq ($(sysroot),/)
-in_sysroot =
-else
-in_sysroot = \
-	bwrap \
-	--ro-bind $(sysroot) / \
-	--dev-bind /dev /dev \
-	--ro-bind /etc/resolv.conf /etc/resolv.conf \
-	--tmpfs /tmp \
-	--tmpfs /home \
-	--bind $(top_builddir) $(top_builddir) \
-	--bind $(top_srcdir) $(top_srcdir) \
-	--chdir $(top_builddir) \
-	--setenv LC_ALL C.UTF-8 \
-	$(NULL)
-endif
-
-all binary check install: $(sysroot)/etc/debian_version
-	$(in_sysroot) $(MAKE) -C $(top_builddir) $@
-
-clean:
-	if [ -e $(sysroot)/etc/debian_version ]; then \
-		$(in_sysroot) $(MAKE) -C $(top_builddir) $@; \
-	else \
-		$(MAKE) -C $(top_builddir) $@; \
-	fi
diff --git a/sysroot/install-dependencies.sh b/sysroot/install-dependencies.sh
index 7dea20906350b2545fc235f2751646853e20651f..12b2d191ca544f5cbc680a2bb986c6fa83dcb506 100755
--- a/sysroot/install-dependencies.sh
+++ b/sysroot/install-dependencies.sh
@@ -42,6 +42,7 @@ apt-get -q -y --no-install-recommends install \
     libelf-dev libelf-dev:i386 \
     libipc-run-perl \
     libjpeg62-turbo libjpeg62-turbo:i386 \
+    meson \
     perl \
     xsltproc \
     zlib1g zlib1g:i386 \
diff --git a/sysroot/run-in-sysroot.py b/sysroot/run-in-sysroot.py
new file mode 100755
index 0000000000000000000000000000000000000000..948b1e3107cf58dd887fcc09c044b0e2635a5284
--- /dev/null
+++ b/sysroot/run-in-sysroot.py
@@ -0,0 +1,120 @@
+#!/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 logging
+import os
+import subprocess
+import sys
+
+try:
+    import typing
+except ImportError:
+    pass
+else:
+    typing      # silence pyflakes
+
+
+logger = logging.getLogger('run-in-sysroot')
+
+
+def main():
+    # type: () -> int
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--srcdir', default='.')
+    parser.add_argument('--builddir', default='_build')
+    parser.add_argument('--sysroot', default=None)
+    parser.add_argument('--tarball', default=None)
+    parser.add_argument('command')
+    parser.add_argument('args', nargs=argparse.REMAINDER)
+    args = parser.parse_args()
+
+    abs_srcdir = os.path.abspath(args.srcdir)
+    abs_builddir = os.path.abspath(args.builddir)
+
+    if args.sysroot is None:
+        args.sysroot = os.path.join(abs_builddir, 'sysroot')
+
+    if args.tarball is None:
+        args.tarball = os.path.join(abs_builddir, 'sysroot.tar.gz')
+
+    abs_sysroot = os.path.abspath(args.sysroot)
+
+    if not os.path.exists(os.path.join(abs_sysroot, 'usr', 'bin', 'env')):
+        if not os.path.exists(args.tarball):
+            logger.error(
+                'A sysroot tarball (--tarball or %s) is required)',
+                args.tarball,
+            )
+            return 1
+
+        os.makedirs(abs_sysroot, exist_ok=True)
+        subprocess.check_call([
+            'tar',
+            '-xvf',
+            args.tarball,
+            '--exclude=./dev/*',
+            '--exclude=dev/*',
+            '-C', abs_sysroot,
+        ])
+
+    os.makedirs(os.path.join(abs_sysroot, 'tmp'), exist_ok=True)
+    os.makedirs(os.path.join(abs_sysroot, 'home'), exist_ok=True)
+    os.makedirs(
+        os.path.join(
+            abs_sysroot,
+            'var', 'cache', 'apt', 'archives', 'partial',
+        ), exist_ok=True,
+    )
+    os.makedirs(
+        os.path.join(abs_sysroot, './' + abs_srcdir), exist_ok=True)
+    os.makedirs(
+        os.path.join(abs_sysroot, './' + abs_builddir), exist_ok=True)
+
+    os.execvp(
+        'bwrap',
+        [
+            'bwrap',
+            '--ro-bind', abs_sysroot, '/',
+            '--dev-bind', '/dev', '/dev',
+            '--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
+            '--proc', '/proc',
+            '--tmpfs', '/tmp',
+            '--tmpfs', '/var/tmp',
+            '--tmpfs', '/home',
+            '--bind', abs_srcdir, abs_srcdir,
+            '--bind', abs_builddir, abs_builddir,
+            '--chdir', os.getcwd(),
+            '--setenv', 'LC_ALL', 'C.UTF-8',
+            args.command,
+        ] + args.args,
+    )
+
+
+if __name__ == '__main__':
+    sys.exit(main())
+
+# vim:set sw=4 sts=4 et:
diff --git a/t/mypy.sh b/t/mypy.sh
index 64238232c5d8d8bbc7354defa93b4f059ae2d32e..3d0d9c44d2b38687bf2e415aa9a7596e27cec1bb 100755
--- a/t/mypy.sh
+++ b/t/mypy.sh
@@ -8,11 +8,20 @@
 set -e
 set -u
 
-export MYPYPATH="${PYTHONPATH:=$(pwd)}"
+if [ -z "${G_TEST_SRCDIR-}" ]; then
+    me="$(readlink -f "$0")"
+    srcdir="${me%/*}"
+    G_TEST_SRCDIR="${srcdir%/*}"
+fi
+
+cd "$G_TEST_SRCDIR"
+
+export MYPYPATH="${PYTHONPATH:="$(pwd)"}"
 
 i=0
 for script in \
-    ./*.py \
+    "${G_TEST_SRCDIR}"/*.py \
+    "${G_TEST_SRCDIR}"/sysroot/*.py \
 ; do
     i=$((i + 1))
     if [ "x${MYPY:="$(command -v mypy || echo false)"}" = xfalse ]; then
diff --git a/t/pycodestyle.sh b/t/pycodestyle.sh
index 04209a490165d2ea7db1de7a7e76bfbda5a5c3b4..f3481eddba5445dfec04b0fe66f3bf79dcee2386 100755
--- a/t/pycodestyle.sh
+++ b/t/pycodestyle.sh
@@ -8,6 +8,14 @@
 set -e
 set -u
 
+if [ -z "${G_TEST_SRCDIR-}" ]; then
+    me="$(readlink -f "$0")"
+    srcdir="${me%/*}"
+    G_TEST_SRCDIR="${srcdir%/*}"
+fi
+
+cd "$G_TEST_SRCDIR"
+
 if [ "x${PYCODESTYLE:=pycodestyle}" = xfalse ] || \
         [ -z "$(command -v "$PYCODESTYLE")" ]; then
     echo "1..0 # SKIP pycodestyle not found"
@@ -17,7 +25,8 @@ fi
 echo "1..1"
 
 if "${PYCODESTYLE}" \
-    ./*.py \
+    "$G_TEST_SRCDIR"/*.py \
+    "${G_TEST_SRCDIR}"/sysroot/*.py \
     >&2; then
     echo "ok 1 - $PYCODESTYLE reported no issues"
 else
diff --git a/t/pyflakes.sh b/t/pyflakes.sh
index f90936a6ab89eb5db7a0860de7d22853423b91ef..db2c87e91f7c938e40434f2f4707a156acbbcce8 100755
--- a/t/pyflakes.sh
+++ b/t/pyflakes.sh
@@ -8,11 +8,20 @@
 set -e
 set -u
 
+if [ -z "${G_TEST_SRCDIR-}" ]; then
+    me="$(readlink -f "$0")"
+    srcdir="${me%/*}"
+    G_TEST_SRCDIR="${srcdir%/*}"
+fi
+
+cd "$G_TEST_SRCDIR"
+
 if [ "x${PYFLAKES:=pyflakes3}" = xfalse ] || \
         [ -z "$(command -v "$PYFLAKES")" ]; then
     echo "1..0 # SKIP pyflakes3 not found"
 elif "${PYFLAKES}" \
-    ./*.py \
+    "${G_TEST_SRCDIR}"/*.py \
+    "${G_TEST_SRCDIR}"/sysroot/*.py \
     >&2; then
     echo "1..1"
     echo "ok 1 - $PYFLAKES reported no issues"
diff --git a/t/relocatable-install.sh b/t/relocatable-install.sh
index c7fcba6c19a062e476ea042a2d73005af57635c2..9d9c1f571a76a06ade87ee7ee446cd11e4715cb0 100755
--- a/t/relocatable-install.sh
+++ b/t/relocatable-install.sh
@@ -1,6 +1,14 @@
 #!/bin/sh
 set -eu
 
+if [ -z "${G_TEST_SRCDIR-}" ]; then
+    me="$(readlink -f "$0")"
+    srcdir="${me%/*}"
+    G_TEST_SRCDIR="${srcdir%/*}"
+fi
+
+: "${G_TEST_BUILDDIR:=.}"
+
 n=0
 failed=
 
@@ -18,15 +26,17 @@ pressure-vessel-wrap
 pressure-vessel-unruntime
 "
 
-if ! [ -e relocatable-install ]; then
-    echo "1..0 # SKIP relocatable-install not built"
+relocatable_install="${G_TEST_BUILDDIR}/relocatable-install"
+
+if ! [ -e "${relocatable_install}" ]; then
+    echo "1..0 # SKIP relocatable-install not in expected location"
     exit 0
 fi
 
 for exe in $EXES; do
     n=$(( n + 1 ))
 
-    if "relocatable-install/bin/$exe" --help >&2; then
+    if "${relocatable_install}/bin/$exe" --help >&2; then
         echo "ok $n - $exe --help"
     else
         echo "not ok $n - $exe --help"
@@ -45,7 +55,7 @@ for basename in $MULTIARCH; do
 
         n=$(( n + 1 ))
 
-        if [ -x "relocatable-install/bin/$exe" ]; then
+        if [ -x "${relocatable_install}/bin/$exe" ]; then
             echo "ok $n - $exe exists and is executable"
         else
             echo "not ok $n - $exe not executable"
@@ -58,7 +68,7 @@ for basename in $MULTIARCH; do
             echo "ok $n - $exe # SKIP: $ld_so not found"
         elif [ "$basename" = "capsule-symbols" ]; then
             echo "ok $n - $exe # SKIP: capsule-symbols has no --help yet"
-        elif "relocatable-install/bin/$exe" --help >&2; then
+        elif "${relocatable_install}/bin/$exe" --help >&2; then
             echo "ok $n - $exe --help"
         else
             echo "not ok $n - $exe --help"
@@ -72,7 +82,7 @@ for exe in $SCRIPTS; do
 
     if ! [ -x /bin/bash ]; then
         echo "ok $n - $exe # SKIP Cannot run a bash script without bash"
-    elif "relocatable-install/bin/$exe" --help >&2; then
+    elif "${relocatable_install}/bin/$exe" --help >&2; then
         echo "ok $n - $exe --help"
     else
         echo "not ok $n - $exe --help"
diff --git a/t/shellcheck.sh b/t/shellcheck.sh
index 663b73dd5335dd4e8ee7f6bd783a404b81b27961..f5e72ef97fbf8f6e5f1a208893696a03950bf2ae 100755
--- a/t/shellcheck.sh
+++ b/t/shellcheck.sh
@@ -26,16 +26,18 @@ if ! command -v shellcheck >/dev/null 2>&1; then
     exit 0
 fi
 
-if [ -z "${TOP_SRCDIR-}" ]; then
+if [ -z "${G_TEST_SRCDIR-}" ]; then
     me="$(readlink -f "$0")"
     srcdir="${me%/*}"
-    TOP_SRCDIR="${srcdir%/*}"
+    G_TEST_SRCDIR="${srcdir%/*}"
 fi
 
-cd "$TOP_SRCDIR"
+cd "$G_TEST_SRCDIR"
 
 n=0
 for shell_script in \
+        *.sh \
+        sysroot/*.sh \
         t/*.sh \
         ; do
     n=$((n + 1))