Blob Blame History Raw
/* builder-cache.c
 *
 * Copyright (C) 2015 Red Hat, Inc
 *
 * This file 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 of the
 * License, or (at your option) any later version.
 *
 * This file 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 program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/statfs.h>

#include <gio/gio.h>
#include <ostree.h>
#include "libglnx/libglnx.h"

#include "builder-flatpak-utils.h"
#include "builder-utils.h"
#include "builder-cache.h"
#include "builder-context.h"

struct BuilderCache
{
  GObject     parent;
  BuilderContext *context;
  GChecksum  *checksum;
  GFile      *app_dir;
  char       *branch;
  char       *stage;
  GHashTable *unused_stages;
  char       *last_parent;
  char       *current_checksum;
  OstreeRepo *repo;
  gboolean    disabled;
  OstreeRepoDevInoCache *devino_to_csum_cache;
};

typedef struct
{
  GObjectClass parent_class;
} BuilderCacheClass;

G_DEFINE_TYPE (BuilderCache, builder_cache, G_TYPE_OBJECT);

enum {
  PROP_0,
  PROP_CONTEXT,
  PROP_APP_DIR,
  PROP_BRANCH,
  LAST_PROP
};

#define OSTREE_GIO_FAST_QUERYINFO ("standard::name,standard::type,standard::size,standard::is-symlink,standard::symlink-target," \
                                   "unix::device,unix::inode,unix::mode,unix::uid,unix::gid,unix::rdev")

static GPtrArray   *builder_cache_get_changes_to (BuilderCache *self,
                                                  GFile        *current_root,
                                                  GPtrArray   **removals,
                                                  GError      **error);


static void
builder_cache_finalize (GObject *object)
{
  BuilderCache *self = (BuilderCache *) object;

  g_clear_object (&self->context);
  g_clear_object (&self->app_dir);
  g_clear_object (&self->repo);
  g_checksum_free (self->checksum);
  g_free (self->branch);
  g_free (self->last_parent);
  g_free (self->stage);
  g_free (self->current_checksum);
  if (self->unused_stages)
    g_hash_table_unref (self->unused_stages);

  if (self->devino_to_csum_cache)
    ostree_repo_devino_cache_unref (self->devino_to_csum_cache);

  G_OBJECT_CLASS (builder_cache_parent_class)->finalize (object);
}

static void
builder_cache_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  BuilderCache *self = BUILDER_CACHE (object);

  switch (prop_id)
    {
    case PROP_CONTEXT:
      g_value_set_object (value, self->context);
      break;

    case PROP_APP_DIR:
      g_value_set_object (value, self->app_dir);
      break;

    case PROP_BRANCH:
      g_value_set_string (value, self->branch);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
builder_cache_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  BuilderCache *self = BUILDER_CACHE (object);

  switch (prop_id)
    {
    case PROP_BRANCH:
      g_free (self->branch);
      self->branch = g_value_dup_string (value);
      break;

    case PROP_CONTEXT:
      g_set_object (&self->context, g_value_get_object (value));
      break;

    case PROP_APP_DIR:
      g_set_object (&self->app_dir, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
builder_cache_class_init (BuilderCacheClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = builder_cache_finalize;
  object_class->get_property = builder_cache_get_property;
  object_class->set_property = builder_cache_set_property;

  g_object_class_install_property (object_class,
                                   PROP_CONTEXT,
                                   g_param_spec_object ("context",
                                                        "",
                                                        "",
                                                        BUILDER_TYPE_CONTEXT,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
  g_object_class_install_property (object_class,
                                   PROP_APP_DIR,
                                   g_param_spec_object ("app-dir",
                                                        "",
                                                        "",
                                                        G_TYPE_FILE,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
  g_object_class_install_property (object_class,
                                   PROP_BRANCH,
                                   g_param_spec_string ("branch",
                                                        "",
                                                        "",
                                                        NULL,
                                                        G_PARAM_READWRITE));
}

static void
builder_cache_init (BuilderCache *self)
{
  self->checksum = g_checksum_new (G_CHECKSUM_SHA256);
  self->devino_to_csum_cache = ostree_repo_devino_cache_new ();
}

BuilderCache *
builder_cache_new (BuilderContext *context,
                   GFile      *app_dir,
                   const char *branch)
{
  return g_object_new (BUILDER_TYPE_CACHE,
                       "context", context,
                       "app-dir", app_dir,
                       "branch", branch,
                       NULL);
}

GChecksum *
builder_cache_get_checksum (BuilderCache *self)
{
  return self->checksum;
}

static void
append_escaped_stage (GString *s,
                      const char *stage)
{
  while (*stage)
    {
      char c = *stage++;
      if (g_ascii_isalnum (c) ||
          c == '-' ||
          c == '_' ||
          c == '.')
        g_string_append_c (s, c);
      else
        g_string_append_printf (s, "%x", c);
    }
}

static char *
get_ref (BuilderCache *self, const char *stage)
{
  GString *s = g_string_new (self->branch);

  g_string_append_c (s, '/');

  append_escaped_stage (s, stage);

  return g_string_free (s, FALSE);
}

gboolean
builder_cache_open (BuilderCache *self,
                    GError      **error)
{
  g_autoptr(GKeyFile) config = NULL;
  g_autofree char *old_mfsp = NULL;

  self->repo = ostree_repo_new (builder_context_get_cache_dir (self->context));

  if (!g_file_query_exists (builder_context_get_cache_dir (self->context), NULL))
    {
      g_autoptr(GFile) parent = g_file_get_parent (builder_context_get_cache_dir (self->context));

      if (!flatpak_mkdir_p (parent, NULL, error))
        return FALSE;

      if (!ostree_repo_create (self->repo, OSTREE_REPO_MODE_BARE_USER_ONLY, NULL, error))
        return FALSE;
    }

  if (!ostree_repo_open (self->repo, NULL, error))
    return FALSE;

  config = ostree_repo_copy_config (self->repo);

  old_mfsp = g_key_file_get_value (config, "core", "min-free-space-percent", NULL);
  if (g_strcmp0 (old_mfsp, "0") != 0)
    {
      g_key_file_set_value (config, "core", "min-free-space-percent", "0");

      if (!ostree_repo_write_config (self->repo, config, error))
        return FALSE;

      /* Re-open */
      g_clear_object (&self->repo);
      self->repo = ostree_repo_new (builder_context_get_cache_dir (self->context));
      if (!ostree_repo_open (self->repo, NULL, error))
          return FALSE;
    }

  /* We don't need fsync on checkouts as they are transient, and we
     rely on the syncfs() in the transaction commit for commits. */
  ostree_repo_set_disable_fsync (self->repo, TRUE);

  /* At one point we used just the branch name as a ref, make sure to
   * remove this to handle using the branch as a subdir */
  ostree_repo_set_ref_immediate (self->repo,
                                 NULL,
                                 self->branch,
                                 NULL,
                                 NULL, NULL);

  /* List all stages first so we can purge unused ones at the end */
  if (!ostree_repo_list_refs (self->repo,
                              self->branch,
                              &self->unused_stages,
                              NULL, error))
    return FALSE;

  return TRUE;
}

static gboolean
builder_cache_checkout (BuilderCache *self, const char *commit, gboolean delete_dir, GError **error)
{
  g_autoptr(GError) my_error = NULL;
  OstreeRepoCheckoutAtOptions options = { 0, };

  if (delete_dir)
    {
      if (!g_file_delete (self->app_dir, NULL, &my_error) &&
          !g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        {
          g_propagate_error (error, g_steal_pointer (&my_error));
          return FALSE;
        }

      if (!flatpak_mkdir_p (self->app_dir, NULL, error))
        return FALSE;
    }

  /* If rofiles-fuse is disabled, we check out with force_copy
     because we want to force the checkout to not use
     hardlinks. Hard links into the cache without rofiles-fuse are notx
     safe, as the build could mutate the cache. */
  if (!builder_context_get_use_rofiles (self->context))
    options.force_copy = TRUE;

  options.mode = OSTREE_REPO_CHECKOUT_MODE_USER;
  options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES;
  options.devino_to_csum_cache = self->devino_to_csum_cache;

  if (!ostree_repo_checkout_at (self->repo, &options,
                                AT_FDCWD, flatpak_file_get_path_cached (self->app_dir),
                                commit, NULL, error))
    return FALSE;

  /* There is a bug in ostree (https://github.com/ostreedev/ostree/issues/326) that
     causes it to not reset mtime to 0 in them force_copy case. So we do that
     manually */
  if (options.force_copy &&
      !flatpak_zero_mtime (AT_FDCWD, flatpak_file_get_path_cached (self->app_dir),
                           NULL, error))
    return FALSE;

  return TRUE;
}

gboolean
builder_cache_has_checkout (BuilderCache *self)
{
  return self->disabled;
}

void
builder_cache_ensure_checkout (BuilderCache *self)
{
  if (builder_cache_has_checkout (self))
    return;

  if (self->last_parent)
    {
      g_autoptr(GError) error = NULL;
      g_print ("Everything cached, checking out from cache\n");

      if (!builder_cache_checkout (self, self->last_parent, TRUE, &error))
        g_error ("Failed to check out cache: %s", error->message);
    }

  self->disabled = TRUE;
}

static char *
builder_cache_get_current_ref (BuilderCache *self)
{
  return get_ref (self, self->stage);
}

gboolean
builder_cache_lookup (BuilderCache *self,
                      const char   *stage)
{
  g_autofree char *commit = NULL;
  g_autofree char *ref = NULL;
  g_autoptr(GString) s = g_string_new ("");

  g_free (self->stage);
  self->stage = g_strdup (stage);

  append_escaped_stage (s, stage);
  g_hash_table_remove (self->unused_stages, s->str);

  g_free (self->current_checksum);
  self->current_checksum = g_strdup (g_checksum_get_string (self->checksum));

  /* Reset the checksum, but feed it previous checksum so we chain it */
  g_checksum_reset (self->checksum);
  builder_cache_checksum_str (self, self->current_checksum);

  if (self->disabled)
    return FALSE;

  ref = builder_cache_get_current_ref (self);
  if (!ostree_repo_resolve_rev (self->repo, ref, TRUE, &commit, NULL))
    goto checkout;


  if (commit != NULL)
    {
      g_autoptr(GVariant) variant = NULL;
      const gchar *subject;

      if (!ostree_repo_load_variant (self->repo, OSTREE_OBJECT_TYPE_COMMIT, commit,
                                     &variant, NULL))
        goto checkout;

      g_variant_get (variant, "(a{sv}aya(say)&s&stayay)", NULL, NULL, NULL,
                     &subject, NULL, NULL, NULL, NULL);

      if (strcmp (subject, self->current_checksum) == 0)
        {
          g_free (self->last_parent);
          self->last_parent = g_steal_pointer (&commit);

          return TRUE;
        }
    }

checkout:
  if (self->last_parent)
    {
      g_autoptr(GError) error = NULL;
      g_print ("Cache miss, checking out last cache hit\n");

      if (!builder_cache_checkout (self, self->last_parent, TRUE, &error))
        g_error ("Failed to check out cache: %s", error->message);
    }

  self->disabled = TRUE; /* Don't use cache any more after first miss */

  return FALSE;
}

static gboolean
mtree_empty (OstreeMutableTree *mtree)
{
  GHashTable *files = ostree_mutable_tree_get_files (mtree);
  GHashTable *subdirs = ostree_mutable_tree_get_subdirs (mtree);

  return
    g_hash_table_size (files) == 0 &&
    g_hash_table_size (subdirs) == 0;
}

/* This takes a mutable tree and an existing OstreeRepoFile, and recursively
 * removes all mtree files that already exists in the OstreeRepoFile.
 * This is very useful to create a commit with just the new files, which
 * we can then check out in order to get a the new hardlinks to the
 * cache repo.
 */
static gboolean
mtree_prune_old_files (OstreeMutableTree *mtree,
                       OstreeRepoFile *old,
                       GError **error)
{
  GHashTable *files = ostree_mutable_tree_get_files (mtree);
  GHashTable *subdirs = ostree_mutable_tree_get_subdirs (mtree);
  GHashTableIter iter;
  gpointer key, value;

  ostree_mutable_tree_set_contents_checksum (mtree, NULL);

  if (old != NULL && !ostree_repo_file_ensure_resolved (old, error))
    return FALSE;

  g_hash_table_iter_init (&iter, files);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      const char *name = key;
      const char *csum = value;
      int n = -1;
      gboolean is_dir;
      g_autoptr(GVariant) container = NULL;
      gboolean same = FALSE;

      if (old)
        n = ostree_repo_file_tree_find_child  (old, name, &is_dir, &container);

      if (n >= 0)
        {
          if (!is_dir)
            {
              g_autoptr(GVariant) old_csum_bytes = NULL;
              g_autofree char *old_csum = NULL;

              g_variant_get_child (container, n,
                                   "(@s@ay)", NULL, &old_csum_bytes);
              old_csum = ostree_checksum_from_bytes_v (old_csum_bytes);

              if (strcmp (old_csum, csum) == 0)
                same = TRUE; /* Modified file */
            }
        }

      if (same)
        g_hash_table_iter_remove (&iter);
    }

  g_hash_table_iter_init (&iter, subdirs);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      const char *name = key;
      OstreeMutableTree *subdir = value;
      g_autoptr(GFile) old_subdir = NULL;
      int n = -1;
      gboolean is_dir;

      if (old)
        n = ostree_repo_file_tree_find_child  (old, name, &is_dir, NULL);

      if (n >= 0 && is_dir)
        old_subdir = g_file_get_child (G_FILE (old), name);

      if (!mtree_prune_old_files (subdir, OSTREE_REPO_FILE (old_subdir), error))
        return FALSE;

      if (mtree_empty (subdir))
        g_hash_table_iter_remove (&iter);
    }

  return TRUE;
}

static OstreeRepoCommitFilterResult
commit_filter (OstreeRepo *repo,
               const char *path,
               GFileInfo  *file_info,
               gpointer    commit_data)
{
  guint mode;

  /* No user info */
  g_file_info_set_attribute_uint32 (file_info, "unix::uid", 0);
  g_file_info_set_attribute_uint32 (file_info, "unix::gid", 0);

  /* In flatpak, there is no real reason for files to have different
   * permissions based on the group or user really, everything is
   * always used readonly for everyone. Having things be writeable
   * for anyone but the user just causes risks for the system-installed
   * case. So, we canonicalize the mode to writable only by the user,
   * readable to all, and executable for all for directories and
   * files that the user can execute.
  */
  mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode");
  if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
    mode = 0755 | S_IFDIR;
  else if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
    {
      /* If use can execute, make executable by all */
      if (mode & S_IXUSR)
        mode = 0755 | S_IFREG;
      else /* otherwise executable by none */
        mode = 0644 | S_IFREG;
    }
  g_file_info_set_attribute_uint32 (file_info, "unix::mode", mode);

  return OSTREE_REPO_COMMIT_FILTER_ALLOW;
}

gboolean
builder_cache_commit (BuilderCache *self,
                      const char   *body,
                      GError      **error)
{
  const char *current = NULL;
  OstreeRepoCommitModifier *modifier = NULL;

  g_autoptr(OstreeMutableTree) mtree = NULL;
  g_autoptr(GFile) root = NULL;
  g_autofree char *commit_checksum = NULL;
  g_autofree char *new_commit_checksum = NULL;
  gboolean res = FALSE;
  g_autofree char *ref = NULL;
  g_autoptr(GFile) last_root = NULL;
  g_autoptr(GFile) new_root = NULL;
  g_autoptr(GPtrArray) changes = NULL;
  g_autoptr(GPtrArray) removals = NULL;
  g_autoptr(GVariantDict) metadata_dict = NULL;
  g_autoptr(GVariant) metadata = NULL;
  g_autoptr(GVariant) changesv = NULL;
  g_autoptr(GVariant) removalsv = NULL;
  g_autoptr(GVariant) changesvz = NULL;
  g_autoptr(GVariant) removalsvz = NULL;

  g_print ("Committing stage %s to cache\n", self->stage);

  /* We set all mtimes to 0 during a commit, to simulate what would happen when
     running via flatpak deploy (and also if we checked out from the cache). */
  if (!flatpak_zero_mtime (AT_FDCWD, flatpak_file_get_path_cached (self->app_dir),
                           NULL, NULL))
    return FALSE;

  if (!ostree_repo_prepare_transaction (self->repo, NULL, NULL, error))
    return FALSE;

  mtree = ostree_mutable_tree_new ();

  modifier = ostree_repo_commit_modifier_new (OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS,
                                              (OstreeRepoCommitFilter) commit_filter, NULL, NULL);
  if (self->devino_to_csum_cache)
    ostree_repo_commit_modifier_set_devino_cache (modifier, self->devino_to_csum_cache);

  if (!ostree_repo_write_directory_to_mtree (self->repo, self->app_dir,
                                             mtree, modifier, NULL, error))
    goto out;

  if (!ostree_repo_write_mtree (self->repo, mtree, &root, NULL, error))
    goto out;

  changes = builder_cache_get_changes_to (self, root, &removals, NULL);

  metadata_dict = g_variant_dict_new (NULL);

  changesv = g_variant_ref_sink (g_variant_new_strv ((const gchar * const  *) changes->pdata, changes->len));
  changesvz = flatpak_variant_compress (changesv);
  g_variant_dict_insert_value (metadata_dict, "changesz", changesvz);

  removalsv = g_variant_ref_sink (g_variant_new_strv ((const gchar * const  *) removals->pdata, removals->len));
  removalsvz = flatpak_variant_compress (removalsv);
  g_variant_dict_insert_value (metadata_dict, "removalsz", removalsvz);

  metadata = g_variant_ref_sink (g_variant_dict_end (metadata_dict));

  current = self->current_checksum;

  if (!ostree_repo_write_commit (self->repo, self->last_parent, current, body, metadata,
                                 OSTREE_REPO_FILE (root),
                                 &commit_checksum, NULL, error))
    goto out;

  ref = builder_cache_get_current_ref (self);
  ostree_repo_transaction_set_ref (self->repo, NULL, ref, commit_checksum);

  if (self->last_parent &&
      !ostree_repo_read_commit (self->repo, self->last_parent, &last_root, NULL, NULL, error))
    goto out;

  if (!mtree_prune_old_files (mtree, OSTREE_REPO_FILE (last_root), error))
    goto out;

  if (!ostree_repo_write_mtree (self->repo, mtree, &new_root, NULL, error))
    goto out;

  if (!ostree_repo_write_commit (self->repo, NULL, current, body, metadata,
                                 OSTREE_REPO_FILE (new_root),
                                 &new_commit_checksum, NULL, error))
    goto out;

  if (!ostree_repo_commit_transaction (self->repo, NULL, NULL, error))
    goto out;

  /* Check out the just commited cache so we hardlinks to the cache */
  if (builder_context_get_use_rofiles (self->context) &&
      !builder_cache_checkout (self, new_commit_checksum, FALSE, error))
    goto out;

  g_free (self->last_parent);
  self->last_parent = g_steal_pointer (&commit_checksum);

  res = TRUE;

out:
  if (!res)
    {
      if (!ostree_repo_abort_transaction (self->repo, NULL, NULL))
        g_warning ("failed to abort transaction");
    }
  if (modifier)
    ostree_repo_commit_modifier_unref (modifier);

  return res;
}

typedef struct {
  dev_t dev;
  ino_t ino;
  char checksum[OSTREE_SHA256_STRING_LEN+1];
} OstreeDevIno;

static const char *
devino_cache_lookup (OstreeRepoDevInoCache *devino_to_csum_cache,
                     guint32               device,
                     guint32               inode)
{
  OstreeDevIno dev_ino_key;
  OstreeDevIno *dev_ino_val;
  GHashTable *cache = (GHashTable *)devino_to_csum_cache;

  if (devino_to_csum_cache == NULL)
    return NULL;

  dev_ino_key.dev = device;
  dev_ino_key.ino = inode;
  dev_ino_val = g_hash_table_lookup (cache, &dev_ino_key);

  if (!dev_ino_val)
    return NULL;

  return dev_ino_val->checksum;
}

static gboolean
get_file_checksum (OstreeRepoDevInoCache *devino_to_csum_cache,
                   GFile *f,
                   GFileInfo *f_info,
                   char  **out_checksum,
                   GCancellable *cancellable,
                   GError   **error)
{
  g_autofree char *ret_checksum = NULL;
  g_autofree guchar *csum = NULL;

  if (OSTREE_IS_REPO_FILE (f))
    {
      ret_checksum = g_strdup (ostree_repo_file_get_checksum ((OstreeRepoFile*)f));
    }
  else
    {
      const char *cached = devino_cache_lookup (devino_to_csum_cache,
                                                g_file_info_get_attribute_uint32 (f_info, "unix::device"),
                                                g_file_info_get_attribute_uint64 (f_info, "unix::inode"));
      if (cached)
        ret_checksum = g_strdup (cached);
      else
        {
          g_autoptr(GInputStream) in = NULL;

          if (g_file_info_get_file_type (f_info) == G_FILE_TYPE_REGULAR)
            {
              in = (GInputStream*)g_file_read (f, cancellable, error);
              if (!in)
                return FALSE;
            }

          if (!ostree_checksum_file_from_input (f_info, NULL, in,
                                                OSTREE_OBJECT_TYPE_FILE,
                                                &csum, cancellable, error))
            return FALSE;

          ret_checksum = ostree_checksum_from_bytes (csum);
        }
    }

  *out_checksum = g_steal_pointer (&ret_checksum);
  return TRUE;
}

static gboolean
diff_files (OstreeRepoDevInoCache *devino_to_csum_cache,
            GFile           *a,
            GFileInfo       *a_info,
            GFile           *b,
            GFileInfo       *b_info,
            gboolean        *was_changed,
            GCancellable    *cancellable,
            GError         **error)
{
  g_autofree char *checksum_a = NULL;
  g_autofree char *checksum_b = NULL;

  if (!get_file_checksum (devino_to_csum_cache, a, a_info, &checksum_a, cancellable, error))
    return FALSE;

  if (!get_file_checksum (devino_to_csum_cache, b, b_info, &checksum_b, cancellable, error))
    return FALSE;

  *was_changed = strcmp (checksum_a, checksum_b) != 0;

  return TRUE;
}

static gboolean
diff_add_dir_recurse (GFile          *d,
                      GPtrArray      *added,
                      GCancellable   *cancellable,
                      GError        **error)
{
  GError *temp_error = NULL;
  g_autoptr(GFileEnumerator) dir_enum = NULL;
  g_autoptr(GFile) child = NULL;
  g_autoptr(GFileInfo) child_info = NULL;

  dir_enum = g_file_enumerate_children (d, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        cancellable,
                                        error);
  if (!dir_enum)
    return FALSE;

  while ((child_info = g_file_enumerator_next_file (dir_enum, cancellable, &temp_error)) != NULL)
    {
      const char *name;

      name = g_file_info_get_name (child_info);

      g_clear_object (&child);
      child = g_file_get_child (d, name);

      g_ptr_array_add (added, g_object_ref (child));

      if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY)
        {
          if (!diff_add_dir_recurse (child, added, cancellable, error))
            return FALSE;
        }

      g_clear_object (&child_info);
    }

  if (temp_error != NULL)
    {
      g_propagate_error (error, temp_error);
      return FALSE;
    }

  return TRUE;
}


static gboolean
diff_dirs (OstreeRepoDevInoCache *devino_to_csum_cache,
           GFile          *a,
           GFile          *b,
           GPtrArray      *changed,
           GCancellable   *cancellable,
           GError        **error)
{
  GError *temp_error = NULL;
  g_autoptr(GFileEnumerator) dir_enum = NULL;
  g_autoptr(GFile) child_a = NULL;
  g_autoptr(GFile) child_b = NULL;
  g_autoptr(GFileInfo) child_a_info = NULL;
  g_autoptr(GFileInfo) child_b_info = NULL;

  if (a == NULL)
    {
      if (!diff_add_dir_recurse (b, changed, cancellable, error))
        return FALSE;

      return TRUE;
    }

  dir_enum = g_file_enumerate_children (a, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        cancellable, error);
  if (!dir_enum)
    return FALSE;

  while ((child_a_info = g_file_enumerator_next_file (dir_enum, cancellable, &temp_error)) != NULL)
    {
      const char *name;
      GFileType child_a_type;
      GFileType child_b_type;

      name = g_file_info_get_name (child_a_info);

      g_clear_object (&child_a);
      child_a = g_file_get_child (a, name);
      child_a_type = g_file_info_get_file_type (child_a_info);

      g_clear_object (&child_b);
      child_b = g_file_get_child (b, name);

      g_clear_object (&child_b_info);
      child_b_info = g_file_query_info (child_b, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        cancellable,
                                        &temp_error);
      if (!child_b_info)
        {
          if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
            {
              g_clear_error (&temp_error);
              /* Removed, ignore */
            }
          else
            {
              g_propagate_error (error, temp_error);
              return FALSE;
            }
        }
      else
        {
          child_b_type = g_file_info_get_file_type (child_b_info);
          if (child_a_type != child_b_type)
            {
              g_ptr_array_add (changed, g_object_ref (child_b));
            }
          else
            {
              gboolean was_changed = FALSE;

              if (!diff_files (devino_to_csum_cache,
                               child_a, child_a_info,
                               child_b, child_b_info,
                               &was_changed,
                               cancellable, error))
                return FALSE;

              if (was_changed)
                g_ptr_array_add (changed, g_object_ref (child_b));

              if (child_a_type == G_FILE_TYPE_DIRECTORY)
                {
                  if (!diff_dirs (devino_to_csum_cache, child_a, child_b, changed,
                                  cancellable, error))
                    return FALSE;
                }
            }
        }

      g_clear_object (&child_a_info);
    }
  if (temp_error != NULL)
    {
      g_propagate_error (error, temp_error);
      return FALSE;
    }

  g_clear_object (&dir_enum);
  dir_enum = g_file_enumerate_children (b, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        cancellable, error);
  if (!dir_enum)
    return FALSE;

  g_clear_object (&child_b_info);
  while ((child_b_info = g_file_enumerator_next_file (dir_enum, cancellable, &temp_error)) != NULL)
    {
      const char *name;

      name = g_file_info_get_name (child_b_info);

      g_clear_object (&child_a);
      child_a = g_file_get_child (a, name);

      g_clear_object (&child_b);
      child_b = g_file_get_child (b, name);

      g_clear_object (&child_a_info);
      child_a_info = g_file_query_info (child_a, OSTREE_GIO_FAST_QUERYINFO,
                                        G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                        cancellable,
                                        &temp_error);
      if (!child_a_info)
        {
          if (g_error_matches (temp_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
            {
              g_clear_error (&temp_error);
              g_ptr_array_add (changed, g_object_ref (child_b));
              if (g_file_info_get_file_type (child_b_info) == G_FILE_TYPE_DIRECTORY)
                {
                  if (!diff_add_dir_recurse (child_b, changed, cancellable, error))
                    return FALSE;
                }
            }
          else
            {
              g_propagate_error (error, temp_error);
              return FALSE;
            }
        }
      g_clear_object (&child_b_info);
    }
  if (temp_error != NULL)
    {
      g_propagate_error (error, temp_error);
      return FALSE;
    }

  return TRUE;
}

gboolean
builder_cache_get_outstanding_changes (BuilderCache *self,
                                       GPtrArray   **changed_out,
                                       GError      **error)
{
  g_autoptr(GPtrArray) changed = g_ptr_array_new_with_free_func (g_object_unref);
  g_autoptr(GPtrArray) changed_paths = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GFile) last_root = NULL;
  int i;

  if (self->last_parent &&
      !ostree_repo_read_commit (self->repo, self->last_parent, &last_root, NULL, NULL, error))
    return FALSE;

  if (!diff_dirs (self->devino_to_csum_cache,
                  last_root,
                  self->app_dir,
                  changed,
                  NULL, error))
    return FALSE;

  for (i = 0; i < changed->len; i++)
    {
      GFile *changed_file = g_ptr_array_index (changed, i);
      char *path = g_file_get_relative_path (self->app_dir, changed_file);
      g_ptr_array_add (changed_paths, path);
    }

  if (changed_out)
    *changed_out = g_steal_pointer (&changed_paths);

  return TRUE;
}

static int
cmpstringp (const void *p1, const void *p2)
{
  return strcmp (*(char * const *) p1, *(char * const *) p2);
}

static GPtrArray *
get_changes (BuilderCache *self,
             GFile       *from,
             GFile       *to,
             GPtrArray  **removed_out,
             GError      **error)
{
  g_autoptr(GPtrArray) added = g_ptr_array_new_with_free_func (g_object_unref);
  g_autoptr(GPtrArray) modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref);
  g_autoptr(GPtrArray) removed = g_ptr_array_new_with_free_func (g_object_unref);
  g_autoptr(GPtrArray) changed_paths = g_ptr_array_new_with_free_func (g_free);
  int i;

  if (!ostree_diff_dirs (OSTREE_DIFF_FLAGS_NONE,
                         from,
                         to,
                         modified,
                         removed,
                         added,
                         NULL, error))
    return NULL;

  for (i = 0; i < added->len; i++)
    {
      char *path = g_file_get_relative_path (to, g_ptr_array_index (added, i));
      g_ptr_array_add (changed_paths, path);
    }

  for (i = 0; i < modified->len; i++)
    {
      OstreeDiffItem *modified_item = g_ptr_array_index (modified, i);
      char *path = g_file_get_relative_path (to, modified_item->target);
      g_ptr_array_add (changed_paths, path);
    }

  g_ptr_array_sort (changed_paths, cmpstringp);

  if (removed_out)
    {
      GPtrArray *removed_paths = g_ptr_array_new_with_free_func (g_free);

      for (i = 0; i < removed->len; i++)
        {
          char *path = g_file_get_relative_path (to, g_ptr_array_index (removed, i));
          g_ptr_array_add (removed_paths, path);
        }
      *removed_out = removed_paths;
    }

  return g_steal_pointer (&changed_paths);
}


/* This returns removals too */
static GPtrArray *
get_all_changes (BuilderCache *self,
                 GFile       *from,
                 GFile       *to,
                 GError      **error)
{
  g_autoptr(GPtrArray) added = g_ptr_array_new_with_free_func (g_object_unref);
  g_autoptr(GPtrArray) modified = g_ptr_array_new_with_free_func ((GDestroyNotify) ostree_diff_item_unref);
  g_autoptr(GPtrArray) removed = g_ptr_array_new_with_free_func (g_object_unref);
  g_autoptr(GPtrArray) changed_paths = g_ptr_array_new_with_free_func (g_free);
  int i;

  if (!ostree_diff_dirs (OSTREE_DIFF_FLAGS_NONE,
                         from,
                         to,
                         modified,
                         removed,
                         added,
                         NULL, error))
    return NULL;

  for (i = 0; i < added->len; i++)
    {
      char *path = g_file_get_relative_path (to, g_ptr_array_index (added, i));
      g_ptr_array_add (changed_paths, path);
    }

  for (i = 0; i < modified->len; i++)
    {
      OstreeDiffItem *modified_item = g_ptr_array_index (modified, i);
      char *path = g_file_get_relative_path (to, modified_item->target);
      g_ptr_array_add (changed_paths, path);
    }

  for (i = 0; i < removed->len; i++)
    {
      char *path = g_file_get_relative_path (to, g_ptr_array_index (removed, i));
      g_ptr_array_add (changed_paths, path);
    }

  g_ptr_array_sort (changed_paths, cmpstringp);

  return g_steal_pointer (&changed_paths);
}

/* This returns removals too */
GPtrArray *
builder_cache_get_all_changes (BuilderCache *self,
                               GError      **error)
{
  g_autoptr(GFile) init_root = NULL;
  g_autoptr(GFile) finish_root = NULL;
  g_autofree char *init_commit = NULL;
  g_autofree char *finish_commit = NULL;
  g_autofree char *init_ref = get_ref (self, "init");
  g_autofree char *finish_ref = get_ref (self, "finish");

  if (!ostree_repo_resolve_rev (self->repo, init_ref, FALSE, &init_commit, NULL))
    return FALSE;

  if (!ostree_repo_resolve_rev (self->repo, finish_ref, FALSE, &finish_commit, NULL))
    return FALSE;

  if (!ostree_repo_read_commit (self->repo, init_commit, &init_root, NULL, NULL, error))
    return NULL;

  if (!ostree_repo_read_commit (self->repo, finish_commit, &finish_root, NULL, NULL, error))
    return NULL;

  return get_all_changes (self, init_root, finish_root, error);
}

static GPtrArray   *
builder_cache_get_changes_to (BuilderCache *self,
                              GFile        *current_root,
                              GPtrArray   **removals,
                              GError      **error)
{
  g_autoptr(GFile) parent_root = NULL;

  if (self->last_parent != NULL &&
      !ostree_repo_read_commit (self->repo, self->last_parent, &parent_root, NULL, NULL, error))
    return FALSE;

  return get_changes (self, parent_root, current_root, removals, error);
}

GPtrArray   *
builder_cache_get_changes (BuilderCache *self,
                           GError      **error)
{
  g_autoptr(GFile) current_root = NULL;
  g_autoptr(GFile) parent_root = NULL;
  g_autoptr(GVariant) variant = NULL;
  g_autoptr(GVariant) commit_metadata = NULL;
  g_autofree char *parent_commit = NULL;
  g_autoptr(GVariant) changesz_v = NULL;
  g_autoptr(GVariant) changes_v = NULL;

  if (!ostree_repo_read_commit (self->repo, self->last_parent, &current_root, NULL, NULL, error))
    return NULL;

  if (!ostree_repo_load_variant (self->repo, OSTREE_OBJECT_TYPE_COMMIT, self->last_parent,
                                 &variant, NULL))
    return NULL;

  commit_metadata = g_variant_get_child_value (variant, 0);
  changesz_v = g_variant_lookup_value (commit_metadata, "changesz", G_VARIANT_TYPE_BYTESTRING);

  if (changesz_v)
    changes_v = flatpak_variant_uncompress (changesz_v, G_VARIANT_TYPE ("as"));
  else
    changes_v = g_variant_lookup_value (commit_metadata, "changes", G_VARIANT_TYPE ("as"));

  if (changes_v)
    {
      g_autoptr(GPtrArray) changed_paths = g_ptr_array_new_with_free_func (g_free);
      int i;

      for (i = 0; i < g_variant_n_children (changes_v); i++)
        {
          char *str;
          g_variant_get_child (changes_v, i, "s", &str);
          g_ptr_array_add (changed_paths, str);
        }

      return g_steal_pointer (&changed_paths);
    }

  parent_commit = ostree_commit_get_parent (variant);
  if (parent_commit != NULL)
    {
      if (!ostree_repo_read_commit (self->repo, parent_commit, &parent_root, NULL, NULL, error))
        return FALSE;
    }

  return get_changes (self, parent_root, current_root, NULL, error);
}

GPtrArray   *
builder_cache_get_files (BuilderCache *self,
                         GError      **error)
{
  g_autoptr(GFile) current_root = NULL;

  if (!ostree_repo_read_commit (self->repo, self->last_parent, &current_root, NULL, NULL, error))
    return NULL;

  return get_changes (self, NULL, current_root, NULL, error);
}

void
builder_cache_disable_lookups (BuilderCache *self)
{
  self->disabled = TRUE;
}

gboolean
builder_gc (BuilderCache *self,
            gboolean      prune_unused_stages,
            GError      **error)
{
  gint objects_total;
  gint objects_pruned;
  guint64 pruned_object_size_total;
  GHashTableIter iter;
  gpointer key, value;

  if (prune_unused_stages)
    {
      g_hash_table_iter_init (&iter, self->unused_stages);
      while (g_hash_table_iter_next (&iter, &key, &value))
        {
          const char *unused_stage = (const char *) key;
          g_autofree char *unused_ref = get_ref (self, unused_stage);

          g_debug ("Removing unused ref %s", unused_ref);

          if (!ostree_repo_set_ref_immediate (self->repo,
                                              NULL,
                                              unused_ref,
                                              NULL,
                                              NULL, error))
            return FALSE;
        }
    }

  g_print ("Pruning cache\n");
  return ostree_repo_prune (self->repo,
                            OSTREE_REPO_PRUNE_FLAGS_REFS_ONLY, -1,
                            &objects_total,
                            &objects_pruned,
                            &pruned_object_size_total,
                            NULL, error);
}

/* Only add to cache if non-empty. This means we can add
   these things compatibly without invalidating the cache.
   This is useful if empty means no change from what was
   before */
void
builder_cache_checksum_compat_str (BuilderCache *self,
                                   const char   *str)
{
  if (str)
    builder_cache_checksum_str (self, str);
}

void
builder_cache_checksum_str (BuilderCache *self,
                            const char   *str)
{
  /* We include the terminating zero so that we make
   * a difference between NULL and "". */

  if (str)
    g_checksum_update (self->checksum, (const guchar *) str, strlen (str) + 1);
  else
    /* Always add something so we can't be fooled by a sequence like
       NULL, "a" turning into "a", NULL. */
    g_checksum_update (self->checksum, (const guchar *) "\1", 1);
}

/* Only add to cache if non-empty. This means we can add
   these things compatibly without invalidating the cache.
   This is useful if empty means no change from what was
   before */
void
builder_cache_checksum_compat_strv (BuilderCache *self,
                                    char        **strv)
{
  if (strv != NULL && strv[0] != NULL)
    builder_cache_checksum_strv (self, strv);
}


void
builder_cache_checksum_strv (BuilderCache *self,
                             char        **strv)
{
  int i;

  if (strv)
    {
      g_checksum_update (self->checksum, (const guchar *) "\1", 1);
      for (i = 0; strv[i] != NULL; i++)
        builder_cache_checksum_str (self, strv[i]);
    }
  else
    {
      g_checksum_update (self->checksum, (const guchar *) "\2", 1);
    }
}

void
builder_cache_checksum_boolean (BuilderCache *self,
                                gboolean      val)
{
  if (val)
    g_checksum_update (self->checksum, (const guchar *) "\1", 1);
  else
    g_checksum_update (self->checksum, (const guchar *) "\0", 1);
}

/* Only add to cache if true. This means we can add
   these things compatibly without invalidating the cache.
   This is useful if false means no change from what was
   before */
void
builder_cache_checksum_compat_boolean (BuilderCache *self,
                                       gboolean      val)
{
  if (val)
    builder_cache_checksum_boolean (self, val);
}

void
builder_cache_checksum_uint32 (BuilderCache *self,
                               guint32       val)
{
  guchar v[4];

  v[0] = (val >> 0) & 0xff;
  v[1] = (val >> 8) & 0xff;
  v[2] = (val >> 16) & 0xff;
  v[3] = (val >> 24) & 0xff;
  g_checksum_update (self->checksum, v, 4);
}

void
builder_cache_checksum_random (BuilderCache *self)
{
  builder_cache_checksum_uint32 (self, g_random_int ());
  builder_cache_checksum_uint32 (self, g_random_int ());
}

void
builder_cache_checksum_uint64 (BuilderCache *self,
                               guint64       val)
{
  guchar v[8];

  v[0] = (val >> 0) & 0xff;
  v[1] = (val >> 8) & 0xff;
  v[2] = (val >> 16) & 0xff;
  v[3] = (val >> 24) & 0xff;
  v[4] = (val >> 32) & 0xff;
  v[5] = (val >> 40) & 0xff;
  v[6] = (val >> 48) & 0xff;
  v[7] = (val >> 56) & 0xff;

  g_checksum_update (self->checksum, v, 8);
}

void
builder_cache_checksum_data (BuilderCache *self,
                             guint8       *data,
                             gsize         len)
{
  g_checksum_update (self->checksum, data, len);
}