diff --git a/src/glib-backports.c b/src/glib-backports.c
index c7bfc1f9c8f460ac343a5e423f4c63a43cdb6984..ce4990ee15acbd95fbe213285a2e28c35370ea39 100644
--- a/src/glib-backports.c
+++ b/src/glib-backports.c
@@ -5,6 +5,8 @@
  *  Copyright 1997-2000 GLib team
  *  Copyright 2000 Red Hat, Inc.
  *  Copyright 2013-2019 Collabora Ltd.
+ *  Copyright 2018 Georges Basile Stavracas Neto
+ *  Copyright 2018 Philip Withnall
  *  g_execvpe implementation based on GNU libc execvp:
  *   Copyright 1991, 92, 95, 96, 97, 98, 99 Free Software Foundation, Inc.
  *
@@ -351,3 +353,140 @@ my_g_unix_fd_add_full (int priority,
   return ret;
 }
 #endif
+
+#if !GLIB_CHECK_VERSION(2, 58, 0)
+/**
+ * g_canonicalize_filename:
+ * @filename: (type filename): the name of the file
+ * @relative_to: (type filename) (nullable): the relative directory, or %NULL
+ * to use the current working directory
+ *
+ * Gets the canonical file name from @filename. All triple slashes are turned into
+ * single slashes, and all `..` and `.`s resolved against @relative_to.
+ *
+ * Symlinks are not followed, and the returned path is guaranteed to be absolute.
+ *
+ * If @filename is an absolute path, @relative_to is ignored. Otherwise,
+ * @relative_to will be prepended to @filename to make it absolute. @relative_to
+ * must be an absolute path, or %NULL. If @relative_to is %NULL, it'll fallback
+ * to g_get_current_dir().
+ *
+ * This function never fails, and will canonicalize file paths even if they don't
+ * exist.
+ *
+ * No file system I/O is done.
+ *
+ * Returns: (type filename) (transfer full): a newly allocated string with the
+ * canonical file path
+ * Since: 2.58
+ */
+gchar *
+my_g_canonicalize_filename (const gchar *filename,
+                            const gchar *relative_to)
+{
+  gchar *canon, *start, *p, *q;
+  guint i;
+
+  g_return_val_if_fail (relative_to == NULL || g_path_is_absolute (relative_to), NULL);
+
+  if (!g_path_is_absolute (filename))
+    {
+      gchar *cwd_allocated = NULL;
+      const gchar  *cwd;
+
+      if (relative_to != NULL)
+        cwd = relative_to;
+      else
+        cwd = cwd_allocated = g_get_current_dir ();
+
+      canon = g_build_filename (cwd, filename, NULL);
+      g_free (cwd_allocated);
+    }
+  else
+    {
+      canon = g_strdup (filename);
+    }
+
+  start = (char *)g_path_skip_root (canon);
+
+  if (start == NULL)
+    {
+      /* This shouldn't really happen, as g_get_current_dir() should
+         return an absolute pathname, but bug 573843 shows this is
+         not always happening */
+      g_free (canon);
+      return g_build_filename (G_DIR_SEPARATOR_S, filename, NULL);
+    }
+
+  /* POSIX allows double slashes at the start to
+   * mean something special (as does windows too).
+   * So, "//" != "/", but more than two slashes
+   * is treated as "/".
+   */
+  i = 0;
+  for (p = start - 1;
+       (p >= canon) &&
+         G_IS_DIR_SEPARATOR (*p);
+       p--)
+    i++;
+  if (i > 2)
+    {
+      i -= 1;
+      start -= i;
+      memmove (start, start+i, strlen (start+i) + 1);
+    }
+
+  /* Make sure we're using the canonical dir separator */
+  p++;
+  while (p < start && G_IS_DIR_SEPARATOR (*p))
+    *p++ = G_DIR_SEPARATOR;
+
+  p = start;
+  while (*p != 0)
+    {
+      if (p[0] == '.' && (p[1] == 0 || G_IS_DIR_SEPARATOR (p[1])))
+        {
+          memmove (p, p+1, strlen (p+1)+1);
+        }
+      else if (p[0] == '.' && p[1] == '.' && (p[2] == 0 || G_IS_DIR_SEPARATOR (p[2])))
+        {
+          q = p + 2;
+          /* Skip previous separator */
+          p = p - 2;
+          if (p < start)
+            p = start;
+          while (p > start && !G_IS_DIR_SEPARATOR (*p))
+            p--;
+          if (G_IS_DIR_SEPARATOR (*p))
+            *p++ = G_DIR_SEPARATOR;
+          memmove (p, q, strlen (q)+1);
+        }
+      else
+        {
+          /* Skip until next separator */
+          while (*p != 0 && !G_IS_DIR_SEPARATOR (*p))
+            p++;
+
+          if (*p != 0)
+            {
+              /* Canonicalize one separator */
+              *p++ = G_DIR_SEPARATOR;
+            }
+        }
+
+      /* Remove additional separators */
+      q = p;
+      while (*q && G_IS_DIR_SEPARATOR (*q))
+        q++;
+
+      if (p != q)
+        memmove (p, q, strlen (q) + 1);
+    }
+
+  /* Remove trailing slashes */
+  if (p > start && G_IS_DIR_SEPARATOR (*(p-1)))
+    *(p-1) = 0;
+
+  return canon;
+}
+#endif
diff --git a/src/glib-backports.h b/src/glib-backports.h
index 0e047412531ef71e032762e4e044c300657aeb10..78f7fe164f4b3566c4a3b69ee79255144234b766 100644
--- a/src/glib-backports.h
+++ b/src/glib-backports.h
@@ -94,3 +94,9 @@ guint my_g_unix_fd_add_full (int priority,
 GSource *my_g_unix_fd_source_new (int fd,
                                   GIOCondition condition);
 #endif
+
+#if !GLIB_CHECK_VERSION(2, 58, 0)
+#define g_canonicalize_filename(f, r) my_g_canonicalize_filename (f, r)
+gchar *my_g_canonicalize_filename (const gchar *filename,
+                                   const gchar *relative_to);
+#endif
diff --git a/src/meson.build b/src/meson.build
index 0ac2f0a4f7d7db605188bb57fb6474967f4880c3..27242f101364c88817a4708058217a7ca2f4bd72 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -54,6 +54,8 @@ pressure_vessel_utils = static_library(
     'glib-backports.h',
     'resolve-in-sysroot.c',
     'resolve-in-sysroot.h',
+    'tree-copy.c',
+    'tree-copy.h',
     'utils.c',
     'utils.h',
   ],
diff --git a/src/runtime.c b/src/runtime.c
index c7dd569f74ef3ac08d9d19c6415fb93976c4694b..6a90102578dc64013f057145b8dfdcb5be408250 100644
--- a/src/runtime.c
+++ b/src/runtime.c
@@ -36,6 +36,7 @@
 #include "enumtypes.h"
 #include "flatpak-run-private.h"
 #include "resolve-in-sysroot.h"
+#include "tree-copy.h"
 #include "utils.h"
 
 /*
@@ -550,16 +551,19 @@ pv_runtime_init_mutable (PvRuntime *self,
        * ${temp_dir}/usr/bin, etc. */
       source_usr = self->source_files;
 
-      if (!pv_cheap_tree_copy (self->source_files, dest_usr, error))
+      if (!pv_cheap_tree_copy (self->source_files, dest_usr,
+                               PV_COPY_FLAGS_NONE, error))
         return FALSE;
     }
   else
     {
       /* ${source_files}/usr exists, so assume it's a complete sysroot.
-       * Copy ${source_files}/bin to ${temp_dir}/bin, etc. */
+       * Merge ${source_files}/bin and ${source_files}/usr/bin into
+       * ${temp_dir}/usr/bin, etc. */
       source_usr = source_usr_subdir;
 
-      if (!pv_cheap_tree_copy (self->source_files, temp_dir, error))
+      if (!pv_cheap_tree_copy (self->source_files, temp_dir,
+                               PV_COPY_FLAGS_USRMERGE, error))
         return FALSE;
     }
 
@@ -2962,7 +2966,8 @@ pv_runtime_bind (PvRuntime *self,
 
       dest = glnx_fdrel_abspath (parent_dirfd, "from-host");
 
-      if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest, error))
+      if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest,
+                               PV_COPY_FLAGS_NONE, error))
         return FALSE;
 
       flatpak_bwrap_add_args (bwrap,
diff --git a/src/tree-copy.c b/src/tree-copy.c
new file mode 100644
index 0000000000000000000000000000000000000000..81160366e8880ac9b96ca180d1d2e5802abea110
--- /dev/null
+++ b/src/tree-copy.c
@@ -0,0 +1,332 @@
+/*
+ * Contains code taken from Flatpak.
+ *
+ * Copyright © 2014-2019 Red Hat, Inc
+ * Copyright © 2017-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 "tree-copy.h"
+
+#include <ftw.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+#include "flatpak-bwrap-private.h"
+#include "flatpak-utils-base-private.h"
+#include "flatpak-utils-private.h"
+
+/* Enabling debug logging for this is rather too verbose, so only
+ * enable it when actively debugging this module */
+#if 0
+#define trace(...) g_debug (__VA_ARGS__)
+#else
+#define trace(...) do { } while (0)
+#endif
+
+static inline gboolean
+gets_usrmerged (const char *path)
+{
+  while (path[0] == '/')
+    path++;
+
+  if (strcmp (path, "bin") == 0 ||
+      strcmp (path, "sbin") == 0 ||
+      g_str_has_prefix (path, "bin/") ||
+      g_str_has_prefix (path, "sbin/") ||
+      (g_str_has_prefix (path, "lib") &&
+       strcmp (path, "libexec") != 0 &&
+       !g_str_has_prefix (path, "libexec/")))
+    return TRUE;
+
+  return FALSE;
+}
+
+/* nftw() doesn't have a user_data argument so we need to use a global
+ * variable :-( */
+static struct
+{
+  gchar *source_root;
+  gchar *dest_root;
+  PvCopyFlags flags;
+  GError *error;
+} nftw_data;
+
+static int
+copy_tree_helper (const char *fpath,
+                  const struct stat *sb,
+                  int typeflag,
+                  struct FTW *ftwbuf)
+{
+  size_t len;
+  const char *suffix;
+  g_autofree gchar *dest = NULL;
+  g_autofree gchar *target = NULL;
+  GError **error = &nftw_data.error;
+  gboolean usrmerge;
+
+  g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1);
+
+  if (strcmp (fpath, nftw_data.source_root) == 0)
+    {
+      if (typeflag != FTW_D)
+        {
+          glnx_throw (error, "\"%s\" is not a directory", fpath);
+          return 1;
+        }
+
+      if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root,
+                                   sb->st_mode & 07777, NULL, error))
+        return 1;
+
+      return 0;
+    }
+
+  len = strlen (nftw_data.source_root);
+  g_return_val_if_fail (fpath[len] == '/', 1);
+  suffix = &fpath[len + 1];
+
+  while (suffix[0] == '/')
+    suffix++;
+
+  trace ("\"%s\": suffix=\"%s\"", fpath, suffix);
+
+  /* If source_root was /path/to/source and fpath was /path/to/source/foo/bar,
+   * then suffix is now foo/bar. */
+
+  if ((nftw_data.flags & PV_COPY_FLAGS_USRMERGE) != 0 &&
+      gets_usrmerged (suffix))
+    {
+      trace ("Transforming to \"usr/%s\" for /usr merge", suffix);
+      usrmerge = TRUE;
+      /* /path/to/dest/usr/foo/bar */
+      dest = g_build_filename (nftw_data.dest_root, "usr", suffix, NULL);
+    }
+  else
+    {
+      usrmerge = FALSE;
+      /* /path/to/dest/foo/bar */
+      dest = g_build_filename (nftw_data.dest_root, suffix, NULL);
+    }
+
+  switch (typeflag)
+    {
+      case FTW_D:
+      trace ("Is a directory");
+
+        /* If merging /usr, replace /bin, /sbin, /lib* with symlinks like
+         * /bin -> usr/bin */
+        if (usrmerge && strchr (suffix, '/') == NULL)
+          {
+            /* /path/to/dest/bin or similar */
+            g_autofree gchar *in_root = g_build_filename (nftw_data.dest_root,
+                                                          suffix, NULL);
+
+            target = g_build_filename ("usr", suffix, NULL);
+
+            if (TEMP_FAILURE_RETRY (symlink (target, in_root)) != 0)
+              {
+                glnx_throw_errno_prefix (error,
+                                         "Unable to create symlink \"%s\" -> \"%s\"",
+                                         dest, target);
+                return 1;
+              }
+
+            /* Fall through to create usr/bin or similar too */
+          }
+
+        if (!glnx_shutil_mkdir_p_at (-1, dest, sb->st_mode & 07777,
+                                     NULL, error))
+          return 1;
+        break;
+
+      case FTW_SL:
+        target = glnx_readlinkat_malloc (-1, fpath, NULL, error);
+
+        if (target == NULL)
+          return 1;
+
+        trace ("Is a symlink to \"%s\"", target);
+
+        if (usrmerge)
+          {
+            trace ("Checking for compat symlinks into /usr");
+
+            /* Ignore absolute compat symlinks /lib/foo -> /usr/lib/foo.
+             * In this case suffix would be lib/foo. (In a Debian-based
+             * source root, Debian Policy §10.5 says this is the only
+             * form of compat symlink that should exist in this
+             * direction.) */
+            if (g_str_has_prefix (target, "/usr/") &&
+                strcmp (target + 5, suffix) == 0)
+              {
+                trace ("Ignoring compat symlink \"%s\" -> \"%s\"",
+                       fpath, target);
+                return 0;
+              }
+
+            /* Ignore relative compat symlinks /lib/foo -> ../usr/lib/foo. */
+            if (target[0] != '/')
+              {
+                g_autofree gchar *dir = g_path_get_dirname (suffix);
+                g_autofree gchar *joined = NULL;
+                g_autofree gchar *canon = NULL;
+
+                joined = g_build_filename (dir, target, NULL);
+                trace ("Joined: \"%s\"", joined);
+                canon = g_canonicalize_filename (joined, "/");
+                trace ("Canonicalized: \"%s\"", canon);
+
+                if (g_str_has_prefix (canon, "/usr/") &&
+                    strcmp (canon + 5, suffix) == 0)
+                  {
+                    trace ("Ignoring compat symlink \"%s\" -> \"%s\"",
+                           fpath, target);
+                    return 0;
+                  }
+              }
+          }
+
+        if ((nftw_data.flags & PV_COPY_FLAGS_USRMERGE) != 0 &&
+             g_str_has_prefix (suffix, "usr/") &&
+             gets_usrmerged (suffix + 4))
+          {
+            trace ("Checking for compat symlinks out of /usr");
+
+            /* Ignore absolute compat symlinks /usr/lib/foo -> /lib/foo.
+             * In this case suffix would be usr/lib/foo. (In a Debian-based
+             * source root, Debian Policy §10.5 says this is the only
+             * form of compat symlink that should exist in this
+             * direction.) */
+            if (strcmp (suffix + 3, target) == 0)
+              {
+                trace ("Ignoring compat symlink \"%s\" -> \"%s\"",
+                       fpath, target);
+                return 0;
+              }
+
+            /* Ignore relative compat symlinks
+             * /usr/lib/foo -> ../../lib/foo. */
+            if (target[0] != '/')
+              {
+                g_autofree gchar *dir = g_path_get_dirname (suffix);
+                g_autofree gchar *joined = NULL;
+                g_autofree gchar *canon = NULL;
+
+                joined = g_build_filename (dir, target, NULL);
+                trace ("Joined: \"%s\"", joined);
+                canon = g_canonicalize_filename (joined, "/");
+                trace ("Canonicalized: \"%s\"", canon);
+                g_assert (canon[0] == '/');
+
+                if (strcmp (suffix + 3, canon) == 0)
+                  {
+                    trace ("Ignoring compat symlink \"%s\" -> \"%s\"",
+                           fpath, target);
+                    return 0;
+                  }
+              }
+          }
+
+        if (TEMP_FAILURE_RETRY (symlink (target, dest)) != 0)
+          {
+            glnx_throw_errno_prefix (error,
+                                     "Unable to create symlink \"%s\" -> \"%s\"",
+                                     dest, target);
+            return 1;
+          }
+        break;
+
+      case FTW_F:
+        trace ("Is a regular file");
+
+        /* Fast path: try to make a hard link. */
+        if (link (fpath, dest) == 0)
+          break;
+
+        /* Slow path: fall back to copying.
+         *
+         * This does a FICLONE or copy_file_range to get btrfs reflinks
+         * if possible, making the copy as cheap as cp --reflink=auto.
+         *
+         * Rather than second-guessing which errno values would result
+         * in link() failing but a copy succeeding, we just try it
+         * unconditionally - the worst that can happen is that this
+         * fails too. */
+        if (!glnx_file_copy_at (AT_FDCWD, fpath, sb,
+                                AT_FDCWD, dest,
+                                GLNX_FILE_COPY_OVERWRITE,
+                                NULL, error))
+          {
+            glnx_prefix_error (error, "Unable to copy \"%s\" to \"%s\"",
+                               fpath, dest);
+            return 1;
+          }
+        break;
+
+      default:
+        glnx_throw (&nftw_data.error,
+                    "Don't know how to handle ftw type flag %d at %s",
+                    typeflag, fpath);
+        return 1;
+    }
+
+  return 0;
+}
+
+gboolean
+pv_cheap_tree_copy (const char *source_root,
+                    const char *dest_root,
+                    PvCopyFlags flags,
+                    GError **error)
+{
+  int res;
+
+  g_return_val_if_fail (source_root != NULL, FALSE);
+  g_return_val_if_fail (dest_root != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  /* Can't run concurrently */
+  g_return_val_if_fail (nftw_data.source_root == NULL, FALSE);
+
+  nftw_data.source_root = flatpak_canonicalize_filename (source_root);
+  nftw_data.dest_root = flatpak_canonicalize_filename (dest_root);
+  nftw_data.flags = flags;
+  nftw_data.error = NULL;
+
+  res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS);
+
+  if (res == -1)
+    {
+      g_assert (nftw_data.error == NULL);
+      glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"",
+                               source_root, dest_root);
+    }
+  else if (res != 0)
+    {
+      g_propagate_error (error, g_steal_pointer (&nftw_data.error));
+    }
+
+  g_clear_pointer (&nftw_data.source_root, g_free);
+  g_clear_pointer (&nftw_data.dest_root, g_free);
+  g_assert (nftw_data.error == NULL);
+  return (res == 0);
+}
diff --git a/src/tree-copy.h b/src/tree-copy.h
new file mode 100644
index 0000000000000000000000000000000000000000..473005148fea484ac9d8195f45ef6f55e0a74448
--- /dev/null
+++ b/src/tree-copy.h
@@ -0,0 +1,45 @@
+/*
+ * Contains code taken from Flatpak.
+ *
+ * Copyright © 2014-2019 Red Hat, Inc
+ * Copyright © 2017-2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library 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>
+
+/*
+ * PvCopyFlags:
+ * @PV_COPY_FLAGS_USRMERGE: Transform the copied tree by merging
+ *  /bin, /sbin, /lib* into /usr, and replacing them with symbolic
+ *  links /bin -> usr/bin and so on.
+ * @PV_RESOLVE_FLAGS_NONE: No special behaviour.
+ *
+ * Flags affecting how pv_cheap_tree_copy() behaves.
+ */
+typedef enum
+{
+  PV_COPY_FLAGS_USRMERGE = (1 << 0),
+  PV_COPY_FLAGS_NONE = 0
+} PvCopyFlags;
+
+gboolean pv_cheap_tree_copy (const char *source_root,
+                             const char *dest_root,
+                             PvCopyFlags flags,
+                             GError **error);
diff --git a/src/utils.c b/src/utils.c
index 95f3bf80358d4362ecbbe29a5a88230f15409f3d..e646399bdd7eb9112a79d4438e6f554d4c74e9cf 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -286,143 +286,6 @@ pv_hash_table_get_arbitrary_key (GHashTable *table)
     return NULL;
 }
 
-/* nftw() doesn't have a user_data argument so we need to use a global
- * variable :-( */
-static struct
-{
-  gchar *source_root;
-  gchar *dest_root;
-  GError *error;
-} nftw_data;
-
-static int
-copy_tree_helper (const char *fpath,
-                  const struct stat *sb,
-                  int typeflag,
-                  struct FTW *ftwbuf)
-{
-  size_t len;
-  const char *suffix;
-  g_autofree gchar *dest = NULL;
-  g_autofree gchar *target = NULL;
-  GError **error = &nftw_data.error;
-
-  g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1);
-
-  if (strcmp (fpath, nftw_data.source_root) == 0)
-    {
-      if (typeflag != FTW_D)
-        {
-          glnx_throw (error, "\"%s\" is not a directory", fpath);
-          return 1;
-        }
-
-      if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root,
-                                   sb->st_mode & 07777, NULL, error))
-        return 1;
-
-      return 0;
-    }
-
-  len = strlen (nftw_data.source_root);
-  g_return_val_if_fail (fpath[len] == '/', 1);
-  suffix = &fpath[len + 1];
-  dest = g_build_filename (nftw_data.dest_root, suffix, NULL);
-
-  switch (typeflag)
-    {
-      case FTW_D:
-        if (!glnx_shutil_mkdir_p_at (-1, dest, sb->st_mode & 07777,
-                                     NULL, error))
-          return 1;
-        break;
-
-      case FTW_SL:
-        target = glnx_readlinkat_malloc (-1, fpath, NULL, error);
-
-        if (target == NULL)
-          return 1;
-
-        if (symlink (target, dest) != 0)
-          {
-            glnx_throw_errno_prefix (error,
-                                     "Unable to create symlink at \"%s\"",
-                                     dest);
-            return 1;
-          }
-        break;
-
-      case FTW_F:
-        /* Fast path: try to make a hard link. */
-        if (link (fpath, dest) == 0)
-          break;
-
-        /* Slow path: fall back to copying.
-         *
-         * This does a FICLONE or copy_file_range to get btrfs reflinks
-         * if possible, making the copy as cheap as cp --reflink=auto.
-         *
-         * Rather than second-guessing which errno values would result
-         * in link() failing but a copy succeeding, we just try it
-         * unconditionally - the worst that can happen is that this
-         * fails too. */
-        if (!glnx_file_copy_at (AT_FDCWD, fpath, sb,
-                                AT_FDCWD, dest,
-                                GLNX_FILE_COPY_OVERWRITE,
-                                NULL, error))
-          {
-            glnx_prefix_error (error, "Unable to copy \"%s\" to \"%s\"",
-                               fpath, dest);
-            return 1;
-          }
-        break;
-
-      default:
-        glnx_throw (&nftw_data.error,
-                    "Don't know how to handle ftw type flag %d at %s",
-                    typeflag, fpath);
-        return 1;
-    }
-
-  return 0;
-}
-
-gboolean
-pv_cheap_tree_copy (const char *source_root,
-                    const char *dest_root,
-                    GError **error)
-{
-  int res;
-
-  g_return_val_if_fail (source_root != NULL, FALSE);
-  g_return_val_if_fail (dest_root != NULL, FALSE);
-  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
-  /* Can't run concurrently */
-  g_return_val_if_fail (nftw_data.source_root == NULL, FALSE);
-
-  nftw_data.source_root = flatpak_canonicalize_filename (source_root);
-  nftw_data.dest_root = flatpak_canonicalize_filename (dest_root);
-  nftw_data.error = NULL;
-
-  res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS);
-
-  if (res == -1)
-    {
-      g_assert (nftw_data.error == NULL);
-      glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"",
-                               source_root, dest_root);
-    }
-  else if (res != 0)
-    {
-      g_propagate_error (error, g_steal_pointer (&nftw_data.error));
-    }
-
-  g_clear_pointer (&nftw_data.source_root, g_free);
-  g_clear_pointer (&nftw_data.dest_root, g_free);
-  g_assert (nftw_data.error == NULL);
-  return (res == 0);
-}
-
 static gint
 ftw_remove (const gchar *path,
             const struct stat *sb,
diff --git a/src/utils.h b/src/utils.h
index 3d89b7cedce569deca2ad6d61b2053e38a08e7d0..0966efedb88a1af8abd8e32dfbe45796941cacde 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -54,10 +54,6 @@ gchar *pv_capture_output (const char * const * argv,
 
 gpointer pv_hash_table_get_arbitrary_key (GHashTable *table);
 
-gboolean pv_cheap_tree_copy (const char *source_root,
-                             const char *dest_root,
-                             GError **error);
-
 gboolean pv_rm_rf (const char *directory);
 
 gboolean pv_boolean_environment (const gchar *name,
diff --git a/tests/cheap-copy.c b/tests/cheap-copy.c
index 9a5bf686c8a1183b48a58cbd2ad761c3942bc73d..f9fc6b3d2457cf4a2a6603ebdbc88df224fe05da 100644
--- a/tests/cheap-copy.c
+++ b/tests/cheap-copy.c
@@ -26,10 +26,18 @@
 #include "libglnx/libglnx.h"
 
 #include "glib-backports.h"
+#include "tree-copy.h"
 #include "utils.h"
 
+static gboolean opt_usrmerge = FALSE;
+
 static GOptionEntry options[] =
 {
+  { "usrmerge", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_usrmerge,
+    "Assume SOURCE is a sysroot, and carry out the /usr merge in DEST.",
+    NULL },
+
   { NULL }
 };
 
@@ -40,6 +48,7 @@ main (int argc,
   g_autoptr(GOptionContext) context = NULL;
   g_autoptr(GError) local_error = NULL;
   GError **error = &local_error;
+  PvCopyFlags flags = PV_COPY_FLAGS_NONE;
   int ret = EX_USAGE;
 
   setlocale (LC_ALL, "");
@@ -65,7 +74,10 @@ main (int argc,
 
   ret = EX_UNAVAILABLE;
 
-  if (!pv_cheap_tree_copy (argv[1], argv[2], error))
+  if (opt_usrmerge)
+    flags |= PV_COPY_FLAGS_USRMERGE;
+
+  if (!pv_cheap_tree_copy (argv[1], argv[2], flags, error))
     goto out;
 
   ret = 0;
diff --git a/tests/cheap-copy.py b/tests/cheap-copy.py
index e14082ebd679b7b13f0c7b6e56ce77cb8fbe86c3..f98d7d94eafa8c53c747149fed99e5d6fbcc6ddb 100755
--- a/tests/cheap-copy.py
+++ b/tests/cheap-copy.py
@@ -7,6 +7,7 @@ import os
 import subprocess
 import sys
 import tempfile
+from pathlib import Path
 
 
 try:
@@ -24,6 +25,7 @@ from testutils import (
 class TestCheapCopy(BaseTest):
     def setUp(self) -> None:
         super().setUp()
+        os.environ['G_MESSAGES_DEBUG'] = 'all'
         self.cheap_copy = os.path.join(self.G_TEST_BUILDDIR, 'test-cheap-copy')
 
     def assert_tree_is_superset(
@@ -156,6 +158,79 @@ class TestCheapCopy(BaseTest):
         """
         self.test_populated('/tmp', '/var/tmp', require_hard_links=False)
 
+    def test_usrmerge(self):
+        with tempfile.TemporaryDirectory(
+        ) as source, tempfile.TemporaryDirectory(
+        ) as parent, tempfile.TemporaryDirectory(
+        ) as expected:
+            (Path(source) / 'bin').mkdir(parents=True)
+            (Path(source) / 'lib').mkdir(parents=True)
+            (Path(source) / 'lib/x86_64-linux-gnu').mkdir(parents=True)
+            (Path(source) / 'lib32').mkdir(parents=True)
+            (Path(source) / 'usr/bin').mkdir(parents=True)
+            (Path(source) / 'usr/bin/which').touch()
+            (Path(source) / 'bin/which').symlink_to('/usr/bin/which')
+            (Path(source) / 'bin/less').touch()
+            (Path(source) / 'usr/bin/less').symlink_to('/bin/less')
+            (Path(source) / 'bin/more').touch()
+            (Path(source) / 'usr/bin/more').symlink_to('../../bin/more')
+            (Path(source) / 'usr/bin/env').touch()
+            (Path(source) / 'bin/env').symlink_to('../usr/bin/env')
+            (Path(source) / 'usr/bin/gcc').symlink_to('gcc-9')
+            (Path(source) / 'usr/bin/foo').symlink_to('/bin/foo-1')
+            (Path(source) / 'usr/bin/bar').symlink_to('../../bin/bar-2')
+            (Path(source) / 'usr/lib/x86_64-linux-gnu').mkdir(parents=True)
+            (Path(source) / 'bin/x').symlink_to('/usr/bin/x-1')
+            (Path(source) / 'bin/y').symlink_to('../usr/bin/x-2')
+            (Path(source) / 'lib/x86_64-linux-gnu/libpng12.so.0').symlink_to(
+                'libpng12.so.0.46.0')
+            (Path(source) / 'lib/x86_64-linux-gnu/libpng12.so.0.46.0').touch()
+            (
+                Path(source) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0'
+            ).symlink_to('/lib/x86_64-linux-gnu/libpng12.so.0')
+            (
+                Path(source) / 'usr/lib/x86_64-linux-gnu/libpng12.so'
+            ).symlink_to('libpng12.so.0')
+
+            (Path(expected) / 'bin').symlink_to('usr/bin')
+            (Path(expected) / 'lib').symlink_to('usr/lib')
+            (Path(expected) / 'lib32').symlink_to('usr/lib32')
+            (Path(expected) / 'usr/lib').mkdir(parents=True)
+            (Path(expected) / 'usr/lib/x86_64-linux-gnu').mkdir(parents=True)
+            (Path(expected) / 'usr/lib32').mkdir(parents=True)
+            (Path(expected) / 'usr/bin').mkdir(parents=True)
+            (Path(expected) / 'usr/bin/which').touch()
+            (Path(expected) / 'usr/bin/less').touch()
+            (Path(expected) / 'usr/bin/more').touch()
+            (Path(expected) / 'usr/bin/env').touch()
+            (Path(expected) / 'usr/bin/gcc').symlink_to('gcc-9')
+            (Path(expected) / 'usr/bin/foo').symlink_to('/bin/foo-1')
+            (Path(expected) / 'usr/bin/bar').symlink_to('../../bin/bar-2')
+            (Path(expected) / 'bin/x').symlink_to('/usr/bin/x-1')
+            (Path(expected) / 'bin/y').symlink_to('../usr/bin/x-2')
+            (
+                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0'
+            ).symlink_to('libpng12.so.0.46.0')
+            (
+                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0.46.0'
+            ).touch()
+            (
+                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so'
+            ).symlink_to('libpng12.so.0')
+
+            dest = os.path.join(parent, 'dest')
+            subprocess.run(
+                [
+                    self.cheap_copy,
+                    '--usrmerge',
+                    source,
+                    dest,
+                ],
+                check=True,
+                stdout=2,
+            )
+            self.assert_tree_is_same(expected, dest, require_hard_links=False)
+
     def tearDown(self) -> None:
         super().tearDown()
 
diff --git a/tests/containers.py b/tests/containers.py
index 77a5c760055065eeea5a1abb179709cb4ad336ea..9e1721e063d3307f0f9eb8e984ed8153a218a84c 100755
--- a/tests/containers.py
+++ b/tests/containers.py
@@ -25,6 +25,8 @@ ideally contains at least:
     The Platform merged-/usr from the SteamLinuxRuntime depot
 * scout_sysroot, soldier_sysroot:
     An SDK sysroot like the one recommended for the Docker container
+* scout_sysroot_usrmerge:
+    The same, but /usr-merged
 
 and run (for example) 'meson test -v -C _build' as usual.
 
@@ -906,6 +908,27 @@ class TestContainers(BaseTest):
         with self.subTest('transient'):
             self._test_scout('scout_sysroot', scout, locales=True)
 
+    def test_scout_sysroot_usrmerge(self) -> None:
+        scout = os.path.join(self.containers_dir, 'scout_sysroot_usrmerge')
+
+        if os.path.isdir(os.path.join(scout, 'files')):
+            scout = os.path.join(scout, 'files')
+
+        with self.subTest('only-prepare'):
+            self._test_scout(
+                'scout_sysroot_prep_usrmerge', scout,
+                copy=True, only_prepare=True,
+            )
+
+        with self.subTest('copy'):
+            self._test_scout(
+                'scout_sysroot_copy_usrmerge', scout,
+                copy=True, gc=False,
+            )
+
+        with self.subTest('transient'):
+            self._test_scout('scout_sysroot_usrmerge', scout, locales=True)
+
     def test_scout_usr(self) -> None:
         scout = os.path.join(self.containers_dir, 'scout', 'files')
 
diff --git a/tests/meson.build b/tests/meson.build
index 34d42008754fe57f8e93dca0fe7a19961288a657..0e9a3eb2a5e665572cfa5c6eff4afecd608e96ba 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -92,6 +92,10 @@ foreach test_name : tests
     test_args += ['-e', python.path()]
   endif
 
+  if test_name.endswith('launcher.py')
+    timeout = 60
+  endif
+
   if test_name.endswith('containers.py')
     timeout = 300
   endif