diff --git a/.gitignore b/.gitignore
index f8bf5718..ddcbadc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,48 +1,8 @@
-/.pls_cache
-*.o
 *~
-Makefile
-Makefile.in
-.deps
-.hydra-data
-/config.guess
-/config.log
-/config.status
-/config.sub
-/configure
-/depcomp
-/libtool
-/ltmain.sh
-/autom4te.cache
-/aclocal.m4
-/missing
-/install-sh
+.test_info.*
 /src/sql/hydra-postgresql.sql
 /src/sql/hydra-sqlite.sql
 /src/sql/tmp.sqlite
-/src/hydra-eval-jobs/hydra-eval-jobs
-/src/root/static/bootstrap
-/src/root/static/js/flot
-/tests
-/doc/manual/images
-/doc/manual/manual.html
-/doc/manual/manual.pdf
-/t/.bzr*
-/t/.git*
-/t/.hg*
-/t/nix
-/t/data
-/t/jobs/config.nix
-t/jobs/declarative/project.json
-/inst
-hydra-config.h
-hydra-config.h.in
 result
 result-*
 outputs
-config
-stamp-h1
-src/hydra-evaluator/hydra-evaluator
-src/hydra-queue-runner/hydra-queue-runner
-src/root/static/fontawesome/
-src/root/static/bootstrap*/
diff --git a/.yath.rc b/.yath.rc
deleted file mode 100644
index 19bb35af..00000000
--- a/.yath.rc
+++ /dev/null
@@ -1,2 +0,0 @@
-[test]
--I=rel(t/lib)
diff --git a/Makefile.am b/Makefile.am
deleted file mode 100644
index a28e3f33..00000000
--- a/Makefile.am
+++ /dev/null
@@ -1,12 +0,0 @@
-SUBDIRS = src doc
-if CAN_DO_CHECK
-  SUBDIRS += t
-endif
-
-BOOTCLEAN_SUBDIRS = $(SUBDIRS)
-DIST_SUBDIRS      = $(SUBDIRS)
-EXTRA_DIST        = nixos-modules/hydra.nix
-
-install-data-local: nixos-modules/hydra.nix
-	$(INSTALL) -d $(DESTDIR)$(datadir)/nix
-	$(INSTALL_DATA) nixos-modules/hydra.nix $(DESTDIR)$(datadir)/nix/hydra-module.nix
diff --git a/configure.ac b/configure.ac
deleted file mode 100644
index e5c57d14..00000000
--- a/configure.ac
+++ /dev/null
@@ -1,91 +0,0 @@
-AC_INIT([Hydra], [m4_esyscmd([echo -n $(cat ./version.txt)$VERSION_SUFFIX])])
-AC_CONFIG_AUX_DIR(config)
-AM_INIT_AUTOMAKE([foreign serial-tests])
-
-AC_LANG([C++])
-
-AC_PROG_CC
-AC_PROG_INSTALL
-AC_PROG_LN_S
-AC_PROG_LIBTOOL
-AC_PROG_CXX
-
-AC_PATH_PROG([XSLTPROC], [xsltproc])
-
-AC_ARG_WITH([docbook-xsl],
-  [AS_HELP_STRING([--with-docbook-xsl=PATH],
-    [path of the DocBook XSL stylesheets])],
-  [docbookxsl="$withval"],
-  [docbookxsl="/docbook-xsl-missing"])
-AC_SUBST([docbookxsl])
-
-
-AC_DEFUN([NEED_PROG],
-[
-AC_PATH_PROG($1, $2)
-if test -z "$$1"; then
-    AC_MSG_ERROR([$2 is required])
-fi
-])
-
-NEED_PROG(perl, perl)
-
-NEED_PROG([NIX_STORE_PROGRAM], [nix-store])
-
-AC_MSG_CHECKING([whether $NIX_STORE_PROGRAM is recent enough])
-if test -n "$NIX_STORE" -a -n "$TMPDIR"
-then
-    # This may be executed from within a build chroot, so pacify
-    # `nix-store' instead of letting it choke while trying to mkdir
-    # /nix/var.
-    NIX_STATE_DIR="$TMPDIR"
-    export NIX_STATE_DIR
-fi
-if NIX_REMOTE=daemon PAGER=cat "$NIX_STORE_PROGRAM" --timeout 123 -q; then
-    AC_MSG_RESULT([yes])
-else
-    AC_MSG_RESULT([no])
-    AC_MSG_ERROR([`$NIX_STORE_PROGRAM' doesn't support `--timeout'; please use a newer version.])
-fi
-
-PKG_CHECK_MODULES([NIX], [nix-main nix-expr nix-store])
-
-testPath="$(dirname $(type -p expr))"
-AC_SUBST(testPath)
-
-CXXFLAGS+=" -include nix/config.h"
-
-AC_CONFIG_FILES([
-  Makefile
-  doc/Makefile
-  doc/manual/Makefile
-  src/Makefile
-  src/hydra-evaluator/Makefile
-  src/hydra-eval-jobs/Makefile
-  src/hydra-queue-runner/Makefile
-  src/sql/Makefile
-  src/ttf/Makefile
-  src/lib/Makefile
-  src/root/Makefile
-  src/script/Makefile
-])
-
-# Tests might be filtered out
-AM_CONDITIONAL([CAN_DO_CHECK], [test -f "$srcdir/t/api-test.t"])
-AM_COND_IF(
-  [CAN_DO_CHECK],
-  [
-    jobsPath="$(realpath ./t/jobs)"
-    AC_SUBST(jobsPath)
-    AC_CONFIG_FILES([
-      t/Makefile
-      t/jobs/config.nix
-      t/jobs/declarative/project.json
-    ])
-  ])
-
-AC_CONFIG_COMMANDS([executable-scripts], [])
-
-AC_CONFIG_HEADER([hydra-config.h])
-
-AC_OUTPUT
diff --git a/doc/Makefile.am b/doc/Makefile.am
deleted file mode 100644
index 9ac91d24..00000000
--- a/doc/Makefile.am
+++ /dev/null
@@ -1,4 +0,0 @@
-SUBDIRS = manual
-BOOTCLEAN_SUBDIRS = $(SUBDIRS)
-DIST_SUBDIRS      = $(SUBDIRS)
-
diff --git a/doc/manual/Makefile.am b/doc/manual/Makefile.am
deleted file mode 100644
index ec732166..00000000
--- a/doc/manual/Makefile.am
+++ /dev/null
@@ -1,6 +0,0 @@
-MD_FILES = src/*.md
-
-EXTRA_DIST = $(MD_FILES)
-
-install: $(MD_FILES)
-	mdbook build . -d $(docdir)
diff --git a/doc/manual/meson.build b/doc/manual/meson.build
new file mode 100644
index 00000000..11178809
--- /dev/null
+++ b/doc/manual/meson.build
@@ -0,0 +1,36 @@
+srcs = files(
+  'src/SUMMARY.md',
+  'src/about.md',
+  'src/api.md',
+  'src/configuration.md',
+  'src/hacking.md',
+  'src/installation.md',
+  'src/introduction.md',
+  'src/jobs.md',
+  'src/monitoring/README.md',
+  'src/notifications.md',
+  'src/plugins/README.md',
+  'src/plugins/RunCommand.md',
+  'src/plugins/declarative-projects.md',
+  'src/projects.md',
+  'src/webhooks.md',
+)
+
+manual = custom_target(
+  'manual',
+  command: [
+    mdbook,
+    'build',
+    '@SOURCE_ROOT@/doc/manual',
+    '-d', meson.current_build_dir() / 'html'
+  ],
+  depend_files: srcs,
+  output: ['html'],
+  build_by_default: true,
+)
+
+install_subdir(
+  manual.full_path(),
+  install_dir: get_option('datadir') / 'doc/hydra',
+  strip_directory: true,
+)
diff --git a/flake.nix b/flake.nix
index c06b5f62..fccd45b9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -47,7 +47,7 @@
           pkgs.runCommand "hydra-manual-${hydra.version}" { }
             ''
               mkdir -p $out/share
-              cp -prvd ${hydra}/share/doc $out/share/
+              cp -prvd ${hydra.doc}/share/doc $out/share/
 
               mkdir $out/nix-support
               echo "doc manual $out/share/doc/hydra" >> $out/nix-support/hydra-build-products
diff --git a/meson.build b/meson.build
new file mode 100644
index 00000000..b9327d51
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,40 @@
+project('hydra', 'cpp',
+  version: files('version.txt'),
+  license: 'GPL-3.0',
+  default_options: [
+    'debug=true',
+    'optimization=2',
+    'cpp_std=c++20',
+  ],
+)
+
+nix_store_dep = dependency('nix-store', required: true)
+nix_main_dep = dependency('nix-main', required: true)
+nix_expr_dep = dependency('nix-expr', required: true)
+nix_flake_dep = dependency('nix-flake', required: true)
+nix_cmd_dep = dependency('nix-cmd', required: true)
+
+# Nix need extra flags not provided in its pkg-config files.
+nix_dep = declare_dependency(
+  dependencies: [
+    nix_store_dep,
+    nix_main_dep,
+    nix_expr_dep,
+    nix_flake_dep,
+    nix_cmd_dep,
+  ],
+  compile_args: ['-include', 'nix/config.h'],
+)
+
+pqxx_dep = dependency('libpqxx', required: true)
+
+prom_cpp_core_dep = dependency('prometheus-cpp-core', required: true)
+prom_cpp_pull_dep = dependency('prometheus-cpp-pull', required: true)
+
+mdbook = find_program('mdbook', native: true)
+perl = find_program('perl', native: true)
+
+subdir('doc/manual')
+subdir('nixos-modules')
+subdir('src')
+subdir('t')
diff --git a/nixos-modules/meson.build b/nixos-modules/meson.build
new file mode 100644
index 00000000..95c47e9f
--- /dev/null
+++ b/nixos-modules/meson.build
@@ -0,0 +1,4 @@
+install_data('hydra.nix',
+  install_dir: get_option('datadir') / 'nix',
+  rename: ['hydra-module.nix'],
+)
diff --git a/package.nix b/package.nix
index 73dd0f2a..f944fe2b 100644
--- a/package.nix
+++ b/package.nix
@@ -13,7 +13,8 @@
 , git
 
 , makeWrapper
-, autoreconfHook
+, meson
+, ninja
 , nukeReferences
 , pkg-config
 , mdbook
@@ -91,6 +92,7 @@ let
         DigestSHA1
         EmailMIME
         EmailSender
+        FileCopyRecursive
         FileLibMagic
         FileSlurper
         FileWhich
@@ -138,28 +140,24 @@ stdenv.mkDerivation (finalAttrs: {
   src = fileset.toSource {
     root = ./.;
     fileset = fileset.unions ([
-      ./version.txt
-      ./configure.ac
-      ./Makefile.am
-      ./src
       ./doc
-      ./nixos-modules/hydra.nix
-      # These are always needed to appease Automake
-      ./t/Makefile.am
-      ./t/jobs/config.nix.in
-      ./t/jobs/declarative/project.json.in
-    ] ++ lib.optionals finalAttrs.doCheck [
+      ./meson.build
+      ./nixos-modules
+      ./src
       ./t
+      ./version.txt
       ./.perlcriticrc
-      ./.yath.rc
     ]);
   };
 
+  outputs = [ "out" "doc" ];
+
   strictDeps = true;
 
   nativeBuildInputs = [
     makeWrapper
-    autoreconfHook
+    meson
+    ninja
     nukeReferences
     pkg-config
     mdbook
@@ -225,6 +223,12 @@ stdenv.mkDerivation (finalAttrs: {
 
   OPENLDAP_ROOT = openldap;
 
+  mesonBuildType = "release";
+
+  postPatch = ''
+    patchShebangs .
+  '';
+
   shellHook = ''
     pushd $(git rev-parse --show-toplevel) >/dev/null
 
@@ -238,14 +242,11 @@ stdenv.mkDerivation (finalAttrs: {
     popd >/dev/null
   '';
 
-  NIX_LDFLAGS = [ "-lpthread" ];
-
-  enableParallelBuilding = true;
-
   doCheck = true;
 
+  mesonCheckFlags = [ "--verbose" ];
+
   preCheck = ''
-    patchShebangs .
     export LOGNAME=''${LOGNAME:-foo}
     # set $HOME for bzr so it can create its trace file
     export HOME=$(mktemp -d)
diff --git a/src/Makefile.am b/src/Makefile.am
deleted file mode 100644
index a28780b6..00000000
--- a/src/Makefile.am
+++ /dev/null
@@ -1,3 +0,0 @@
-SUBDIRS = hydra-evaluator hydra-eval-jobs hydra-queue-runner sql script lib root ttf
-BOOTCLEAN_SUBDIRS = $(SUBDIRS)
-DIST_SUBDIRS      = $(SUBDIRS)
diff --git a/src/hydra-eval-jobs/Makefile.am b/src/hydra-eval-jobs/Makefile.am
deleted file mode 100644
index fb67f89b..00000000
--- a/src/hydra-eval-jobs/Makefile.am
+++ /dev/null
@@ -1,5 +0,0 @@
-bin_PROGRAMS = hydra-eval-jobs
-
-hydra_eval_jobs_SOURCES = hydra-eval-jobs.cc
-hydra_eval_jobs_LDADD = $(NIX_LIBS) -lnixcmd -lnixflake
-hydra_eval_jobs_CXXFLAGS = $(NIX_CFLAGS) -I ../libhydra
diff --git a/src/hydra-eval-jobs/meson.build b/src/hydra-eval-jobs/meson.build
new file mode 100644
index 00000000..916212e1
--- /dev/null
+++ b/src/hydra-eval-jobs/meson.build
@@ -0,0 +1,8 @@
+hydra_eval_jobs = executable('hydra-eval-jobs',
+  'hydra-eval-jobs.cc',
+  dependencies: [
+    libhydra_dep,
+    nix_dep,
+  ],
+  install: true,
+)
diff --git a/src/hydra-evaluator/Makefile.am b/src/hydra-evaluator/Makefile.am
deleted file mode 100644
index 73638cfe..00000000
--- a/src/hydra-evaluator/Makefile.am
+++ /dev/null
@@ -1,5 +0,0 @@
-bin_PROGRAMS = hydra-evaluator
-
-hydra_evaluator_SOURCES = hydra-evaluator.cc
-hydra_evaluator_LDADD = $(NIX_LIBS) -lpqxx
-hydra_evaluator_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
diff --git a/src/hydra-evaluator/meson.build b/src/hydra-evaluator/meson.build
new file mode 100644
index 00000000..b3dc542e
--- /dev/null
+++ b/src/hydra-evaluator/meson.build
@@ -0,0 +1,9 @@
+hydra_evaluator = executable('hydra-evaluator',
+  'hydra-evaluator.cc',
+  dependencies: [
+    libhydra_dep,
+    nix_dep,
+    pqxx_dep,
+  ],
+  install: true,
+)
diff --git a/src/hydra-queue-runner/Makefile.am b/src/hydra-queue-runner/Makefile.am
deleted file mode 100644
index 117112f6..00000000
--- a/src/hydra-queue-runner/Makefile.am
+++ /dev/null
@@ -1,8 +0,0 @@
-bin_PROGRAMS = hydra-queue-runner
-
-hydra_queue_runner_SOURCES = hydra-queue-runner.cc queue-monitor.cc dispatcher.cc \
- builder.cc build-result.cc build-remote.cc \
- hydra-build-result.hh counter.hh state.hh db.hh \
- nar-extractor.cc nar-extractor.hh
-hydra_queue_runner_LDADD = $(NIX_LIBS) -lpqxx -lprometheus-cpp-pull -lprometheus-cpp-core
-hydra_queue_runner_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
diff --git a/src/hydra-queue-runner/meson.build b/src/hydra-queue-runner/meson.build
new file mode 100644
index 00000000..1c20299f
--- /dev/null
+++ b/src/hydra-queue-runner/meson.build
@@ -0,0 +1,22 @@
+srcs = files(
+  'builder.cc',
+  'build-remote.cc',
+  'build-result.cc',
+  'dispatcher.cc',
+  'hydra-queue-runner.cc',
+  'nar-extractor.cc',
+  'queue-monitor.cc',
+)
+
+hydra_queue_runner = executable('hydra-queue-runner',
+  'hydra-queue-runner.cc',
+  srcs,
+  dependencies: [
+    libhydra_dep,
+    nix_dep,
+    pqxx_dep,
+    prom_cpp_core_dep,
+    prom_cpp_pull_dep,
+  ],
+  install: true,
+)
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
deleted file mode 100644
index 434868e0..00000000
--- a/src/lib/Makefile.am
+++ /dev/null
@@ -1,22 +0,0 @@
-PERL_MODULES =					\
-  $(wildcard *.pm)				\
-  $(wildcard Hydra/*.pm)			\
-  $(wildcard Hydra/Helper/*.pm)			\
-  $(wildcard Hydra/Model/*.pm)			\
-  $(wildcard Hydra/View/*.pm)			\
-  $(wildcard Hydra/Schema/*.pm)			\
-  $(wildcard Hydra/Schema/Result/*.pm)			\
-  $(wildcard Hydra/Schema/ResultSet/*.pm)			\
-  $(wildcard Hydra/Controller/*.pm)		\
-  $(wildcard Hydra/Base/*.pm)			\
-  $(wildcard Hydra/Base/Controller/*.pm) 	\
-  $(wildcard Hydra/Script/*.pm)			\
-  $(wildcard Hydra/Component/*.pm)		\
-  $(wildcard Hydra/Event/*.pm)		\
-  $(wildcard Hydra/Plugin/*.pm)
-
-EXTRA_DIST = \
-  $(PERL_MODULES)
-
-hydradir = $(libexecdir)/hydra/lib
-nobase_hydra_DATA = $(PERL_MODULES)
diff --git a/src/libhydra/meson.build b/src/libhydra/meson.build
new file mode 100644
index 00000000..1866233c
--- /dev/null
+++ b/src/libhydra/meson.build
@@ -0,0 +1,5 @@
+libhydra_inc = include_directories('.')
+
+libhydra_dep = declare_dependency(
+  include_directories: [libhydra_inc],
+)
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 00000000..8c7562ed
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,86 @@
+# Native code
+subdir('libhydra')
+subdir('hydra-eval-jobs')
+subdir('hydra-evaluator')
+subdir('hydra-queue-runner')
+
+hydra_libexecdir = get_option('libexecdir') / 'hydra'
+
+# Data and interpreted
+foreach dir : ['lib', 'root']
+  install_subdir(dir,
+    install_dir: hydra_libexecdir,
+  )
+endforeach
+subdir('sql')
+subdir('ttf')
+
+# Static files for website
+
+hydra_libexecdir_static = hydra_libexecdir / 'root' / 'static'
+
+## Bootstrap
+
+bootstrap_name = 'bootstrap-4.3.1-dist'
+bootstrap = custom_target(
+  'extract-bootstrap',
+  input: 'root' / (bootstrap_name + '.zip'),
+  output: bootstrap_name,
+  command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
+)
+custom_target(
+  'name-bootstrap',
+  input: bootstrap,
+  output: 'bootstrap',
+  command: ['cp', '-r', '@INPUT@' , '@OUTPUT@'],
+  install: true,
+  install_dir: hydra_libexecdir_static,
+)
+
+## Flot
+
+custom_target(
+  'extract-flot',
+  input: 'root' / 'flot-0.8.3.zip',
+  output: 'flot',
+  command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
+  install: true,
+  install_dir: hydra_libexecdir_static / 'js',
+)
+
+## Fontawesome
+
+fontawesome_name = 'fontawesome-free-5.10.2-web'
+fontawesome = custom_target(
+  'extract-fontawesome',
+  input: 'root' / (fontawesome_name + '.zip'),
+  output: fontawesome_name,
+  command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
+)
+custom_target(
+  'name-fontawesome-css',
+  input: fontawesome,
+  output: 'css',
+  command: ['cp', '-r', '@INPUT@/css', '@OUTPUT@'],
+  install: true,
+  install_dir: hydra_libexecdir_static / 'fontawesome',
+)
+custom_target(
+  'name-fontawesome-webfonts',
+  input: fontawesome,
+  output: 'webfonts',
+  command: ['cp', '-r', '@INPUT@/webfonts', '@OUTPUT@'],
+  install: true,
+  install_dir: hydra_libexecdir_static / 'fontawesome',
+)
+
+# Scripts
+
+install_subdir('script',
+  install_dir: get_option('bindir'),
+  exclude_files: [
+    'hydra-dev-server',
+  ],
+  install_mode: 'rwxr-xr-x',
+  strip_directory: true,
+)
diff --git a/src/root/Makefile.am b/src/root/Makefile.am
deleted file mode 100644
index 163b96e0..00000000
--- a/src/root/Makefile.am
+++ /dev/null
@@ -1,39 +0,0 @@
-TEMPLATES = $(wildcard *.tt)
-STATIC = \
-  $(wildcard static/images/*) \
-  $(wildcard static/css/*) \
-  static/js/bootbox.min.js \
-  static/js/popper.min.js \
-  static/js/common.js \
-  static/js/jquery/jquery-3.4.1.min.js \
-  static/js/jquery/jquery-ui-1.10.4.min.js
-
-FLOT = flot-0.8.3.zip
-BOOTSTRAP = bootstrap-4.3.1-dist.zip
-FONTAWESOME = fontawesome-free-5.10.2-web.zip
-
-ZIPS = $(FLOT) $(BOOTSTRAP) $(FONTAWESOME)
-
-EXTRA_DIST = $(TEMPLATES) $(STATIC) $(ZIPS)
-
-hydradir = $(libexecdir)/hydra/root
-nobase_hydra_DATA = $(EXTRA_DIST)
-
-all:
-	mkdir -p $(srcdir)/static/js
-	unzip -u -d $(srcdir)/static $(BOOTSTRAP)
-	rm -rf $(srcdir)/static/bootstrap
-	mv $(srcdir)/static/$(basename $(BOOTSTRAP)) $(srcdir)/static/bootstrap
-	unzip -u -d $(srcdir)/static/js $(FLOT)
-	unzip -u -d $(srcdir)/static $(FONTAWESOME)
-	rm -rf $(srcdir)/static/fontawesome
-	mv $(srcdir)/static/$(basename $(FONTAWESOME)) $(srcdir)/static/fontawesome
-
-install-data-local: $(ZIPS)
-	mkdir -p $(hydradir)/static/js
-	cp -prvd $(srcdir)/static/js/* $(hydradir)/static/js
-	mkdir -p $(hydradir)/static/bootstrap
-	cp -prvd $(srcdir)/static/bootstrap/* $(hydradir)/static/bootstrap
-	mkdir -p $(hydradir)/static/fontawesome/{css,webfonts}
-	cp -prvd $(srcdir)/static/fontawesome/css/* $(hydradir)/static/fontawesome/css
-	cp -prvd $(srcdir)/static/fontawesome/webfonts/* $(hydradir)/static/fontawesome/webfonts
diff --git a/src/script/Makefile.am b/src/script/Makefile.am
deleted file mode 100644
index 466d3153..00000000
--- a/src/script/Makefile.am
+++ /dev/null
@@ -1,19 +0,0 @@
-EXTRA_DIST =					\
-  $(distributable_scripts)
-
-distributable_scripts =				\
-  hydra-backfill-ids				\
-  hydra-init					\
-  hydra-eval-jobset				\
-  hydra-server					\
-  hydra-update-gc-roots				\
-  hydra-s3-backup-collect-garbage		\
-  hydra-create-user				\
-  hydra-notify					\
-  hydra-send-stats				\
-  nix-prefetch-git				\
-  nix-prefetch-bzr				\
-  nix-prefetch-hg
-
-bin_SCRIPTS =					\
-  $(distributable_scripts)
diff --git a/src/sql/Makefile.am b/src/sql/Makefile.am
deleted file mode 100644
index a6b96886..00000000
--- a/src/sql/Makefile.am
+++ /dev/null
@@ -1,9 +0,0 @@
-sqldir = $(libexecdir)/hydra/sql
-nobase_dist_sql_DATA =				\
-  hydra.sql					\
-  test.sql					\
-  upgrade-*.sql					\
-  update-dbix.pl
-
-update-dbix: hydra.sql
-	./update-dbix-harness.sh
diff --git a/src/sql/meson.build b/src/sql/meson.build
new file mode 100644
index 00000000..2da35b3c
--- /dev/null
+++ b/src/sql/meson.build
@@ -0,0 +1,90 @@
+sql_files = files(
+  'hydra.sql',
+  'test.sql',
+  'update-dbix.pl',
+  'upgrade-2.sql',
+  'upgrade-3.sql',
+  'upgrade-4.sql',
+  'upgrade-5.sql',
+  'upgrade-6.sql',
+  'upgrade-7.sql',
+  'upgrade-8.sql',
+  'upgrade-9.sql',
+  'upgrade-10.sql',
+  'upgrade-11.sql',
+  'upgrade-12.sql',
+  'upgrade-13.sql',
+  'upgrade-14.sql',
+  'upgrade-15.sql',
+  'upgrade-16.sql',
+  'upgrade-17.sql',
+  'upgrade-18.sql',
+  'upgrade-19.sql',
+  'upgrade-20.sql',
+  'upgrade-21.sql',
+  'upgrade-22.sql',
+  'upgrade-23.sql',
+  'upgrade-24.sql',
+  'upgrade-25.sql',
+  'upgrade-26.sql',
+  'upgrade-27.sql',
+  'upgrade-28.sql',
+  'upgrade-29.sql',
+  'upgrade-30.sql',
+  'upgrade-31.sql',
+  'upgrade-32.sql',
+  'upgrade-33.sql',
+  'upgrade-34.sql',
+  'upgrade-35.sql',
+  'upgrade-36.sql',
+  'upgrade-37.sql',
+  'upgrade-38.sql',
+  'upgrade-39.sql',
+  'upgrade-40.sql',
+  'upgrade-41.sql',
+  'upgrade-42.sql',
+  'upgrade-43.sql',
+  'upgrade-44.sql',
+  'upgrade-45.sql',
+  'upgrade-46.sql',
+  'upgrade-47.sql',
+  'upgrade-48.sql',
+  'upgrade-49.sql',
+  'upgrade-50.sql',
+  'upgrade-51.sql',
+  'upgrade-52.sql',
+  'upgrade-53.sql',
+  'upgrade-54.sql',
+  'upgrade-55.sql',
+  'upgrade-56.sql',
+  'upgrade-57.sql',
+  'upgrade-58.sql',
+  'upgrade-59.sql',
+  'upgrade-60.sql',
+  'upgrade-61.sql',
+  'upgrade-62.sql',
+  'upgrade-63.sql',
+  'upgrade-64.sql',
+  'upgrade-65.sql',
+  'upgrade-66.sql',
+  'upgrade-67.sql',
+  'upgrade-68.sql',
+  'upgrade-69.sql',
+  'upgrade-70.sql',
+  'upgrade-71.sql',
+  'upgrade-72.sql',
+  'upgrade-73.sql',
+  'upgrade-74.sql',
+  'upgrade-75.sql',
+  'upgrade-76.sql',
+  'upgrade-77.sql',
+  'upgrade-78.sql',
+  'upgrade-79.sql',
+  'upgrade-80.sql',
+  'upgrade-81.sql',
+  'upgrade-82.sql',
+  'upgrade-83.sql',
+  'upgrade-84.sql',
+)
+
+install_data(sql_files, install_dir: hydra_libexecdir / 'sql')
diff --git a/src/ttf/Makefile.am b/src/ttf/Makefile.am
deleted file mode 100644
index eba78239..00000000
--- a/src/ttf/Makefile.am
+++ /dev/null
@@ -1,4 +0,0 @@
-EXTRA_DIST = COPYING.LIB StayPuft.ttf
-
-ttfdir = $(libexecdir)/hydra/ttf
-nobase_ttf_DATA = $(EXTRA_DIST)
diff --git a/src/ttf/meson.build b/src/ttf/meson.build
new file mode 100644
index 00000000..260439e5
--- /dev/null
+++ b/src/ttf/meson.build
@@ -0,0 +1,5 @@
+data_files = files(
+  'StayPuft.ttf',
+  'COPYING.LIB',
+)
+install_data(data_files, install_dir: hydra_libexecdir / 'ttf')
diff --git a/t/Makefile.am b/t/Makefile.am
deleted file mode 100644
index 9c14c1e3..00000000
--- a/t/Makefile.am
+++ /dev/null
@@ -1,39 +0,0 @@
-TESTS_ENVIRONMENT = \
-  BZR_HOME="$(abs_builddir)/data"			\
-  HYDRA_DBI="dbi:Pg:dbname=hydra-test-suite;port=6433"	\
-  HYDRA_DATA="$(abs_builddir)/data"			\
-  HYDRA_HOME="$(top_srcdir)/src"			\
-  HYDRA_CONFIG=						\
-  NIX_REMOTE=						\
-  NIX_REMOTE_SYSTEMS=					\
-  NIX_CONF_DIR="$(abs_builddir)/nix/etc/nix"		\
-  NIX_STATE_DIR="$(abs_builddir)/nix/var/nix"		\
-  NIX_STORE_DIR="$(abs_builddir)/nix/store"			\
-  NIX_LOG_DIR="$(abs_builddir)/nix/var/log/nix"		\
-  PGHOST=/tmp	\
-  PERL5LIB="$(srcdir):$(abs_top_srcdir)/src/lib:$$PERL5LIB"	\
-  PYTHONPATH= \
-  PATH=$(abs_top_srcdir)/src/hydra-evaluator:$(abs_top_srcdir)/src/script:$(abs_top_srcdir)/src/hydra-eval-jobs:$(abs_top_srcdir)/src/hydra-queue-runner:$$PATH \
-  perl -w
-
-EXTRA_DIST = \
-  $(wildcard *.pm) \
-  $(wildcard jobs/*.nix) \
-  $(wildcard jobs/*.sh) \
-  $(TESTS)
-
-TESTS = \
-  perlcritic.pl \
-  test.pl
-
-check_SCRIPTS = repos
-
-repos: dirs
-
-dirs:
-	mkdir -p data
-	touch data/hydra.conf
-	mkdir -p nix
-	mkdir -p nix/etc/nix
-	mkdir -p nix/store
-	mkdir -p nix/var
diff --git a/t/lib/HydraTestContext.pm b/t/lib/HydraTestContext.pm
index 1d6fa909..d1de2212 100644
--- a/t/lib/HydraTestContext.pm
+++ b/t/lib/HydraTestContext.pm
@@ -4,6 +4,8 @@ use warnings;
 package HydraTestContext;
 use File::Path qw(make_path);
 use File::Basename;
+use File::Copy::Recursive qw(rcopy);
+use File::Which qw(which);
 use Cwd qw(abs_path getcwd);
 use CliRunners;
 use Hydra::Helper::Exec;
@@ -77,6 +79,13 @@ sub new {
     );
     $ENV{'HYDRA_DBI'} = $pgsql->dsn;
 
+    my $jobsdir = "$dir/jobs";
+    rcopy(abs_path(dirname(__FILE__) . "/../jobs"), $jobsdir);
+
+    my $coreutils_path = dirname(which 'install');
+    replace_variable_in_file($jobsdir . "/config.nix", '@testPath@', $coreutils_path);
+    replace_variable_in_file($jobsdir . "/declarative/project.json", '@jobsPath@', $jobsdir);
+
     my $self = bless {
         _db => undef,
         db_handle => $pgsql,
@@ -84,7 +93,7 @@ sub new {
         nix_state_dir => $nix_state_dir,
         nix_log_dir => $nix_log_dir,
         testdir => abs_path(dirname(__FILE__) . "/.."),
-        jobsdir => abs_path(dirname(__FILE__) . "/../jobs"),
+        jobsdir => $jobsdir,
         deststoredir => $deststoredir,
     }, $class;
 
@@ -243,6 +252,18 @@ sub write_file {
     close $fh;
 }
 
+sub replace_variable_in_file {
+    my ($fn, $var, $val) = @_;
+
+    open (my $input, '<', "$fn.in") or die $!;
+    open (my $output, '>', $fn) or die $!;
+
+    while (my $line = <$input>) {
+        $line =~ s/$var/$val/g;
+        print $output $line;
+    }
+}
+
 sub rand_chars {
     return sprintf("t%08X", rand(0xFFFFFFFF));
 }
diff --git a/t/meson.build b/t/meson.build
new file mode 100644
index 00000000..11044a03
--- /dev/null
+++ b/t/meson.build
@@ -0,0 +1,43 @@
+fs = import('fs')
+
+test('perlcritic',
+  perl,
+  args: ['-w', files('perlcritic.pl')],
+  workdir: meson.project_source_root(),
+  timeout: -1,
+)
+
+testenv = environment(
+  {
+    'BZR_HOME': meson.current_build_dir() / 'data',
+    'HYDRA_DBI': 'dbi:Pg:dbname=hydra-test-suite;port=6433',
+    'HYDRA_DATA': meson.current_build_dir() / 'data',
+    'HYDRA_HOME': meson.project_source_root() / 'src',
+    'PGHOST': '/tmp',
+    'PYTHONPATH': '',
+
+    # libpqxx seems to randomly crash with certain values of MALLOC_PERTURB_,
+    # set by default by Meson's test(). Very promising, high quality software.
+    'MALLOC_PERTURB_': '0',
+  },
+)
+testenv.prepend('PERL5LIB',
+  meson.current_source_dir(),
+  meson.project_source_root() / 'src/lib',
+  separator: ':'
+)
+testenv.prepend('PATH',
+  fs.parent(hydra_eval_jobs.full_path()),
+  fs.parent(hydra_evaluator.full_path()),
+  fs.parent(hydra_queue_runner.full_path()),
+  meson.project_source_root() / 'src/script',
+  separator: ':'
+)
+
+test('testsuite',
+  perl,
+  args: ['-I', meson.current_source_dir() / 'lib', '-w', files('test.pl')],
+  env: testenv,
+  workdir: meson.current_source_dir(),
+  timeout: -1,
+)