diff --git a/meson.build b/meson.build
index 4ae11f0328e33c2ccafa4e3cca6040b7251989ff..3cf5dcd1e61987926ebb356545370c87d42971f5 100644
--- a/meson.build
+++ b/meson.build
@@ -224,6 +224,8 @@ executable(
     'src/flatpak-utils-base-private.h',
     'src/flatpak-utils.c',
     'src/flatpak-utils-private.h',
+    'src/runtime.c',
+    'src/runtime.h',
     'src/utils.c',
     'src/utils.h',
     'src/wrap.c',
diff --git a/src/runtime.c b/src/runtime.c
new file mode 100644
index 0000000000000000000000000000000000000000..6214b0c516fdc34432b442c5ae7f97586f708e64
--- /dev/null
+++ b/src/runtime.c
@@ -0,0 +1,314 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "src/runtime.h"
+
+#include <gio/gio.h>
+
+#include "bwrap.h"
+#include "bwrap-lock.h"
+
+/*
+ * PvRuntime:
+ *
+ * Object representing a runtime to be used as the /usr for a game.
+ */
+
+struct _PvRuntime
+{
+  GObject parent;
+
+  gchar *bubblewrap;
+  gchar *source_files;
+  gchar *tools_dir;
+  PvBwrapLock *runtime_lock;
+};
+
+struct _PvRuntimeClass
+{
+  GObjectClass parent;
+};
+
+enum {
+  PROP_0,
+  PROP_BUBBLEWRAP,
+  PROP_SOURCE_FILES,
+  PROP_TOOLS_DIRECTORY,
+  N_PROPERTIES
+};
+
+static GParamSpec *properties[N_PROPERTIES] = { NULL };
+
+static void pv_runtime_initable_iface_init (GInitableIface *iface,
+                                            gpointer unused);
+
+G_DEFINE_TYPE_WITH_CODE (PvRuntime, pv_runtime, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+                                                pv_runtime_initable_iface_init))
+
+static void
+pv_runtime_init (PvRuntime *self)
+{
+}
+
+static void
+pv_runtime_get_property (GObject *object,
+                         guint prop_id,
+                         GValue *value,
+                         GParamSpec *pspec)
+{
+  PvRuntime *self = PV_RUNTIME (object);
+
+  switch (prop_id)
+    {
+      case PROP_BUBBLEWRAP:
+        g_value_set_string (value, self->bubblewrap);
+        break;
+
+      case PROP_SOURCE_FILES:
+        g_value_set_string (value, self->source_files);
+        break;
+
+      case PROP_TOOLS_DIRECTORY:
+        g_value_set_string (value, self->tools_dir);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+pv_runtime_set_property (GObject *object,
+                         guint prop_id,
+                         const GValue *value,
+                         GParamSpec *pspec)
+{
+  PvRuntime *self = PV_RUNTIME (object);
+
+  switch (prop_id)
+    {
+      case PROP_BUBBLEWRAP:
+        /* Construct-only */
+        g_return_if_fail (self->bubblewrap == NULL);
+        self->bubblewrap = g_value_dup_string (value);
+        break;
+
+      case PROP_SOURCE_FILES:
+        /* Construct-only */
+        g_return_if_fail (self->source_files == NULL);
+        self->source_files = g_value_dup_string (value);
+        break;
+
+      case PROP_TOOLS_DIRECTORY:
+        /* Construct-only */
+        g_return_if_fail (self->tools_dir == NULL);
+        self->tools_dir = g_value_dup_string (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+pv_runtime_constructed (GObject *object)
+{
+  PvRuntime *self = PV_RUNTIME (object);
+
+  G_OBJECT_CLASS (pv_runtime_parent_class)->constructed (object);
+
+  g_return_if_fail (self->bubblewrap != NULL);
+  g_return_if_fail (self->source_files != NULL);
+  g_return_if_fail (self->tools_dir != NULL);
+}
+
+static gboolean
+pv_runtime_initable_init (GInitable *initable,
+                          GCancellable *cancellable G_GNUC_UNUSED,
+                          GError **error)
+{
+  PvRuntime *self = PV_RUNTIME (initable);
+  g_autofree gchar *files_ref = NULL;
+
+  g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (!g_file_test (self->bubblewrap, G_FILE_TEST_IS_EXECUTABLE))
+    {
+      return glnx_throw (error, "\"%s\" is not executable",
+                         self->bubblewrap);
+    }
+
+  if (!g_file_test (self->source_files, G_FILE_TEST_IS_DIR))
+    {
+      return glnx_throw (error, "\"%s\" is not a directory",
+                         self->source_files);
+    }
+
+  if (!g_file_test (self->tools_dir, G_FILE_TEST_IS_DIR))
+    {
+      return glnx_throw (error, "\"%s\" is not a directory",
+                         self->tools_dir);
+    }
+
+  /* Take a lock on the runtime until we're finished with setup,
+   * to make sure it doesn't get deleted. */
+  files_ref = g_build_filename (self->source_files, ".ref", NULL);
+  self->runtime_lock = pv_bwrap_lock_new (files_ref,
+                                          PV_BWRAP_LOCK_FLAGS_CREATE,
+                                          error);
+
+  /* If the runtime is being deleted, ... don't use it, I suppose? */
+  if (self->runtime_lock == NULL)
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+pv_runtime_finalize (GObject *object)
+{
+  PvRuntime *self = PV_RUNTIME (object);
+
+  g_free (self->bubblewrap);
+  g_free (self->source_files);
+  g_free (self->tools_dir);
+  pv_bwrap_lock_free (self->runtime_lock);
+
+  G_OBJECT_CLASS (pv_runtime_parent_class)->finalize (object);
+}
+
+static void
+pv_runtime_class_init (PvRuntimeClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = pv_runtime_get_property;
+  object_class->set_property = pv_runtime_set_property;
+  object_class->constructed = pv_runtime_constructed;
+  object_class->finalize = pv_runtime_finalize;
+
+  properties[PROP_BUBBLEWRAP] =
+    g_param_spec_string ("bubblewrap", "Bubblewrap",
+                         "Bubblewrap executable",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_SOURCE_FILES] =
+    g_param_spec_string ("source-files", "Source files",
+                         ("Path to read-only runtime files (merged-/usr "
+                          "or sysroot) on host system"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TOOLS_DIRECTORY] =
+    g_param_spec_string ("tools-directory", "Tools directory",
+                         "Path to pressure-vessel/bin on host system",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+}
+
+PvRuntime *
+pv_runtime_new (const char *source_files,
+                const char *bubblewrap,
+                const char *tools_dir,
+                GError **error)
+{
+  g_return_val_if_fail (source_files != NULL, NULL);
+  g_return_val_if_fail (bubblewrap != NULL, NULL);
+  g_return_val_if_fail (tools_dir != NULL, NULL);
+
+  return g_initable_new (PV_TYPE_RUNTIME,
+                         NULL,
+                         error,
+                         "bubblewrap", bubblewrap,
+                         "source-files", source_files,
+                         "tools-directory", tools_dir,
+                         NULL);
+}
+
+/* If we are using a runtime, pass the lock fd to the executed process,
+ * and make it act as a subreaper for the game itself.
+ *
+ * If we were using --unshare-pid then we could use bwrap --sync-fd
+ * and rely on bubblewrap's init process for this, but we currently
+ * can't do that without breaking gameoverlayrender.so's assumptions. */
+void
+pv_runtime_append_lock_adverb (PvRuntime *self,
+                               FlatpakBwrap *bwrap)
+{
+  g_return_if_fail (PV_IS_RUNTIME (self));
+  g_return_if_fail (!pv_bwrap_was_finished (bwrap));
+
+  flatpak_bwrap_add_args (bwrap,
+                          "/run/pressure-vessel/bin/pressure-vessel-with-lock",
+                          "--subreaper",
+                          NULL);
+
+  if (pv_bwrap_lock_is_ofd (self->runtime_lock))
+    {
+      int fd = pv_bwrap_lock_steal_fd (self->runtime_lock);
+      g_autofree gchar *fd_str = NULL;
+
+      g_debug ("Passing lock fd %d down to with-lock", fd);
+      flatpak_bwrap_add_fd (bwrap, fd);
+      fd_str = g_strdup_printf ("%d", fd);
+      flatpak_bwrap_add_args (bwrap,
+                              "--fd", fd_str,
+                              NULL);
+    }
+  else
+    {
+      /*
+       * We were unable to take out an open file descriptor lock,
+       * so it will be released on fork(). Tell the with-lock process
+       * to take out its own compatible lock instead. There will be
+       * a short window during which we have lost our lock but the
+       * with-lock process has not taken its lock - that's unavoidable
+       * if we want to use exec() to replace ourselves with the
+       * container.
+       *
+       * pv_bwrap_bind_usr() arranges for /.ref to either be a
+       * symbolic link to /usr/.ref which is the runtime_lock
+       * (if opt_runtime is a merged /usr), or the runtime_lock
+       * itself (otherwise).
+       */
+      g_debug ("Telling process in container to lock /.ref");
+      flatpak_bwrap_add_args (bwrap,
+                              "--lock-file", "/.ref",
+                              NULL);
+    }
+
+  flatpak_bwrap_add_args (bwrap,
+                          "--",
+                          NULL);
+}
+
+static void
+pv_runtime_initable_iface_init (GInitableIface *iface,
+                                gpointer unused G_GNUC_UNUSED)
+{
+  iface->init = pv_runtime_initable_init;
+}
diff --git a/src/runtime.h b/src/runtime.h
new file mode 100644
index 0000000000000000000000000000000000000000..bebd82996d6ebe95eab3562c0e763ced75b79ee8
--- /dev/null
+++ b/src/runtime.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "glib-backports.h"
+#include "libglnx/libglnx.h"
+
+#include "flatpak-bwrap-private.h"
+
+typedef struct _PvRuntime PvRuntime;
+typedef struct _PvRuntimeClass PvRuntimeClass;
+
+#define PV_TYPE_RUNTIME (pv_runtime_get_type ())
+#define PV_RUNTIME(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PV_TYPE_RUNTIME, PvRuntime))
+#define PV_RUNTIME_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), PV_TYPE_RUNTIME, PvRuntimeClass))
+#define PV_IS_RUNTIME(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PV_TYPE_RUNTIME))
+#define PV_IS_RUNTIME_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), PV_TYPE_RUNTIME))
+#define PV_RUNTIME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), PV_TYPE_RUNTIME, PvRuntimeClass)
+GType pv_runtime_get_type (void);
+
+PvRuntime *pv_runtime_new (const char *source_files,
+                           const char *bubblewrap,
+                           const char *tools_dir,
+                           GError **error);
+
+void pv_runtime_append_lock_adverb (PvRuntime *self,
+                                    FlatpakBwrap *bwrap);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvRuntime, g_object_unref)
diff --git a/src/wrap.c b/src/wrap.c
index 1eb31fea39781ef39eb23041ff5c016d77b85b83..e1360609cdc2e8bdfbcefbfb11d2c1adf5a08bb7 100644
--- a/src/wrap.c
+++ b/src/wrap.c
@@ -43,6 +43,7 @@
 #include "flatpak-run-private.h"
 #include "flatpak-utils-base-private.h"
 #include "flatpak-utils-private.h"
+#include "runtime.h"
 #include "utils.h"
 #include "wrap-interactive.h"
 
@@ -2013,11 +2014,11 @@ main (int argc,
   g_autoptr(GString) adjusted_ld_preload = g_string_new ("");
   g_autofree gchar *cwd_p = NULL;
   g_autofree gchar *cwd_l = NULL;
-  g_autoptr(PvBwrapLock) runtime_lock = NULL;
   const gchar *home;
   g_autofree gchar *bwrap_help = NULL;
   g_autofree gchar *tools_dir = NULL;
   const gchar *bwrap_help_argv[] = { "<bwrap>", "--help", NULL };
+  g_autoptr(PvRuntime) runtime = NULL;
 
   setlocale (LC_ALL, "");
   pv_avoid_gvfs ();
@@ -2397,23 +2398,18 @@ main (int argc,
 
   if (opt_runtime != NULL && opt_runtime[0] != '\0')
     {
-      g_autofree gchar *files_ref = NULL;
+      g_debug ("Configuring runtime %s...", opt_runtime);
 
-      g_debug ("Configuring runtime...");
+      runtime = pv_runtime_new (opt_runtime,
+                                bwrap_executable,
+                                tools_dir,
+                                error);
 
       /* Start with just the root tmpfs (which appears automatically)
        * and the standard API filesystems */
       pv_bwrap_add_api_filesystems (bwrap);
 
-      /* Take a lock on the runtime until we're finished with setup,
-       * to make sure it doesn't get deleted. */
-      files_ref = g_build_filename (opt_runtime, ".ref", NULL);
-      runtime_lock = pv_bwrap_lock_new (files_ref,
-                                        PV_BWRAP_LOCK_FLAGS_CREATE,
-                                        error);
-
-      /* If the runtime is being deleted, ... don't use it, I suppose? */
-      if (runtime_lock == NULL)
+      if (runtime == NULL)
         goto out;
 
       pv_search_path_append (bin_path, "/overrides/bin");
@@ -2510,8 +2506,7 @@ main (int argc,
 
           if (g_file_test (preload, G_FILE_TEST_EXISTS))
             {
-              if (opt_runtime != NULL
-                  && opt_runtime[0] != '\0'
+              if (runtime != NULL
                   && (g_str_has_prefix (preload, "/usr/")
                       || g_str_has_prefix (preload, "/lib")))
                 {
@@ -2580,7 +2575,7 @@ main (int argc,
 
   /* Put Steam Runtime environment variables back, if /usr is mounted
    * from the host. */
-  if (opt_runtime == NULL || opt_runtime[0] == '\0')
+  if (runtime == NULL)
     {
       g_debug ("Making Steam Runtime available...");
 
@@ -2630,57 +2625,8 @@ main (int argc,
   if (!flatpak_bwrap_bundle_args (bwrap, 1, -1, FALSE, error))
     goto out;
 
-  /* If we are using a runtime, pass the lock fd to the executed process,
-   * and make it act as a subreaper for the game itself.
-   *
-   * If we were using --unshare-pid then we could use bwrap --sync-fd
-   * and rely on bubblewrap's init process for this, but we currently
-   * can't do that without breaking gameoverlayrender.so's assumptions. */
-  if (runtime_lock != NULL)
-    {
-      flatpak_bwrap_add_args (bwrap,
-                              "/run/pressure-vessel/bin/pressure-vessel-with-lock",
-                              "--subreaper",
-                              NULL);
-
-      if (pv_bwrap_lock_is_ofd (runtime_lock))
-        {
-          int fd = pv_bwrap_lock_steal_fd (runtime_lock);
-          g_autofree gchar *fd_str = NULL;
-
-          g_debug ("Passing lock fd %d down to with-lock", fd);
-          flatpak_bwrap_add_fd (bwrap, fd);
-          fd_str = g_strdup_printf ("%d", fd);
-          flatpak_bwrap_add_args (bwrap,
-                                  "--fd", fd_str,
-                                  NULL);
-        }
-      else
-        {
-          /*
-           * We were unable to take out an open file descriptor lock,
-           * so it will be released on fork(). Tell the with-lock process
-           * to take out its own compatible lock instead. There will be
-           * a short window during which we have lost our lock but the
-           * with-lock process has not taken its lock - that's unavoidable
-           * if we want to use exec() to replace ourselves with the
-           * container.
-           *
-           * pv_bwrap_bind_usr() arranges for /.ref to either be a
-           * symbolic link to /usr/.ref which is the runtime_lock
-           * (if opt_runtime is a merged /usr), or the runtime_lock
-           * itself (otherwise).
-           */
-          g_debug ("Telling process in container to lock /.ref");
-          flatpak_bwrap_add_args (bwrap,
-                                  "--lock-file", "/.ref",
-                                  NULL);
-        }
-
-      flatpak_bwrap_add_args (bwrap,
-                              "--",
-                              NULL);
-    }
+  if (runtime != NULL)
+    pv_runtime_append_lock_adverb (runtime, bwrap);
 
   g_debug ("Adding wrapped command...");
   flatpak_bwrap_append_args (bwrap, wrapped_command->argv);