[gnome-builder/wip/chergert/pipeline] wip: start on build pipeline/stages



commit 09e91f32f5c96d0c44f1ad4b8029148eb4324bb3
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jan 9 16:39:32 2017 -0800

    wip: start on build pipeline/stages

 libide/Makefile.am                                 |   20 +
 libide/application/ide-application-private.h       |    2 +
 libide/application/ide-application.c               |   24 +
 libide/application/ide-application.h               |    3 +
 libide/buildsystem/ide-build-log-private.h         |   47 +
 libide/buildsystem/ide-build-log.c                 |  242 +++++
 .../buildsystem/ide-build-log.h                    |   23 +-
 libide/buildsystem/ide-build-pipeline-addin.c      |  109 ++
 libide/buildsystem/ide-build-pipeline-addin.h      |   51 +
 libide/buildsystem/ide-build-pipeline.c            | 1112 ++++++++++++++++++++
 libide/buildsystem/ide-build-pipeline.h            |  102 ++
 libide/buildsystem/ide-build-stage-launcher.c      |  277 +++++
 libide/buildsystem/ide-build-stage-launcher.h      |   41 +
 libide/buildsystem/ide-build-stage-mkdirs.c        |  146 +++
 libide/buildsystem/ide-build-stage-mkdirs.h        |   48 +
 libide/buildsystem/ide-build-stage-private.h       |   38 +
 libide/buildsystem/ide-build-stage-transfer.c      |  183 ++++
 .../buildsystem/ide-build-stage-transfer.h         |   19 +-
 libide/buildsystem/ide-build-stage.c               |  764 ++++++++++++++
 libide/buildsystem/ide-build-stage.h               |  149 +++
 libide/buildsystem/ide-configuration.c             |   31 +
 libide/buildsystem/ide-configuration.h             |    7 +-
 libide/ide-enums.c.in                              |    2 +
 libide/ide-types.h                                 |    5 +-
 libide/ide.h                                       |    7 +
 libide/subprocess/ide-subprocess-launcher.c        |   16 +-
 libide/transfers/ide-transfer-manager.c            |  141 +++-
 libide/transfers/ide-transfer-manager.h            |   30 +-
 libide/transfers/ide-transfer.c                    |    3 +
 libide/transfers/ide-transfer.h                    |    1 +
 libide/util/ide-directory-reaper.c                 |  353 +++++++
 libide/util/ide-directory-reaper.h                 |   54 +
 plugins/autotools/Makefile.am                      |    4 +
 plugins/autotools/autotools-plugin.c               |    2 +
 .../autotools/ide-autotools-application-addin.c    |    4 +
 plugins/autotools/ide-autotools-autogen-stage.c    |  179 ++++
 .../ide-autotools-autogen-stage.h}                 |   14 +-
 plugins/autotools/ide-autotools-pipeline-addin.c   |  259 +++++
 .../ide-autotools-pipeline-addin.h}                |   14 +-
 plugins/flatpak/Makefile.am                        |   24 +-
 plugins/flatpak/gbp-flatpak-pipeline-addin.c       |  485 +++++++++
 .../gbp-flatpak-pipeline-addin.h}                  |   14 +-
 plugins/flatpak/gbp-flatpak-runtime.c              |   70 ++
 plugins/flatpak/gbp-flatpak-transfer.c             |  477 +++++++++
 .../gbp-flatpak-transfer.h}                        |   20 +-
 plugins/flatpak/gbp-flatpak-util.c                 |   69 ++
 .../gbp-flatpak-util.h}                            |   15 +-
 plugins/gcc/Makefile.am                            |    4 +-
 ...ild-result-addin.c => gbp-gcc-pipeline-addin.c} |  115 +-
 ...ild-result-addin.h => gbp-gcc-pipeline-addin.h} |   14 +-
 plugins/gcc/gbp-gcc-plugin.c                       |    6 +-
 plugins/todo/todo_plugin/__init__.py               |    5 +-
 tests/Makefile.am                                  |    6 +
 tests/data/project1/.gitignore                     |    1 +
 tests/test-ide-build-pipeline.c                    |  117 ++
 55 files changed, 5814 insertions(+), 154 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index d385eb5..ba06cd4 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -32,7 +32,14 @@ libide_1_0_la_public_headers =                            \
        buffers/ide-unsaved-files.h                       \
        buildsystem/ide-build-command.h                   \
        buildsystem/ide-build-command-queue.h             \
+       buildsystem/ide-build-log.h                       \
        buildsystem/ide-build-manager.h                   \
+       buildsystem/ide-build-pipeline.h                  \
+       buildsystem/ide-build-pipeline-addin.h            \
+       buildsystem/ide-build-stage.h                     \
+       buildsystem/ide-build-stage-launcher.h            \
+       buildsystem/ide-build-stage-mkdirs.h              \
+       buildsystem/ide-build-stage-transfer.h            \
        buildsystem/ide-build-result-addin.h              \
        buildsystem/ide-build-result.h                    \
        buildsystem/ide-build-system.h                    \
@@ -159,6 +166,7 @@ libide_1_0_la_public_headers =                            \
        tree/ide-tree.h                                   \
        util/ide-cairo.h                                  \
        util/ide-dnd.h                                    \
+       util/ide-directory-reaper.h                       \
        util/ide-file-manager.h                           \
        util/ide-flatpak.h                                \
        util/ide-glib.h                                   \
@@ -203,6 +211,12 @@ libide_1_0_la_public_sources =                            \
        buildsystem/ide-build-command.c                   \
        buildsystem/ide-build-command-queue.c             \
        buildsystem/ide-build-manager.c                   \
+       buildsystem/ide-build-pipeline.c                  \
+       buildsystem/ide-build-pipeline-addin.c            \
+       buildsystem/ide-build-stage.c                     \
+       buildsystem/ide-build-stage-launcher.c            \
+       buildsystem/ide-build-stage-mkdirs.c              \
+       buildsystem/ide-build-stage-transfer.c            \
        buildsystem/ide-build-result-addin.c              \
        buildsystem/ide-build-result.c                    \
        buildsystem/ide-build-system.c                    \
@@ -334,6 +348,7 @@ libide_1_0_la_public_sources =                            \
        tree/ide-tree.c                                   \
        util/ide-cairo.c                                  \
        util/ide-dnd.c                                    \
+       util/ide-directory-reaper.c                       \
        util/ide-file-manager.c                           \
        util/ide-flatpak.c                                \
        util/ide-glib.c                                   \
@@ -374,6 +389,9 @@ libide_1_0_la_SOURCES =                                   \
        application/ide-application-private.h             \
        application/ide-application-tests.c               \
        application/ide-application-tests.h               \
+       buildsystem/ide-build-log.c                       \
+       buildsystem/ide-build-log-private.h               \
+       buildsystem/ide-build-stage-private.h             \
        editor/ide-editor-frame-actions.c                 \
        editor/ide-editor-frame-actions.h                 \
        editor/ide-editor-frame-private.h                 \
@@ -612,6 +630,8 @@ endif
 
 glib_enum_headers =                        \
        buffers/ide-buffer.h               \
+       buildsystem/ide-build-log.h        \
+       buildsystem/ide-build-pipeline.h   \
        buildsystem/ide-build-result.h     \
        devices/ide-device.h               \
        diagnostics/ide-diagnostic.h       \
diff --git a/libide/application/ide-application-private.h b/libide/application/ide-application-private.h
index 7dcaa10..71ddd04 100644
--- a/libide/application/ide-application-private.h
+++ b/libide/application/ide-application-private.h
@@ -64,6 +64,8 @@ struct _IdeApplication
 
   GHashTable          *plugin_settings;
 
+  GPtrArray           *reapers;
+
   guint                disable_theme_tracking : 1;
 };
 
diff --git a/libide/application/ide-application.c b/libide/application/ide-application.c
index 0ae4e2e..6be3577 100644
--- a/libide/application/ide-application.c
+++ b/libide/application/ide-application.c
@@ -427,6 +427,17 @@ ide_application_shutdown (GApplication *application)
 
   if (G_APPLICATION_CLASS (ide_application_parent_class)->shutdown)
     G_APPLICATION_CLASS (ide_application_parent_class)->shutdown (application);
+
+  /* Run all reapers serially on shutdown */
+
+  for (guint i = 0; i < self->reapers->len; i++)
+    {
+      IdeDirectoryReaper *reaper = g_ptr_array_index (self->reapers, i);
+
+      g_assert (IDE_IS_DIRECTORY_REAPER (reaper));
+
+      ide_directory_reaper_execute (reaper, NULL, NULL);
+    }
 }
 
 static void
@@ -469,6 +480,7 @@ ide_application_finalize (GObject *object)
   g_clear_pointer (&self->merge_ids, g_hash_table_unref);
   g_clear_pointer (&self->plugin_css, g_hash_table_unref);
   g_clear_pointer (&self->plugin_settings, g_hash_table_unref);
+  g_clear_pointer (&self->reapers, g_ptr_array_unref);
   g_clear_object (&self->worker_manager);
   g_clear_object (&self->keybindings);
   g_clear_object (&self->recent_projects);
@@ -504,6 +516,8 @@ ide_application_init (IdeApplication *self)
 {
   ide_set_program_name (PACKAGE_NAME);
 
+  self->reapers = g_ptr_array_new_with_free_func (g_object_unref);
+
   self->started_at = g_date_time_new_now_utc ();
   self->mode = IDE_APPLICATION_MODE_PRIMARY;
 
@@ -826,3 +840,13 @@ ide_application_get_main_thread (void)
 {
   return main_thread;
 }
+
+void
+ide_application_add_reaper (IdeApplication     *self,
+                            IdeDirectoryReaper *reaper)
+{
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (reaper));
+
+  g_ptr_array_add (self->reapers, g_object_ref (reaper));
+}
diff --git a/libide/application/ide-application.h b/libide/application/ide-application.h
index 607ee0a..76edede 100644
--- a/libide/application/ide-application.h
+++ b/libide/application/ide-application.h
@@ -22,6 +22,7 @@
 #include <gtk/gtk.h>
 
 #include "projects/ide-recent-projects.h"
+#include "util/ide-directory-reaper.h"
 
 G_BEGIN_DECLS
 
@@ -58,6 +59,8 @@ GMenu              *ide_application_get_menu_by_id       (IdeApplication       *
                                                           const gchar          *id);
 gboolean            ide_application_open_project         (IdeApplication       *self,
                                                           GFile                *file);
+void                ide_application_add_reaper           (IdeApplication       *self,
+                                                          IdeDirectoryReaper   *reaper);
 
 G_END_DECLS
 
diff --git a/libide/buildsystem/ide-build-log-private.h b/libide/buildsystem/ide-build-log-private.h
new file mode 100644
index 0000000..83e9866
--- /dev/null
+++ b/libide/buildsystem/ide-build-log-private.h
@@ -0,0 +1,47 @@
+/* ide-build-log-private.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_LOG_PRIVATE_H
+#define IDE_BUILD_LOG_PRIVATE_H
+
+#include <gio/gio.h>
+
+#include "ide-build-log.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_LOG (ide_build_log_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildLog, ide_build_log, IDE, BUILD_LOG, GObject)
+
+IdeBuildLog *ide_build_log_new              (void);
+void         ide_build_log_observer         (IdeBuildLogStream    stream,
+                                             const gchar         *message,
+                                             gssize               message_len,
+                                             gpointer             user_data);
+guint        ide_build_log_add_observer     (IdeBuildLog         *self,
+                                             IdeBuildLogObserver  observer,
+                                             gpointer             observer_data,
+                                             GDestroyNotify       observer_data_destroy);
+gboolean     ide_build_log_remove_observer  (IdeBuildLog         *self,
+                                             guint                observer_id);
+
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_LOG_PRIVATE_H */
diff --git a/libide/buildsystem/ide-build-log.c b/libide/buildsystem/ide-build-log.c
new file mode 100644
index 0000000..eccceae
--- /dev/null
+++ b/libide/buildsystem/ide-build-log.c
@@ -0,0 +1,242 @@
+/* ide-build-log.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-log"
+
+#include <string.h>
+
+#include "application/ide-application.h"
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-log-private.h"
+
+#define POINTER_MARK(p)   GSIZE_TO_POINTER(GPOINTER_TO_SIZE(p)|1)
+#define POINTER_UNMARK(p) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(p)&~(gsize)1)
+#define POINTER_MARKED(p) (GPOINTER_TO_SIZE(p)&1)
+#define DISPATCH_MAX      20
+
+struct _IdeBuildLog
+{
+  GObject      parent_instance;
+
+  GArray      *observers;
+  GAsyncQueue *log_queue;
+  GSource     *log_source;
+
+  guint        sequence;
+};
+
+typedef struct
+{
+  IdeBuildLogObserver callback;
+  gpointer            data;
+  GDestroyNotify      destroy;
+  guint               id;
+} Observer;
+
+G_DEFINE_TYPE (IdeBuildLog, ide_build_log, G_TYPE_OBJECT)
+
+static gboolean
+emit_log_from_main (gpointer user_data)
+{
+  IdeBuildLog *self = user_data;
+  g_autoptr(GPtrArray) ar = g_ptr_array_new ();
+  gpointer item;
+
+  g_assert (IDE_IS_BUILD_LOG (self));
+
+  /*
+   * Pull up to DISPATCH_MAX items from the log queue. We have an upper
+   * bound here so that we don't stall the main loop. Additionally, we
+   * update the ready-time when we run out of items while holding the
+   * async queue lock to synchronize with the caller for further wakeups.
+   */
+  g_async_queue_lock (self->log_queue);
+  for (guint i = 0; i < DISPATCH_MAX; i++)
+    {
+      if (NULL == (item = g_async_queue_try_pop_unlocked (self->log_queue)))
+        {
+          g_source_set_ready_time (self->log_source, -1);
+          break;
+        }
+      g_ptr_array_add (ar, item);
+    }
+  g_async_queue_unlock (self->log_queue);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      IdeBuildLogStream stream = IDE_BUILD_LOG_STDOUT;
+      gchar *message;
+      gsize message_len;
+
+      item = g_ptr_array_index (ar, i);
+      message = POINTER_UNMARK (item);
+      message_len = strlen (message);
+
+      if (POINTER_MARKED (item))
+        stream = IDE_BUILD_LOG_STDERR;
+
+      for (guint j = 0; j < self->observers->len; j++)
+        {
+          const Observer *observer = &g_array_index (self->observers, Observer, j);
+
+          observer->callback (stream, message, message_len, observer->data);
+        }
+
+      g_free (message);
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_build_log_finalize (GObject *object)
+{
+  IdeBuildLog *self = (IdeBuildLog *)object;
+
+  g_clear_pointer (&self->log_queue, g_async_queue_unref);
+  g_clear_pointer (&self->log_source, g_source_destroy);
+  g_clear_pointer (&self->observers, g_array_unref);
+
+  G_OBJECT_CLASS (ide_build_log_parent_class)->finalize (object);
+}
+
+static void
+ide_build_log_class_init (IdeBuildLogClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_log_finalize;
+}
+
+static void
+ide_build_log_init (IdeBuildLog *self)
+{
+  self->observers = g_array_new (FALSE, FALSE, sizeof (Observer));
+
+  self->log_queue = g_async_queue_new ();
+
+  self->log_source = g_timeout_source_new (G_MAXINT);
+  g_source_set_ready_time (self->log_source, -1);
+  g_source_set_name (self->log_source, "[ide] IdeBuildLog");
+  g_source_set_callback (self->log_source, emit_log_from_main, self, NULL);
+  g_source_attach (self->log_source, g_main_context_default ());
+}
+
+static void
+ide_build_log_via_main (IdeBuildLog       *self,
+                        IdeBuildLogStream  stream,
+                        const gchar       *message,
+                        gsize              message_len)
+{
+  gchar *copied = g_strndup (message, message_len);
+
+  if G_UNLIKELY (stream == IDE_BUILD_LOG_STDERR)
+    copied = POINTER_MARK (copied);
+
+  /*
+   * Add the log entry to our queue to be dispatched in the main thread.
+   * However, we hold the async queue lock while updating the source ready
+   * time so we are synchronized with the main thread for setting the
+   * ready time. This is needed because the main thread may not dispatch
+   * all available items in a single dispatch (to avoid stalling the
+   * main loop).
+   */
+
+  g_async_queue_lock (self->log_queue);
+  g_async_queue_push_unlocked (self->log_queue, copied);
+  g_source_set_ready_time (self->log_source, 0);
+  g_async_queue_unlock (self->log_queue);
+}
+
+void
+ide_build_log_observer (IdeBuildLogStream  stream,
+                        const gchar       *message,
+                        gssize             message_len,
+                        gpointer           user_data)
+{
+  IdeBuildLog *self = user_data;
+
+  g_assert (message != NULL);
+
+  if (message_len < 0)
+    message_len = strlen (message);
+
+  g_assert (message[message_len] == '\0');
+
+  if G_LIKELY (IDE_IS_MAIN_THREAD ())
+    {
+      for (guint i = 0; i < self->observers->len; i++)
+        {
+          const Observer *observer = &g_array_index (self->observers, Observer, i);
+
+          observer->callback (stream, message, message_len, observer->data);
+        }
+    }
+  else
+    {
+      ide_build_log_via_main (self, stream, message, message_len);
+    }
+}
+
+guint
+ide_build_log_add_observer (IdeBuildLog         *self,
+                            IdeBuildLogObserver  observer,
+                            gpointer             observer_data,
+                            GDestroyNotify       observer_data_destroy)
+{
+  Observer ele;
+
+  g_return_val_if_fail (IDE_IS_BUILD_LOG (self), 0);
+  g_return_val_if_fail (observer != NULL, 0);
+
+  ele.id = ++self->sequence;
+  ele.callback = observer;
+  ele.data = observer_data;
+  ele.destroy = observer_data_destroy;
+
+  g_array_append_val (self->observers, ele);
+
+  return ele.id;
+}
+
+gboolean
+ide_build_log_remove_observer (IdeBuildLog *self,
+                               guint        observer_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_LOG (self), FALSE);
+  g_return_val_if_fail (observer_id > 0, FALSE);
+
+  for (guint i = 0; i < self->observers->len; i++)
+    {
+      const Observer *observer = &g_array_index (self->observers, Observer, i);
+
+      if (observer->id == observer_id)
+        {
+          g_array_remove_index (self->observers, i);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+IdeBuildLog *
+ide_build_log_new (void)
+{
+  return g_object_new (IDE_TYPE_BUILD_LOG, NULL);
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/libide/buildsystem/ide-build-log.h
similarity index 56%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to libide/buildsystem/ide-build-log.h
index 2a77a0b..a6c807d 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/libide/buildsystem/ide-build-log.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* ide-build-log.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef IDE_BUILD_LOG_H
+#define IDE_BUILD_LOG_H
 
-#include <ide.h>
+#include <glib.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+typedef enum
+{
+  IDE_BUILD_LOG_STDOUT,
+  IDE_BUILD_LOG_STDERR,
+} IdeBuildLogStream;
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+typedef void (*IdeBuildLogObserver) (IdeBuildLogStream  log_stream,
+                                     const gchar       *message,
+                                     gssize             message_len,
+                                     gpointer           user_data);
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* IDE_BUILD_LOG_H */
diff --git a/libide/buildsystem/ide-build-pipeline-addin.c b/libide/buildsystem/ide-build-pipeline-addin.c
new file mode 100644
index 0000000..df9ca67
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline-addin.c
@@ -0,0 +1,109 @@
+/* ide-build-pipeline-addin.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-pipeline-addin"
+
+#include "ide-context.h"
+
+#include "buildsystem/ide-build-pipeline-addin.h"
+
+G_DEFINE_INTERFACE (IdeBuildPipelineAddin, ide_build_pipeline_addin, G_TYPE_OBJECT)
+
+static void
+ide_build_pipeline_addin_default_init (IdeBuildPipelineAddinInterface *iface)
+{
+  g_object_interface_install_property (iface,
+                                       g_param_spec_object ("context",
+                                                            NULL,
+                                                            NULL,
+                                                            IDE_TYPE_CONTEXT,
+                                                            (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS)));
+}
+
+void
+ide_build_pipeline_addin_load (IdeBuildPipelineAddin *self,
+                               IdeBuildPipeline      *pipeline)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->load)
+    IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->load (self, pipeline);
+}
+
+void
+ide_build_pipeline_addin_unload (IdeBuildPipelineAddin *self,
+                                 IdeBuildPipeline      *pipeline)
+{
+  GArray *ar;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->unload)
+    IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->unload (self, pipeline);
+
+  /* Unload any stages that are tracked by the addin */
+  ar = g_object_get_data (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES");
+
+  if G_LIKELY (ar != NULL)
+    {
+      for (guint i = 0; i < ar->len; i++)
+        {
+          guint stage_id = g_array_index (ar, guint, i);
+
+          ide_build_pipeline_disconnect (pipeline, stage_id);
+        }
+    }
+}
+
+/**
+ * ide_build_pipeline_addin_track:
+ * @self: An #IdeBuildPipelineAddin
+ * @stage_id: a stage id returned from ide_build_pipeline_connect()
+ *
+ * This function will track the stage_id that was returned from
+ * ide_build_pipeline_connect() or similar functions. Doing so results in
+ * the stage being automatically disconnected when the addin is unloaded.
+ *
+ * This means that many #IdeBuildPipelineAddin implementations do not need
+ * an unload vfunc if they track all registered stages.
+ *
+ * You should not mix this function with manual pipeline disconnections.
+ * While it should work, that is not yet guaranteed.
+ */
+void
+ide_build_pipeline_addin_track (IdeBuildPipelineAddin *self,
+                                guint                  stage_id)
+{
+  GArray *ar;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (stage_id > 0);
+
+  ar = g_object_get_data (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES");
+
+  if (ar == NULL)
+    {
+      ar = g_array_new (FALSE, FALSE, sizeof (guint));
+      g_object_set_data_full (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES",
+                              ar, (GDestroyNotify)g_array_unref);
+    }
+
+  g_array_append_val (ar, stage_id);
+}
diff --git a/libide/buildsystem/ide-build-pipeline-addin.h b/libide/buildsystem/ide-build-pipeline-addin.h
new file mode 100644
index 0000000..b182ddd
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline-addin.h
@@ -0,0 +1,51 @@
+/* ide-build-pipeline-addin.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_PIPELINE_ADDIN_H
+#define IDE_BUILD_PIPELINE_ADDIN_H
+
+#include <gio/gio.h>
+
+#include "ide-build-pipeline.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_PIPELINE_ADDIN (ide_build_pipeline_addin_get_type())
+
+G_DECLARE_INTERFACE (IdeBuildPipelineAddin, ide_build_pipeline_addin, IDE, BUILD_PIPELINE_ADDIN, GObject)
+
+struct _IdeBuildPipelineAddinInterface
+{
+  GTypeInterface type_interface;
+
+  void (*load)   (IdeBuildPipelineAddin *self,
+                  IdeBuildPipeline      *pipeline);
+  void (*unload) (IdeBuildPipelineAddin *self,
+                  IdeBuildPipeline      *pipeline);
+};
+
+void ide_build_pipeline_addin_load   (IdeBuildPipelineAddin *self,
+                                      IdeBuildPipeline      *pipeline);
+void ide_build_pipeline_addin_unload (IdeBuildPipelineAddin *self,
+                                      IdeBuildPipeline      *pipeline);
+void ide_build_pipeline_addin_track  (IdeBuildPipelineAddin *self,
+                                      guint                  stage_id);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_PIPELINE_ADDIN_H */
diff --git a/libide/buildsystem/ide-build-pipeline.c b/libide/buildsystem/ide-build-pipeline.c
new file mode 100644
index 0000000..1fd4b74
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline.c
@@ -0,0 +1,1112 @@
+/* ide-build-pipeline.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-pipeline"
+
+#include <libpeas/peas.h>
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-enums.h"
+
+#include "application/ide-application.h"
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-log-private.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-pipeline-addin.h"
+#include "buildsystem/ide-build-stage-launcher.h"
+#include "diagnostics/ide-diagnostic.h"
+#include "projects/ide-project.h"
+#include "runtimes/ide-runtime.h"
+#include "vcs/ide-vcs.h"
+
+/**
+ * SECTION:ide-build-pipeline
+ * @title: IdeBuildPipeline
+ * @short_description: Pluggable build pipeline
+ *
+ * The #IdeBuildPipeline is responsible for managing the build process
+ * for Builder. It consists of multiple build "phases" (see #IdeBuildPhase
+ * for the individual phases). An #IdeBuildStage can be attached with
+ * a priority to each phase and is the primary mechanism that plugins
+ * use to perform their operations in the proper ordering.
+ *
+ * For example, the flatpak plugin provides its download stage as part of the
+ * %IDE_BUILD_PHASE_DOWNLOAD phase. The autotools plugin provides stages to
+ * phases such as %IDE_BUILD_PHASE_AUTOGEN, %IDE_BUILD_PHASE_CONFIGURE,
+ * %IDE_BUILD_PHASE_BUILD, and %IDE_BUILD_PHASE_INSTALL.
+ *
+ * If you want ensure a particular phase is performed as part of a build,
+ * then fall ide_build_pipeline_request_phase() with the phase you are
+ * interested in seeing complete successfully.
+ *
+ * If your plugin has discovered that something has changed that invalidates a
+ * given phase, use ide_build_pipeline_invalidate_phase() to ensure that the
+ * phase is re-executed the next time a requested phase of higher precidence
+ * is requested.
+ *
+ * It can be useful to perform operations before or after a given stage (but
+ * still be executed as part of that stage) so the %IDE_BUILD_PHASE_BEFORE and
+ * %IDE_BUILD_PHASE_AFTER flags may be xor'd with the requested phase.  If more
+ * precise ordering is required, you may use the priority parameter to order
+ * the operation with regards to other stages in that phase.
+ *
+ * Transient stages may be added to the pipeline and they will be removed after
+ * the ide_build_pipeline_execute_async() operation has completed successfully
+ * or has failed. You can mark a stage as trandient with
+ * ide_build_stage_set_transient(). This may be useful to perform operations
+ * such as an "export tarball" stage which should only run once as determined
+ * by the user requesting a "make dist" style operation.
+ */
+
+typedef struct
+{
+  guint          id;
+  IdeBuildPhase  phase;
+  gint           priority;
+  IdeBuildStage *stage;
+} PipelineEntry;
+
+struct _IdeBuildPipeline
+{
+  IdeObject         parent_instance;
+  PeasExtensionSet *addins;
+  IdeConfiguration *configuration;
+  IdeBuildLog      *log;
+  gchar            *builddir;
+  gchar            *srcdir;
+  GArray           *pipeline;
+  gint              position;
+  IdeBuildPhase     requested_mask;
+  guint             seqnum;
+  guint             failed : 1;
+};
+
+static void ide_build_pipeline_tick (IdeBuildPipeline *self,
+                                     GTask            *task);
+
+G_DEFINE_TYPE (IdeBuildPipeline, ide_build_pipeline, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CONFIGURATION,
+  PROP_PHASE,
+  N_PROPS
+};
+
+enum {
+  DIAGNOSTIC,
+  STARTED,
+  FINISHED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_build_pipeline_release_transients (IdeBuildPipeline *self)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (self->pipeline != NULL);
+
+  for (guint i = self->pipeline->len; i > 0; i--)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i - 1);
+
+      g_assert (IDE_IS_BUILD_STAGE (entry->stage));
+
+      if (ide_build_stage_get_transient (entry->stage))
+        g_array_remove_index (self->pipeline, i);
+    }
+}
+
+/**
+ * ide_build_pipeline_get_phase:
+ *
+ * Gets the current phase that is executing. This is only useful during
+ * execution of the pipeline.
+ */
+IdeBuildPhase
+ide_build_pipeline_get_phase (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+
+  if (self->position < 0)
+    return IDE_BUILD_PHASE_NONE;
+  else if (self->position < self->pipeline->len)
+    return g_array_index (self->pipeline, PipelineEntry, self->position).phase;
+  else if (self->failed)
+    return IDE_BUILD_PHASE_FAILED;
+  else
+    return IDE_BUILD_PHASE_FINISHED;
+}
+
+/**
+ * ide_build_pipeline_get_configuration:
+ *
+ * Gets the #IdeConfiguration to use for the pipeline.
+ *
+ * Returns: (transfer none): An #IdeConfiguration
+ */
+IdeConfiguration *
+ide_build_pipeline_get_configuration (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->configuration;
+}
+
+static void
+clear_pipeline_entry (gpointer data)
+{
+  PipelineEntry *entry = data;
+
+  g_clear_object (&entry->stage);
+}
+
+static gint
+pipeline_entry_compare (gconstpointer a,
+                        gconstpointer b)
+{
+  const PipelineEntry *entry_a = a;
+  const PipelineEntry *entry_b = b;
+  gint ret;
+
+  ret = (gint)(entry_a->phase & IDE_BUILD_PHASE_MASK)
+      - (gint)(entry_b->phase & IDE_BUILD_PHASE_MASK);
+
+  if (ret == 0)
+    {
+      gint whence_a = (entry_a->phase & IDE_BUILD_PHASE_WHENCE_MASK);
+      gint whence_b = (entry_b->phase & IDE_BUILD_PHASE_WHENCE_MASK);
+
+      if (whence_a != whence_b)
+        {
+          if (whence_a == IDE_BUILD_PHASE_BEFORE)
+            return -1;
+
+          if (whence_b == IDE_BUILD_PHASE_BEFORE)
+            return 1;
+
+          if (whence_a == 0)
+            return -1;
+
+          if (whence_b == 0)
+            return 1;
+
+          g_assert_not_reached ();
+        }
+    }
+
+  if (ret == 0)
+    ret = entry_a->priority - entry_b->priority;
+
+  return ret;
+}
+
+static void
+ide_build_pipeline_real_started (IdeBuildPipeline *self)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+}
+
+static void
+ide_build_pipeline_real_finished (IdeBuildPipeline *self,
+                                  gboolean          failed)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /*
+   * Now that the build is finished, we can aggressively drop our pipeline
+   * stages to help ensure that all references are dropped as soon as
+   * possible.
+   */
+
+  g_clear_object (&self->addins);
+}
+
+static void
+ide_build_pipeline_extension_added (PeasExtensionSet *set,
+                                    PeasPluginInfo   *plugin_info,
+                                    PeasExtension    *exten,
+                                    gpointer          user_data)
+{
+  IdeBuildPipeline *self = user_data;
+  IdeBuildPipelineAddin *addin = (IdeBuildPipelineAddin *)exten;
+
+  IDE_ENTRY;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  ide_build_pipeline_addin_load (addin, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_extension_removed (PeasExtensionSet *set,
+                                      PeasPluginInfo   *plugin_info,
+                                      PeasExtension    *exten,
+                                      gpointer          user_data)
+{
+  IdeBuildPipeline *self = user_data;
+  IdeBuildPipelineAddin *addin = (IdeBuildPipelineAddin *)exten;
+
+  IDE_ENTRY;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  ide_build_pipeline_addin_unload (addin, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_finalize (GObject *object)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)object;
+
+  g_clear_object (&self->log);
+  g_clear_object (&self->configuration);
+  g_clear_pointer (&self->pipeline, g_array_unref);
+  g_clear_pointer (&self->srcdir, g_free);
+  g_clear_pointer (&self->builddir, g_free);
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->finalize (object);
+}
+
+static void
+ide_build_pipeline_dispose (GObject *object)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  g_clear_object (&self->addins);
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->dispose (object);
+}
+
+static void
+ide_build_pipeline_constructed (GObject *object)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+  g_autofree gchar *builddir = NULL;
+  const gchar *config_id;
+  const gchar *project_id;
+  IdeProject *project;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->constructed (object);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  project = ide_context_get_project (context);
+  project_id = ide_project_get_id (project);
+
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+
+  config_id = ide_configuration_get_id (self->configuration);
+
+  self->srcdir = g_file_get_path (workdir);
+
+  self->builddir = g_build_filename (g_get_user_cache_dir (),
+                                     "gnome-builder",
+                                     "builds",
+                                     project_id,
+                                     config_id,
+                                     NULL);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                         "context", context,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_build_pipeline_extension_added),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_build_pipeline_extension_removed),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_build_pipeline_extension_added,
+                              self);
+}
+
+static void
+ide_build_pipeline_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIGURATION:
+      g_value_set_object (value, ide_build_pipeline_get_configuration (self));
+      break;
+
+    case PROP_PHASE:
+      g_value_set_flags (value, ide_build_pipeline_get_phase (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_pipeline_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIGURATION:
+      self->configuration = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_pipeline_class_init (IdeBuildPipelineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_build_pipeline_constructed;
+  object_class->dispose = ide_build_pipeline_dispose;
+  object_class->finalize = ide_build_pipeline_finalize;
+  object_class->get_property = ide_build_pipeline_get_property;
+  object_class->set_property = ide_build_pipeline_set_property;
+
+  /**
+   * IdeBuildPipeline:configuration:
+   *
+   * The configuration to use for the build pipeline.
+   */
+  properties [PROP_CONFIGURATION] =
+    g_param_spec_object ("configuration",
+                         "Configuration",
+                         "Configuration",
+                         IDE_TYPE_CONFIGURATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildPipeline:phase:
+   *
+   * The current build phase during execution of the pipeline.
+   */
+  properties [PROP_PHASE] =
+    g_param_spec_flags ("phase",
+                        "Phase",
+                        "The phase that is being executed",
+                        IDE_TYPE_BUILD_PHASE,
+                        IDE_BUILD_PHASE_NONE,
+                        (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuildPipeline::diagnostic:
+   * @self: An #IdeBuildPipeline
+   * @diagnostic: The newly created diagnostic
+   *
+   * This signal is emitted when a plugin has detected a diagnostic while
+   * building the pipeline.
+   */
+  signals [DIAGNOSTIC] =
+    g_signal_new_class_handler ("diagnostic",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_DIAGNOSTIC);
+
+  /**
+   * IdeBuildPipeline::started:
+   * @self: An #IdeBuildPipeline
+   *
+   * This signal is emitted when the pipeline has started executing in
+   * response to ide_build_pipeline_execute_async() being called.
+   */
+  signals [STARTED] =
+    g_signal_new_class_handler ("started",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_pipeline_real_started),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * IdeBuildPipeline::finished:
+   * @self: An #IdeBuildPipeline
+   * @failed: If the build was a failure
+   *
+   * This signal is emitted when the build process has finished executing.
+   * If the build failed to complete all requested stages, then @failed will
+   * be set to %TRUE, otherwise %FALSE.
+   */
+  signals [FINISHED] =
+    g_signal_new_class_handler ("finished",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_pipeline_real_finished),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+static void
+ide_build_pipeline_init (IdeBuildPipeline *self)
+{
+  self->position = -1;
+
+  self->pipeline = g_array_new (FALSE, FALSE, sizeof (PipelineEntry));
+  g_array_set_clear_func (self->pipeline, clear_pipeline_entry);
+
+  self->log = ide_build_log_new ();
+}
+
+static void
+ide_build_pipeline_stage_execute_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeBuildStage *stage = (IdeBuildStage *)object;
+  IdeBuildPipeline *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  if (!ide_build_stage_execute_finish (stage, result, &error))
+    {
+      self->failed = TRUE;
+      g_task_return_error (task, g_steal_pointer (&error));
+      ide_build_pipeline_release_transients (self);
+      return;
+    }
+
+  ide_build_stage_set_completed (stage, TRUE);
+
+  ide_build_pipeline_tick (self, task);
+}
+
+static void
+ide_build_pipeline_tick (IdeBuildPipeline *self,
+                         GTask            *task)
+{
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+  cancellable = g_task_get_cancellable (task);
+
+  for (self->position++; self->position < self->pipeline->len; self->position++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, self->position);
+
+      g_assert (entry->stage != NULL);
+      g_assert (IDE_IS_BUILD_STAGE (entry->stage));
+
+      if (ide_build_stage_get_completed (entry->stage))
+        continue;
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) & self->requested_mask)
+        {
+          ide_build_stage_execute_async (entry->stage,
+                                         self,
+                                         cancellable,
+                                         ide_build_pipeline_stage_execute_cb,
+                                         g_object_ref (task));
+          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PHASE]);
+          IDE_EXIT;
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+
+  ide_build_pipeline_release_transients (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_notify_completed (IdeBuildPipeline *self,
+                                     GParamSpec       *pspec,
+                                     GTask            *task)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+  g_signal_emit (self, signals [FINISHED], 0, self->failed);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_execute_async:
+ * @self: A @IdeBuildPipeline
+ * @cancellable: (nullable): A #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: data for @callback
+ *
+ * Asynchronously starts the build pipeline.
+ *
+ * Any phase that has been invalidated up to the requested phase
+ * will be executed until a stage has failed.
+ *
+ * Upon completion, @callback will be executed and should call
+ * ide_build_pipeline_execute_finish() to get the status of the
+ * operation.
+ */
+void
+ide_build_pipeline_execute_async (IdeBuildPipeline    *self,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GFile) builddir = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->failed = FALSE;
+
+  g_signal_emit (self, signals [STARTED], 0);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_pipeline_execute_async);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_build_pipeline_notify_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /*
+   * Before we make any progoress, ensure the build directory is created
+   * so that pipeline stages need not worry about it. We'll just do this
+   * synchronously because if we can't do directory creation fast, well
+   * then we are pretty much screwed anyway.
+   */
+
+  builddir = g_file_new_for_path (self->builddir);
+
+  if (!g_file_make_directory_with_parents (builddir, cancellable, &error) &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  ide_build_pipeline_tick (self, task);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_execute_finish:
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ */
+gboolean
+ide_build_pipeline_execute_finish (IdeBuildPipeline  *self,
+                                   GAsyncResult      *result,
+                                   GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_connect:
+ * @self: A #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ * @priority: an optional priority for sorting within the phase
+ * @stage: An #IdeBuildStage
+ *
+ * Insert @stage into the pipeline as part of the phase denoted by @phase.
+ *
+ * If priority is non-zero, it will be used to sort the stage among other
+ * stages that are part of the same phase.
+ *
+ * Returns: A stage_id that may be passed to ide_build_pipeline_disconnect().
+ */
+guint
+ide_build_pipeline_connect (IdeBuildPipeline *self,
+                            IdeBuildPhase     phase,
+                            gint              priority,
+                            IdeBuildStage    *stage)
+{
+  GFlagsClass *klass;
+  guint ret = 0;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (stage), 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_WHENCE_MASK) == 0 ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_BEFORE ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_AFTER, 0);
+
+  if G_UNLIKELY (self->position != -1)
+    {
+      g_warning ("Cannot insert stage into pipeline after execution, ignoring");
+      IDE_RETURN (0);
+    }
+
+  klass = g_type_class_ref (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < klass->n_values; i++)
+    {
+      const GFlagsValue *value = &klass->values[i];
+
+      if ((phase & IDE_BUILD_PHASE_MASK) == value->value)
+        {
+          PipelineEntry entry = { 0 };
+
+          IDE_TRACE_MSG ("Adding stage to pipeline with phase %s and priority %d",
+                         value->value_nick, priority);
+
+          entry.id = ++self->seqnum;
+          entry.phase = phase;
+          entry.priority = priority;
+          entry.stage = g_object_ref (stage);
+
+          g_array_append_val (self->pipeline, entry);
+          g_array_sort (self->pipeline, pipeline_entry_compare);
+
+          ret = entry.id;
+
+          ide_build_stage_set_log_observer (stage,
+                                            ide_build_log_observer,
+                                            g_object_ref (self->log),
+                                            g_object_unref);
+
+          IDE_GOTO (cleanup);
+        }
+    }
+
+  g_warning ("No such pipeline phase %02x", phase);
+
+cleanup:
+  g_type_class_unref (klass);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_connect_launcher:
+ * @self: A #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ * @priority: an optional priority for sorting within the phase
+ * @launcher: An #IdeSubprocessLauncher
+ *
+ * This creates a new stage that will spawn a process using @launcher and log
+ * the output of stdin/stdout.
+ *
+ * It is a programmer error to modify @launcher after passing it to this
+ * function.
+ *
+ * Returns: A stage_id that may be passed to ide_build_pipeline_remove().
+ */
+guint
+ide_build_pipeline_connect_launcher (IdeBuildPipeline      *self,
+                                     IdeBuildPhase          phase,
+                                     gint                   priority,
+                                     IdeSubprocessLauncher *launcher)
+{
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeContext *context;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_WHENCE_MASK) == 0 ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_BEFORE ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_AFTER, 0);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  stage = ide_build_stage_launcher_new (context, launcher);
+
+  return ide_build_pipeline_connect (self, phase, priority, stage);
+}
+
+/**
+ * ide_build_pipeline_request_phase:
+ * @self: An #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ *
+ * Requests that the next execution of the pipeline will build up to @phase
+ * including all stages that were previously invalidated.
+ */
+void
+ide_build_pipeline_request_phase (IdeBuildPipeline *self,
+                                  IdeBuildPhase     phase)
+{
+  GFlagsClass *klass;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE);
+
+  /*
+   * You can only request basic phases. That does not include modifiers
+   * like BEFORE, AFTER, FAILED, FINISHED.
+   */
+  phase &= IDE_BUILD_PHASE_MASK;
+
+  if (self->position != -1)
+    {
+      g_warning ("Cannot request phase after execution has started");
+      IDE_EXIT;
+    }
+
+  klass = g_type_class_ref (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < klass->n_values; i++)
+    {
+      const GFlagsValue *value = &klass->values[i];
+
+      if (phase == value->value)
+        {
+          IDE_TRACE_MSG ("requesting pipeline phase %s", value->value_nick);
+          /*
+           * Each flag is a power of two, so we can simply subtract one
+           * to get a mask of all the previous phases.
+           */
+          self->requested_mask |= phase | (phase - 1);
+          IDE_GOTO (cleanup);
+        }
+    }
+
+  g_warning ("No such phase %02x", (guint)phase);
+
+cleanup:
+  g_type_class_unref (klass);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_get_builddir:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the "builddir" to be used for the build process. This is generally
+ * the location that build systems will use for out-of-tree builds.
+ *
+ * Returns: the path of the build directory
+ */
+const gchar *
+ide_build_pipeline_get_builddir (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->builddir;
+}
+
+/**
+ * ide_build_pipeline_get_srcdir:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the "srcdir" of the project. This is equivalent to the
+ * IdeVcs:working-directory property as a string.
+ *
+ * Returns: the path of the source directory
+ */
+const gchar *
+ide_build_pipeline_get_srcdir (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->srcdir;
+}
+
+static gchar *
+ide_build_pipeline_build_path_va_list (const gchar *prefix,
+                                       const gchar *first_part,
+                                       va_list      args)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_assert (prefix != NULL);
+  g_assert (first_part != NULL);
+
+  ar = g_ptr_array_new ();
+  g_ptr_array_add (ar, (gchar *)prefix);
+  do
+    g_ptr_array_add (ar, (gchar *)first_part);
+  while (NULL != (first_part = va_arg (args, const gchar *)));
+  g_ptr_array_add (ar, NULL);
+
+  return g_build_filenamev ((gchar **)ar->pdata);
+}
+
+/**
+ * ide_build_pipeline_build_srcdir_path:
+ *
+ * This is a convenience function to create a new path that starts with
+ * the source directory of the project.
+ *
+ * This is functionally equivalent to calling g_build_filename() with the
+ * working directory of the source tree.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ */
+gchar *
+ide_build_pipeline_build_srcdir_path (IdeBuildPipeline *self,
+                                      const gchar      *first_part,
+                                      ...)
+{
+  gchar *ret;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  va_start (args, first_part);
+  ret = ide_build_pipeline_build_path_va_list (self->srcdir, first_part, args);
+  va_end (args);
+
+  return ret;
+}
+
+/**
+ * ide_build_pipeline_build_builddir_path:
+ *
+ * This is a convenience function to create a new path that starts with
+ * the build directory for this build configuration.
+ *
+ * This is functionally equivalent to calling g_build_filename() with the
+ * result of ide_build_pipeline_get_builddir() as the first parameter.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ */
+gchar *
+ide_build_pipeline_build_builddir_path (IdeBuildPipeline *self,
+                                        const gchar      *first_part,
+                                        ...)
+{
+  gchar *ret;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  va_start (args, first_part);
+  ret = ide_build_pipeline_build_path_va_list (self->builddir, first_part, args);
+  va_end (args);
+
+  return ret;
+}
+
+/**
+ * ide_build_pipeline_disconnect:
+ * @self: An #IdeBuildPipeline
+ * @stage_id: An identifier returned from adding a stage
+ *
+ * This removes the stage matching @stage_id. You are returned a @stage_id when
+ * inserting a stage with functions such as ide_build_pipeline_connect()
+ * or ide_build_pipeline_connect_launcher().
+ *
+ * Plugins should use this function to remove their stages when the plugin
+ * is unloading.
+ */
+void
+ide_build_pipeline_disconnect (IdeBuildPipeline *self,
+                               guint             stage_id)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (self->pipeline != NULL);
+  g_return_if_fail (stage_id != 0);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (entry->id == stage_id)
+        {
+          g_array_remove_index (self->pipeline, i);
+          break;
+        }
+    }
+}
+
+/**
+ * ide_build_pipeline_invalidate_phase:
+ * @self: An #IdeBuildPipeline
+ * @phases: The phases to invalidate
+ *
+ * Invalidates the phases matching @phases flags.
+ *
+ * If the requested phases include the phases invalidated here, the next
+ * execution of the pipeline will execute thse phases.
+ *
+ * This should be used by plugins to ensure a particular phase is re-executed
+ * upon discovering its state is no longer valid. Such an example might be
+ * invalidating the %IDE_BUILD_PHASE_AUTOGEN phase when the an autotools
+ * projects autogen.sh file has been changed.
+ */
+void
+ide_build_pipeline_invalidate_phase (IdeBuildPipeline *self,
+                                     IdeBuildPhase     phases)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) & phases)
+        ide_build_stage_set_completed (entry->stage, FALSE);
+    }
+}
+
+/**
+ * ide_build_pipeline_get_stage_by_id:
+ * @self: An #IdeBuildPipeline
+ * @stage_id: the identfier of the stage
+ *
+ * Gets the stage matching the identifier @stage_id as returned from
+ * ide_build_pipeline_connect().
+ *
+ * Returns: (transfer none) (nullable): An #IdeBuildStage or %NULL if the
+ *   stage could not be found.
+ */
+IdeBuildStage *
+ide_build_pipeline_get_stage_by_id (IdeBuildPipeline *self,
+                                    guint             stage_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (entry->id == stage_id)
+        return entry->stage;
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_build_pipeline_create_launcher:
+ * @self: An #IdeBuildPipeline
+ *
+ * This is a convenience function to create a new #IdeSubprocessLauncher
+ * using the configuration and runtime associated with the pipeline.
+ *
+ * Returns: (transfer full): An #IdeSubprocessLauncher.
+ */
+IdeSubprocessLauncher *
+ide_build_pipeline_create_launcher (IdeBuildPipeline  *self,
+                                    GError           **error)
+{
+  IdeRuntime *runtime;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  runtime = ide_configuration_get_runtime (self->configuration);
+
+  if (runtime == NULL)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "The runtime %s is missing",
+                   ide_configuration_get_runtime_id (self->configuration));
+      return NULL;
+    }
+
+  return ide_runtime_create_launcher (runtime, error);
+}
+
+guint
+ide_build_pipeline_add_log_observer (IdeBuildPipeline    *self,
+                                     IdeBuildLogObserver  observer,
+                                     gpointer             observer_data,
+                                     GDestroyNotify       observer_data_destroy)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail (observer != NULL, 0);
+
+  return ide_build_log_add_observer (self->log, observer, observer_data, observer_data_destroy);
+
+}
+
+gboolean
+ide_build_pipeline_remove_log_observer (IdeBuildPipeline *self,
+                                        guint             observer_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (observer_id > 0, FALSE);
+
+  return ide_build_log_remove_observer (self->log, observer_id);
+}
+
+void
+ide_build_pipeline_emit_diagnostic (IdeBuildPipeline *self,
+                                    IdeDiagnostic    *diagnostic)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (diagnostic != NULL);
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+
+  g_signal_emit (self, signals[DIAGNOSTIC], 0, diagnostic);
+}
diff --git a/libide/buildsystem/ide-build-pipeline.h b/libide/buildsystem/ide-build-pipeline.h
new file mode 100644
index 0000000..d6cce60
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline.h
@@ -0,0 +1,102 @@
+/* ide-build-pipeline.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_PIPELINE_H
+#define IDE_BUILD_PIPELINE_H
+
+#include <gio/gio.h>
+
+#include "ide-types.h"
+
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-stage.h"
+#include "buildsystem/ide-configuration.h"
+#include "subprocess/ide-subprocess-launcher.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_PIPELINE     (ide_build_pipeline_get_type())
+#define IDE_BUILD_PHASE_MASK        (0xFFFFFF)
+#define IDE_BUILD_PHASE_WHENCE_MASK (IDE_BUILD_PHASE_BEFORE | IDE_BUILD_PHASE_AFTER)
+
+typedef enum
+{
+  IDE_BUILD_PHASE_NONE         = 0,
+  IDE_BUILD_PHASE_PREPARE      = 1 << 0,
+  IDE_BUILD_PHASE_DOWNLOADS    = 1 << 1,
+  IDE_BUILD_PHASE_DEPENDENCIES = 1 << 2,
+  IDE_BUILD_PHASE_AUTOGEN      = 1 << 3,
+  IDE_BUILD_PHASE_CONFIGURE    = 1 << 4,
+  IDE_BUILD_PHASE_BUILD        = 1 << 6,
+  IDE_BUILD_PHASE_INSTALL      = 1 << 7,
+  IDE_BUILD_PHASE_EXPORT       = 1 << 8,
+  IDE_BUILD_PHASE_FINAL        = 1 << 9,
+  IDE_BUILD_PHASE_BEFORE       = 1 << 28,
+  IDE_BUILD_PHASE_AFTER        = 1 << 29,
+  IDE_BUILD_PHASE_FINISHED     = 1 << 30,
+  IDE_BUILD_PHASE_FAILED       = 1 << 31,
+} IdeBuildPhase;
+
+G_DECLARE_FINAL_TYPE (IdeBuildPipeline, ide_build_pipeline, IDE, BUILD_PIPELINE, IdeObject)
+
+IdeConfiguration      *ide_build_pipeline_get_configuration   (IdeBuildPipeline       *self);
+const gchar           *ide_build_pipeline_get_builddir        (IdeBuildPipeline       *self);
+const gchar           *ide_build_pipeline_get_srcdir          (IdeBuildPipeline       *self);
+IdeSubprocessLauncher *ide_build_pipeline_create_launcher     (IdeBuildPipeline       *self,
+                                                               GError                **error);
+gchar                 *ide_build_pipeline_build_srcdir_path   (IdeBuildPipeline       *self,
+                                                               const gchar            *first_part,
+                                                               ...) G_GNUC_NULL_TERMINATED;
+gchar                 *ide_build_pipeline_build_builddir_path (IdeBuildPipeline       *self,
+                                                               const gchar            *first_part,
+                                                               ...) G_GNUC_NULL_TERMINATED;
+void                   ide_build_pipeline_invalidate_phase    (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phases);
+void                   ide_build_pipeline_request_phase       (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phase);
+guint                  ide_build_pipeline_connect             (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phase,
+                                                               gint                    priority,
+                                                               IdeBuildStage          *stage);
+guint                  ide_build_pipeline_connect_launcher    (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phase,
+                                                               gint                    priority,
+                                                               IdeSubprocessLauncher  *launcher);
+void                   ide_build_pipeline_disconnect          (IdeBuildPipeline       *self,
+                                                               guint                   stage_id);
+IdeBuildStage         *ide_build_pipeline_get_stage_by_id     (IdeBuildPipeline       *self,
+                                                               guint                   stage_id);
+guint                  ide_build_pipeline_add_log_observer    (IdeBuildPipeline       *self,
+                                                               IdeBuildLogObserver     observer,
+                                                               gpointer                observer_data,
+                                                               GDestroyNotify          
observer_data_destroy);
+gboolean               ide_build_pipeline_remove_log_observer (IdeBuildPipeline       *self,
+                                                               guint                   observer_id);
+void                   ide_build_pipeline_emit_diagnostic     (IdeBuildPipeline       *self,
+                                                               IdeDiagnostic          *diagnostic);
+void                   ide_build_pipeline_execute_async       (IdeBuildPipeline       *self,
+                                                               GCancellable           *cancellable,
+                                                               GAsyncReadyCallback     callback,
+                                                               gpointer                user_data);
+gboolean               ide_build_pipeline_execute_finish      (IdeBuildPipeline       *self,
+                                                               GAsyncResult           *result,
+                                                               GError                **error);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_PIPELINE_H */
diff --git a/libide/buildsystem/ide-build-stage-launcher.c b/libide/buildsystem/ide-build-stage-launcher.c
new file mode 100644
index 0000000..3f881ab
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-launcher.c
@@ -0,0 +1,277 @@
+/* ide-build-stage-launcher.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-stage-launcher"
+
+#include "ide-debug.h"
+
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-stage-launcher.h"
+#include "subprocess/ide-subprocess.h"
+
+struct _IdeBuildStageLauncher
+{
+  IdeBuildStage          parent_instance;
+  IdeSubprocessLauncher *launcher;
+};
+
+enum {
+  PROP_0,
+  PROP_LAUNCHER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE_TYPE_BUILD_STAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_build_stage_launcher_wait_check_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_stage_launcher_execute_async (IdeBuildStage       *stage,
+                                        IdeBuildPipeline    *pipeline,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  const gchar * const *argv;
+  GSubprocessFlags flags;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_stage_launcher_execute_async);
+
+  if (self->launcher == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_INVAL,
+                               "Improperly configured %s",
+                               G_OBJECT_TYPE_NAME (self));
+      IDE_EXIT;
+    }
+
+  flags = ide_subprocess_launcher_get_flags (self->launcher);
+
+  /* Disable flags we do not want set for build pipeline stuff */
+
+  if (flags & G_SUBPROCESS_FLAGS_STDERR_SILENCE)
+    flags &= ~G_SUBPROCESS_FLAGS_STDERR_SILENCE;
+
+  if (flags & G_SUBPROCESS_FLAGS_STDERR_MERGE)
+    flags &= ~G_SUBPROCESS_FLAGS_STDERR_MERGE;
+
+  if (flags & G_SUBPROCESS_FLAGS_STDIN_INHERIT)
+    flags &= ~G_SUBPROCESS_FLAGS_STDIN_INHERIT;
+
+  /* Ensure we have access to stdin/stdout streams */
+
+  flags |= G_SUBPROCESS_FLAGS_STDOUT_PIPE;
+  flags |= G_SUBPROCESS_FLAGS_STDERR_PIPE;
+
+  ide_subprocess_launcher_set_flags (self->launcher, flags);
+
+  /* Log the command line to build log */
+
+  argv = ide_subprocess_launcher_get_argv (self->launcher);
+
+  if (argv != NULL && argv[0] != NULL)
+    {
+      g_autoptr(GString) argv_str = g_string_new ("Executing ");
+
+      g_string_append (argv_str, argv[0]);
+
+      for (guint i = 1; argv[i] != NULL; i++)
+        {
+          g_autofree gchar *quoted = g_shell_quote (argv[i]);
+
+          g_string_append_printf (argv_str, " '%s'", quoted);
+        }
+
+      g_string_append (argv_str, " from directory '");
+      g_string_append (argv_str, ide_subprocess_launcher_get_cwd (self->launcher));
+      g_string_append (argv_str, "'");
+
+      ide_build_stage_log (stage, IDE_BUILD_LOG_STDOUT, argv_str->str, argv_str->len);
+    }
+
+  /* Now launch the process */
+
+  subprocess = ide_subprocess_launcher_spawn (self->launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_build_stage_log_subprocess (IDE_BUILD_STAGE (self), subprocess);
+
+  ide_subprocess_wait_check_async (subprocess,
+                                   cancellable,
+                                   ide_build_stage_launcher_wait_check_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_stage_launcher_execute_finish (IdeBuildStage  *stage,
+                                         GAsyncResult   *result,
+                                         GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (stage));
+  g_assert (G_IS_TASK (result));
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_build_stage_launcher_finalize (GObject *object)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+
+  g_clear_object (&self->launcher);
+
+  G_OBJECT_CLASS (ide_build_stage_launcher_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_launcher_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+
+  switch (prop_id)
+    {
+    case PROP_LAUNCHER:
+      g_value_set_object (value, ide_build_stage_launcher_get_launcher (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_launcher_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+
+  switch (prop_id)
+    {
+    case PROP_LAUNCHER:
+      self->launcher = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_launcher_class_init (IdeBuildStageLauncherClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *build_stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_launcher_finalize;
+  object_class->get_property = ide_build_stage_launcher_get_property;
+  object_class->set_property = ide_build_stage_launcher_set_property;
+
+  build_stage_class->execute_async = ide_build_stage_launcher_execute_async;
+  build_stage_class->execute_finish = ide_build_stage_launcher_execute_finish;
+
+  properties [PROP_LAUNCHER] =
+    g_param_spec_object ("launcher",
+                         "Launcher",
+                         "The subprocess launcher to execute",
+                         IDE_TYPE_SUBPROCESS_LAUNCHER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_build_stage_launcher_init (IdeBuildStageLauncher *self)
+{
+}
+
+/**
+ * ide_build_stage_launcher_get_launcher:
+ *
+ * Returns: (transfer none): An #IdeSubprocessLauncher
+ */
+IdeSubprocessLauncher *
+ide_build_stage_launcher_get_launcher (IdeBuildStageLauncher *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), NULL);
+
+  return self->launcher;
+}
+
+IdeBuildStage *
+ide_build_stage_launcher_new (IdeContext            *context,
+                              IdeSubprocessLauncher *launcher)
+{
+  return g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                       "context", context,
+                       "launcher", launcher,
+                       NULL);
+}
diff --git a/libide/buildsystem/ide-build-stage-launcher.h b/libide/buildsystem/ide-build-stage-launcher.h
new file mode 100644
index 0000000..e3eb2e8
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-launcher.h
@@ -0,0 +1,41 @@
+/* ide-build-stage-launcher.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_STAGE_LAUNCHER_H
+#define IDE_BUILD_STAGE_LAUNCHER_H
+
+#include <gio/gio.h>
+
+#include "ide-context.h"
+
+#include "buildsystem/ide-build-stage.h"
+#include "subprocess/ide-subprocess-launcher.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_LAUNCHER (ide_build_stage_launcher_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE, BUILD_STAGE_LAUNCHER, 
IdeBuildStage)
+
+IdeBuildStage         *ide_build_stage_launcher_new          (IdeContext            *context,
+                                                              IdeSubprocessLauncher *launcher);
+IdeSubprocessLauncher *ide_build_stage_launcher_get_launcher (IdeBuildStageLauncher *self);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_LAUNCHER_H */
diff --git a/libide/buildsystem/ide-build-stage-mkdirs.c b/libide/buildsystem/ide-build-stage-mkdirs.c
new file mode 100644
index 0000000..c284fa7
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-mkdirs.c
@@ -0,0 +1,146 @@
+/* ide-build-stage-mkdirs.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-stage-mkdirs"
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "ide-build-stage-mkdirs.h"
+
+typedef struct
+{
+  gchar    *path;
+  gboolean  with_parents;
+  gint      mode;
+} Path;
+
+typedef struct
+{
+  GArray *paths;
+} IdeBuildStageMkdirsPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE_TYPE_BUILD_STAGE)
+
+static void
+clear_path (gpointer data)
+{
+  Path *p = data;
+
+  g_clear_pointer (&p->path, g_free);
+}
+
+static gboolean
+ide_build_stage_mkdirs_execute (IdeBuildStage     *stage,
+                                IdeBuildPipeline  *pipeline,
+                                GCancellable      *cancellable,
+                                GError           **error)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)stage;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  g_assert (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (guint i = 0; i < priv->paths->len; i++)
+    {
+      const Path *path = &g_array_index (priv->paths, Path, i);
+      gboolean r;
+
+      if (g_file_test (path->path, G_FILE_TEST_IS_DIR))
+        continue;
+
+      if (path->with_parents)
+        r = g_mkdir_with_parents (path->path, path->mode);
+      else
+        r = g_mkdir (path->path, path->mode);
+
+      if (r != 0)
+        {
+          g_set_error_literal (error,
+                               G_FILE_ERROR,
+                               g_file_error_from_errno (errno),
+                               g_strerror (errno));
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+ide_build_stage_mkdirs_finalize (GObject *object)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)object;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  g_clear_pointer (&priv->paths, g_array_unref);
+
+  G_OBJECT_CLASS (ide_build_stage_mkdirs_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_mkdirs_class_init (IdeBuildStageMkdirsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_mkdirs_finalize;
+
+  stage_class->execute = ide_build_stage_mkdirs_execute;
+}
+
+static void
+ide_build_stage_mkdirs_init (IdeBuildStageMkdirs *self)
+{
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  priv->paths = g_array_new (FALSE, FALSE, sizeof (Path));
+  g_array_set_clear_func (priv->paths, clear_path);
+}
+
+IdeBuildStage *
+ide_build_stage_mkdirs_new (IdeContext *context)
+{
+  return g_object_new (IDE_TYPE_BUILD_STAGE_MKDIRS,
+                       "context", context,
+                       NULL);
+}
+
+void
+ide_build_stage_mkdirs_add_path (IdeBuildStageMkdirs *self,
+                                 const gchar         *path,
+                                 gboolean             with_parents,
+                                 gint                 mode)
+{
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+  Path ele = { 0 };
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_return_if_fail (path != NULL);
+
+  ele.path = g_strdup (path);
+  ele.with_parents = with_parents;
+  ele.mode = mode;
+
+  g_array_append_val (priv->paths, ele);
+}
diff --git a/libide/buildsystem/ide-build-stage-mkdirs.h b/libide/buildsystem/ide-build-stage-mkdirs.h
new file mode 100644
index 0000000..3f25659
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-mkdirs.h
@@ -0,0 +1,48 @@
+/* ide-build-stage-mkdirs.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_STAGE_MKDIRS_H
+#define IDE_BUILD_STAGE_MKDIRS_H
+
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_MKDIRS (ide_build_stage_mkdirs_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE, BUILD_STAGE_MKDIRS, 
IdeBuildStage)
+
+struct _IdeBuildStageMkdirsClass
+{
+  IdeBuildStageClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+IdeBuildStage *ide_build_stage_mkdirs_new      (IdeContext          *context);
+void           ide_build_stage_mkdirs_add_path (IdeBuildStageMkdirs *self,
+                                                const gchar         *path,
+                                                gboolean             with_parents,
+                                                gint                 mode);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_MKDIRS_H */
diff --git a/libide/buildsystem/ide-build-stage-private.h b/libide/buildsystem/ide-build-stage-private.h
new file mode 100644
index 0000000..f84245d
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-private.h
@@ -0,0 +1,38 @@
+/* ide-build-stage-private.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_STAGE_PRIVATE_H
+#define IDE_BUILD_STAGE_PRIVATE_H
+
+#include "ide-build-pipeline.h"
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+void     _ide_build_stage_execute_with_query_async  (IdeBuildStage        *self,
+                                                     IdeBuildPipeline     *pipeline,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+gboolean _ide_build_stage_execute_with_query_finish (IdeBuildStage        *self,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_PRIVATE_H */
diff --git a/libide/buildsystem/ide-build-stage-transfer.c b/libide/buildsystem/ide-build-stage-transfer.c
new file mode 100644
index 0000000..5edefe3
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-transfer.c
@@ -0,0 +1,183 @@
+/* ide-build-stage-transfer.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-stage-transfer"
+
+#include "ide-context.h"
+
+#include "buildsystem/ide-build-stage-transfer.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "transfers/ide-transfer-manager.h"
+#include "transfers/ide-transfer.h"
+
+struct _IdeBuildStageTransfer
+{
+  IdeBuildStage  parent_instnace;
+  IdeTransfer   *transfer;
+};
+
+enum {
+  PROP_0,
+  PROP_TRANSFER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE_TYPE_BUILD_STAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_build_stage_transfer_execute_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeTransferManager *transfer_manager = (IdeTransferManager *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (transfer_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_transfer_manager_execute_finish (transfer_manager, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_build_stage_transfer_execute_async (IdeBuildStage       *stage,
+                                        IdeBuildPipeline    *pipeline,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)stage;
+  g_autoptr(GTask) task = NULL;
+  IdeTransferManager *transfer_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_stage_transfer_execute_async);
+
+  if (ide_transfer_has_completed (self->transfer))
+    {
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  transfer_manager = ide_context_get_transfer_manager (context);
+
+  ide_transfer_manager_execute_async (transfer_manager,
+                                      self->transfer,
+                                      cancellable,
+                                      ide_build_stage_transfer_execute_cb,
+                                      g_steal_pointer (&task));
+}
+
+static gboolean
+ide_build_stage_transfer_execute_finish (IdeBuildStage  *stage,
+                                         GAsyncResult   *result,
+                                         GError        **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (stage));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_build_stage_transfer_finalize (GObject *object)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  g_clear_object (&self->transfer);
+
+  G_OBJECT_CLASS (ide_build_stage_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_transfer_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  switch (prop_id)
+    {
+    case PROP_TRANSFER:
+      g_value_set_object (value, self->transfer);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_transfer_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  switch (prop_id)
+    {
+    case PROP_TRANSFER:
+      self->transfer = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_transfer_class_init (IdeBuildStageTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *build_stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_transfer_finalize;
+  object_class->get_property = ide_build_stage_transfer_get_property;
+  object_class->set_property = ide_build_stage_transfer_set_property;
+
+  build_stage_class->execute_async = ide_build_stage_transfer_execute_async;
+  build_stage_class->execute_finish = ide_build_stage_transfer_execute_finish;
+
+  properties [PROP_TRANSFER] =
+    g_param_spec_object ("transfer",
+                         "Transfer",
+                         "The transfer to perform as part of the stage",
+                         IDE_TYPE_TRANSFER,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_build_stage_transfer_init (IdeBuildStageTransfer *self)
+{
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/libide/buildsystem/ide-build-stage-transfer.h
similarity index 54%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to libide/buildsystem/ide-build-stage-transfer.h
index 2a77a0b..8ae4566 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/libide/buildsystem/ide-build-stage-transfer.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* ide-build-stage-transfer.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,20 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef IDE_BUILD_STAGE_TRANSFER_H
+#define IDE_BUILD_STAGE_TRANSFER_H
 
-#include <ide.h>
+#include "buildsystem/ide-build-stage.h"
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define IDE_TYPE_BUILD_STAGE_TRANSFER (ide_build_stage_transfer_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE, BUILD_STAGE_TRANSFER, 
IdeBuildStage)
+
+IdeBuildStageTransfer *ide_build_stage_transfer_new (IdeContext  *context,
+                                                     IdeTransfer *transfer);
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* IDE_BUILD_STAGE_TRANSFER_H */
diff --git a/libide/buildsystem/ide-build-stage.c b/libide/buildsystem/ide-build-stage.c
new file mode 100644
index 0000000..f148971
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage.c
@@ -0,0 +1,764 @@
+/* ide-build-stage.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-build-stage"
+
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-stage.h"
+#include "subprocess/ide-subprocess.h"
+
+typedef struct
+{
+  gchar               *name;
+  IdeBuildLogObserver  observer;
+  gpointer             observer_data;
+  GDestroyNotify       observer_data_destroy;
+  GTask               *queued_execute;
+  gchar               *stdout_path;
+  GOutputStream       *stdout_stream;
+  gint                 n_pause;
+  guint                completed : 1;
+  guint                transient : 1;
+} IdeBuildStagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStage, ide_build_stage, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_COMPLETED,
+  PROP_NAME,
+  PROP_STDOUT_PATH,
+  PROP_TRANSIENT,
+  N_PROPS
+};
+
+enum {
+  QUERY,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+typedef struct
+{
+  IdeBuildStage     *self;
+  IdeBuildLogStream  stream_type;
+} Tail;
+
+static Tail *
+tail_new (IdeBuildStage     *self,
+          IdeBuildLogStream  stream_type)
+{
+  Tail *tail;
+
+  tail = g_slice_new0 (Tail);
+  tail->self = g_object_ref (self);
+  tail->stream_type = stream_type;
+
+  return tail;
+}
+
+static void
+tail_free (Tail *tail)
+{
+  g_clear_object (&tail->self);
+  g_slice_free (Tail, tail);
+}
+
+static void
+ide_build_stage_clear_observer (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  GDestroyNotify notify = priv->observer_data_destroy;
+  gpointer data = priv->observer_data;
+
+  priv->observer_data_destroy = NULL;
+  priv->observer_data = NULL;
+  priv->observer = NULL;
+
+  if (notify != NULL)
+    notify (data);
+}
+
+static gboolean
+ide_build_stage_real_execute (IdeBuildStage     *self,
+                              IdeBuildPipeline  *pipeline,
+                              GCancellable      *cancellable,
+                              GError           **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  return TRUE;
+}
+
+static void
+ide_build_stage_real_execute_worker (GTask        *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  IdeBuildStage *self = source_object;
+  IdeBuildPipeline *pipeline = task_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_STAGE_GET_CLASS (self)->execute (self, pipeline, cancellable, &error))
+    g_task_return_boolean (task, TRUE);
+  else
+    g_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+ide_build_stage_real_execute_async (IdeBuildStage       *self,
+                                    IdeBuildPipeline    *pipeline,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_stage_real_execute_async);
+  g_task_set_task_data (task, g_object_ref (pipeline), g_object_unref);
+  g_task_run_in_thread (task, ide_build_stage_real_execute_worker);
+}
+
+static gboolean
+ide_build_stage_real_execute_finish (IdeBuildStage  *self,
+                                     GAsyncResult   *result,
+                                     GError        **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+const gchar *
+ide_build_stage_get_name (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), NULL);
+
+  return priv->name;
+}
+
+void
+ide_build_stage_set_name (IdeBuildStage *self,
+                          const gchar   *name)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  if (g_strcmp0 (name, priv->name) != 0)
+    {
+      g_free (priv->name);
+      priv->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+    }
+}
+
+static void
+ide_build_stage_finalize (GObject *object)
+{
+  IdeBuildStage *self = (IdeBuildStage *)object;
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  ide_build_stage_clear_observer (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->stdout_path, g_free);
+  g_clear_object (&priv->queued_execute);
+  g_clear_object (&priv->stdout_stream);
+
+  G_OBJECT_CLASS (ide_build_stage_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeBuildStage *self = IDE_BUILD_STAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPLETED:
+      g_value_set_boolean (value, ide_build_stage_get_completed (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, ide_build_stage_get_name (self));
+      break;
+
+    case PROP_STDOUT_PATH:
+      g_value_set_string (value, ide_build_stage_get_stdout_path (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeBuildStage *self = IDE_BUILD_STAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPLETED:
+      ide_build_stage_set_completed (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_NAME:
+      ide_build_stage_set_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_STDOUT_PATH:
+      ide_build_stage_set_stdout_path (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_class_init (IdeBuildStageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_finalize;
+  object_class->get_property = ide_build_stage_get_property;
+  object_class->set_property = ide_build_stage_set_property;
+
+  klass->execute = ide_build_stage_real_execute;
+  klass->execute_async = ide_build_stage_real_execute_async;
+  klass->execute_finish = ide_build_stage_real_execute_finish;
+
+  /**
+   * IdeBuildStage:completed:
+   *
+   * The "completed" property is set to %TRUE after the pipeline has
+   * completed processing the stage. When the pipeline invalidates
+   * phases, completed may be reset to %FALSE.
+   */
+  properties [PROP_COMPLETED] =
+    g_param_spec_boolean ("completed",
+                          "Completed",
+                          "If the stage has been completed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:name:
+   *
+   * The name of the build stage. This is only used by UI to view
+   * the build pipeline.
+   */
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The user visible name of the stage",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:stdout-path:
+   *
+   * The "stdout-path" property allows a build stage to redirect its log
+   * messages to a stdout file. Instead of passing stdout along to the
+   * build pipeline, they will be redirected to this file.
+   *
+   * For safety reasons, the contents are first redirected to a temporary
+   * file and will be redirected to the stdout-path location after the
+   * build stage has completed executing.
+   */
+  properties [PROP_STDOUT_PATH] =
+    g_param_spec_string ("stdout-path",
+                         "Stdout Path",
+                         "Redirect standard output to this path",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:transient:
+   *
+   * If the build stage is transient.
+   *
+   * A transient build stage is removed after the completion of
+   * ide_build_pipeline_execute_async(). This can be a convenient
+   * way to add a temporary item to a build pipeline that should
+   * be immediately discarded.
+   */
+  properties [PROP_TRANSIENT] =
+    g_param_spec_boolean ("transient",
+                          "Transient",
+                          "If the stage should be removed after execution",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [QUERY] =
+    g_signal_new_class_handler ("query",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL, NULL,
+                                G_TYPE_NONE, 2, IDE_TYPE_BUILD_PIPELINE, G_TYPE_CANCELLABLE);
+}
+
+static void
+ide_build_stage_init (IdeBuildStage *self)
+{
+}
+
+void
+ide_build_stage_execute_async (IdeBuildStage       *self,
+                               IdeBuildPipeline    *pipeline,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if G_UNLIKELY (priv->stdout_path != NULL)
+    {
+      g_autoptr(GFileOutputStream) stream = NULL;
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(GError) error = NULL;
+
+      file = g_file_new_for_path (priv->stdout_path);
+      stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, &error);
+
+      if (stream == NULL)
+        {
+          g_task_report_error (self, callback, user_data,
+                               ide_build_stage_execute_async,
+                               g_steal_pointer (&error));
+          return;
+        }
+
+      g_clear_object (&priv->stdout_stream);
+
+      priv->stdout_stream = g_steal_pointer (&stream);
+    }
+
+  IDE_BUILD_STAGE_GET_CLASS (self)->execute_async (self, pipeline, cancellable, callback, user_data);
+}
+
+gboolean
+ide_build_stage_execute_finish (IdeBuildStage  *self,
+                                GAsyncResult   *result,
+                                GError        **error)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  /*
+   * If for some reason execute_finish() is not called (likely due to use of
+   * the build stage without a pipeline, so sort of a programming error) then
+   * we won't clean up the stdout stream. But it gets cleaned up in finalize
+   * anyway, so its safe (if only delayed rename()).
+   *
+   * We can just unref the stream, and the close will happen silently. But we
+   * want to ensure that it wasn't leaked and therefore any future calls on it
+   * would error with G_IO_ERROR_CLOSED.
+   */
+  if (priv->stdout_stream != NULL)
+    {
+      g_output_stream_close (priv->stdout_stream, NULL, NULL);
+      g_clear_object (&priv->stdout_stream);
+    }
+
+  return IDE_BUILD_STAGE_GET_CLASS (self)->execute_finish (self, result, error);
+}
+
+/**
+ * ide_build_stage_set_log_observer:
+ * @self: An #IdeBuildStage
+ * @observer: (scope async): The observer for the log entries
+ * @observer_data: data for @observer
+ * @observer_data_destroy: destroy callback for @observer_data
+ *
+ * Sets the log observer to handle calls to the various stage logging
+ * functions. This will be set by the pipeline to mux logs from all
+ * stages into a unified build log.
+ *
+ * Plugins that need to handle logging from a build stage should set
+ * an observer on the pipeline so that log distribution may be fanned
+ * out to all observers.
+ */
+void
+ide_build_stage_set_log_observer (IdeBuildStage       *self,
+                                  IdeBuildLogObserver  observer,
+                                  gpointer             observer_data,
+                                  GDestroyNotify       observer_data_destroy)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  ide_build_stage_clear_observer (self);
+
+  priv->observer = observer;
+  priv->observer_data = observer_data;
+  priv->observer_data_destroy = observer_data_destroy;
+}
+
+void
+ide_build_stage_log (IdeBuildStage        *self,
+                     IdeBuildLogStream     stream,
+                     const gchar          *message,
+                     gssize                message_len)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  /*
+   * If we are logging to a file instead of the build pipeline, handle that
+   * special case now. It's unlikely (most stages should log to the pipeline)
+   * so mark it as unlikely to prefer a jump to our observer.
+   */
+  if G_UNLIKELY (priv->stdout_stream != NULL)
+    {
+      gsize count;
+
+      if (message_len < 0)
+        message_len = strlen (message);
+
+      if (!g_output_stream_write_all (priv->stdout_stream, message, message_len, &count, NULL, NULL))
+        return;
+
+      if (!g_output_stream_write_all (priv->stdout_stream, "\n", 1, &count, NULL, NULL))
+        return;
+
+      return;
+    }
+
+  if G_LIKELY (priv->observer)
+    priv->observer (stream, message, message_len, priv->observer_data);
+}
+
+gboolean
+ide_build_stage_get_completed (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->completed;
+}
+
+void
+ide_build_stage_set_completed (IdeBuildStage *self,
+                               gboolean       completed)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  completed = !!completed;
+
+  if (completed != priv->completed)
+    {
+      priv->completed = completed;
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPLETED]);
+    }
+}
+
+void
+ide_build_stage_set_transient (IdeBuildStage *self,
+                               gboolean       transient)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  transient = !!transient;
+
+  if (priv->transient != transient)
+    {
+      priv->transient = transient;
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSIENT]);
+    }
+}
+
+gboolean
+ide_build_stage_get_transient (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->transient;
+}
+
+static void
+ide_build_stage_observe_stream_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GDataInputStream *stream = (GDataInputStream *)object;
+  g_autofree gchar *line = NULL;
+  Tail *tail = user_data;
+  gsize n_read = 0;
+
+  g_assert (G_IS_DATA_INPUT_STREAM (stream));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (tail != NULL);
+
+  line = g_data_input_stream_read_line_finish_utf8 (stream, result, &n_read, NULL);
+
+  if G_LIKELY (line != NULL && n_read > 0 && n_read < G_MAXSSIZE)
+    {
+      ide_build_stage_log (tail->self, tail->stream_type, line, (gssize)n_read);
+      g_data_input_stream_read_line_async (stream,
+                                           G_PRIORITY_DEFAULT,
+                                           NULL,
+                                           ide_build_stage_observe_stream_cb,
+                                           tail);
+      return;
+    }
+
+  tail_free (tail);
+}
+
+
+static void
+ide_build_stage_observe_stream (IdeBuildStage     *self,
+                                IdeBuildLogStream  stream_type,
+                                GInputStream      *stream)
+{
+  g_autoptr(GDataInputStream) data_stream = NULL;
+  Tail *tail;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (stream_type == IDE_BUILD_LOG_STDOUT || stream_type == IDE_BUILD_LOG_STDERR);
+  g_assert (G_IS_INPUT_STREAM (stream));
+
+  if (G_IS_DATA_INPUT_STREAM (stream))
+    data_stream = g_object_ref (stream);
+  else
+    data_stream = g_data_input_stream_new (stream);
+
+  tail = tail_new (self, stream_type);
+
+  g_data_input_stream_read_line_async (data_stream,
+                                       G_PRIORITY_DEFAULT,
+                                       NULL,
+                                       ide_build_stage_observe_stream_cb,
+                                       tail);
+}
+
+/**
+ * ide_build_stage_log_subprocess:
+ * @self: An #IdeBuildStage
+ * @subprocess: An #IdeSubprocess
+ *
+ * This function will begin logging @subprocess by reading from the
+ * stdout and stderr streams of the subprocess. You must have created
+ * the subprocess with %G_SUBPROCESS_FLAGS_STDERR_PIPE and
+ * %G_SUBPROCESS_FLAGS_STDOUT_PIPE so that the streams may be read.
+ */
+void
+ide_build_stage_log_subprocess (IdeBuildStage *self,
+                                IdeSubprocess *subprocess)
+{
+  GInputStream *stdout_stream;
+  GInputStream *stderr_stream;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS (subprocess));
+
+  stderr_stream = ide_subprocess_get_stderr_pipe (subprocess);
+  stdout_stream = ide_subprocess_get_stdout_pipe (subprocess);
+
+  if (stderr_stream != NULL)
+    ide_build_stage_observe_stream (self, IDE_BUILD_LOG_STDERR, stderr_stream);
+
+  if (stdout_stream != NULL)
+    ide_build_stage_observe_stream (self, IDE_BUILD_LOG_STDOUT, stdout_stream);
+}
+
+void
+ide_build_stage_pause (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  g_atomic_int_inc (&priv->n_pause);
+}
+
+static void
+ide_build_stage_unpause_execute_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeBuildStage *self = (IdeBuildStage *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_build_stage_execute_finish (self, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+void
+ide_build_stage_unpause (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (priv->n_pause > 0);
+
+  if (g_atomic_int_dec_and_test (&priv->n_pause) && priv->queued_execute != NULL)
+    {
+      g_autoptr(GTask) task = g_steal_pointer (&priv->queued_execute);
+      GCancellable *cancellable = g_task_get_cancellable (task);
+      IdeBuildPipeline *pipeline = g_task_get_task_data (task);
+
+      g_assert (G_IS_TASK (task));
+      g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+      g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+      if (priv->completed)
+        {
+          g_task_return_boolean (task, TRUE);
+          return;
+        }
+
+      ide_build_stage_execute_async (self,
+                                     pipeline,
+                                     cancellable,
+                                     ide_build_stage_unpause_execute_cb,
+                                     g_steal_pointer (&task));
+    }
+}
+
+/**
+ * _ide_build_stage_execute_with_query_async: (skip)
+ *
+ * This function is used to execute the build stage after emitting the
+ * query signal. If the stage is paused after the query, execute will
+ * be delayed until the correct number of ide_build_stage_unpause() calls
+ * have occurred.
+ */
+void
+_ide_build_stage_execute_with_query_async (IdeBuildStage       *self,
+                                           IdeBuildPipeline    *pipeline,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, _ide_build_stage_execute_with_query_async);
+  g_task_set_task_data (task, g_object_ref (pipeline), g_object_unref);
+
+  if (priv->queued_execute != NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_PENDING,
+                               "A build is already in progress");
+      return;
+    }
+
+  priv->queued_execute = g_steal_pointer (&task);
+
+  /*
+   * Pause the pipeline around our query call so that any call to
+   * pause/unpause does not cause the stage to make progress. This allows
+   * us to share the code-path to make progress on the build stage.
+   */
+  ide_build_stage_pause (self);
+  g_signal_emit (self, signals [QUERY], 0, pipeline, cancellable);
+  ide_build_stage_unpause (self);
+}
+
+gboolean
+_ide_build_stage_execute_with_query_finish (IdeBuildStage  *self,
+                                            GAsyncResult   *result,
+                                            GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+ide_build_stage_set_stdout_path (IdeBuildStage *self,
+                                 const gchar   *stdout_path)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  if (g_strcmp0 (stdout_path, priv->stdout_path) != 0)
+    {
+      g_free (priv->stdout_path);
+      priv->stdout_path = g_strdup (stdout_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STDOUT_PATH]);
+    }
+}
+
+const gchar *
+ide_build_stage_get_stdout_path (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), NULL);
+
+  return priv->stdout_path;
+}
diff --git a/libide/buildsystem/ide-build-stage.h b/libide/buildsystem/ide-build-stage.h
new file mode 100644
index 0000000..0f7a3bc
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage.h
@@ -0,0 +1,149 @@
+/* ide-build-stage.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_STAGE_H
+#define IDE_BUILD_STAGE_H
+
+#include <gio/gio.h>
+
+#include "ide-types.h"
+#include "ide-object.h"
+
+#include "buildsystem/ide-build-log.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE (ide_build_stage_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStage, ide_build_stage, IDE, BUILD_STAGE, IdeObject)
+
+struct _IdeBuildStageClass
+{
+  IdeObjectClass parent_class;
+
+  /**
+   * IdeBuildStage::execute:
+   *
+   * This vfunc will be run in a thread by the default
+   * IdeBuildStage::execute_async() and IdeBuildStage::execute_finish()
+   * vfuncs.
+   *
+   * Only use thread-safe API from this function.
+   */
+  gboolean (*execute)        (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GError              **error);
+
+  /**
+   * IdeBuildStage::execute_async:
+   *
+   * Asynchronous version of the #IdeBuildStage API. This is the preferred
+   * way to subclass #IdeBuildStage.
+   */
+  void     (*execute_async)  (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+
+  /**
+   * IdeBuildStage::execute_finish:
+   *
+   * Completes an asynchronous call to ide_build_stage_execute_async().
+   *
+   * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+   *   Upon failure, the pipeline will be stopped.
+   */
+  gboolean (*execute_finish) (IdeBuildStage        *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+
+  /**
+   * IdeBuildStage::query:
+   * @self: An #IdeBuildStage
+   * @pipeline: An #IdeBuildPipeline
+   * @cancellable: (nullable): A #GCancellable or %NULL
+   *
+   * The #IdeBuildStage::query signal is emitted to request that the
+   * build stage update its completed stage from any external resources.
+   *
+   * This can be useful if you want to use an existing build stage instances
+   * and use a signal to pause forward progress until an external system
+   * has been checked.
+   *
+   * For example, in a signal handler, you may call ide_build_stage_pause()
+   * and perform an external operation. Forward progress of the stage will
+   * be paused until a matching number of ide_build_stage_unpause() calls
+   * have been made.
+   */
+  void     (*query)          (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+  gpointer _reserved9;
+  gpointer _reserved10;
+  gpointer _reserved11;
+  gpointer _reserved12;
+};
+
+const gchar   *ide_build_stage_get_name         (IdeBuildStage        *self);
+void           ide_build_stage_set_name         (IdeBuildStage        *self,
+                                                 const gchar          *name);
+void           ide_build_stage_log              (IdeBuildStage        *self,
+                                                 IdeBuildLogStream     stream,
+                                                 const gchar          *message,
+                                                 gssize                message_len);
+void           ide_build_stage_log_subprocess   (IdeBuildStage        *self,
+                                                 IdeSubprocess        *subprocess);
+void           ide_build_stage_set_log_observer (IdeBuildStage        *self,
+                                                 IdeBuildLogObserver   observer,
+                                                 gpointer              observer_data,
+                                                 GDestroyNotify        observer_data_destroy);
+void           ide_build_stage_set_stdout_path  (IdeBuildStage        *self,
+                                                 const gchar          *path);
+const gchar   *ide_build_stage_get_stdout_path  (IdeBuildStage        *self);
+gboolean       ide_build_stage_get_completed    (IdeBuildStage        *self);
+void           ide_build_stage_set_completed    (IdeBuildStage        *self,
+                                                 gboolean              completed);
+gboolean       ide_build_stage_get_transient    (IdeBuildStage        *self);
+void           ide_build_stage_set_transient    (IdeBuildStage        *self,
+                                                 gboolean              transient);
+void           ide_build_stage_execute_async    (IdeBuildStage        *self,
+                                                 IdeBuildPipeline     *pipeline,
+                                                 GCancellable         *cancellable,
+                                                 GAsyncReadyCallback   callback,
+                                                 gpointer              user_data);
+gboolean       ide_build_stage_execute_finish   (IdeBuildStage        *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error);
+void           ide_build_stage_pause            (IdeBuildStage        *self);
+void           ide_build_stage_unpause          (IdeBuildStage        *self);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_H */
+
diff --git a/libide/buildsystem/ide-configuration.c b/libide/buildsystem/ide-configuration.c
index 1fae34c..0ce7fd8 100644
--- a/libide/buildsystem/ide-configuration.c
+++ b/libide/buildsystem/ide-configuration.c
@@ -1192,6 +1192,37 @@ ide_configuration_set_internal_string (IdeConfiguration *self,
   g_value_set_string (v, value);
 }
 
+const gchar * const *
+ide_configuration_get_internal_strv (IdeConfiguration *self,
+                                     const gchar      *key)
+{
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  v = g_hash_table_lookup (self->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS (v, G_TYPE_STRV))
+    return g_value_get_boxed (v);
+
+  return NULL;
+}
+
+void
+ide_configuration_set_internal_strv (IdeConfiguration    *self,
+                                     const gchar         *key,
+                                     const gchar * const *value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_STRV);
+  g_value_set_boxed (v, value);
+}
+
 gboolean
 ide_configuration_get_internal_boolean (IdeConfiguration *self,
                                         const gchar      *key)
diff --git a/libide/buildsystem/ide-configuration.h b/libide/buildsystem/ide-configuration.h
index 96be371..c82088e 100644
--- a/libide/buildsystem/ide-configuration.h
+++ b/libide/buildsystem/ide-configuration.h
@@ -79,12 +79,17 @@ IdeBuildCommandQueue *ide_configuration_get_prebuild         (IdeConfiguration
 IdeBuildCommandQueue *ide_configuration_get_postbuild        (IdeConfiguration  *self);
 const gchar          *ide_configuration_get_app_id           (IdeConfiguration  *self);
 void                  ide_configuration_set_app_id           (IdeConfiguration  *self,
-                                                              const gchar        *app_id);
+                                                              const gchar       *app_id);
 const gchar          *ide_configuration_get_internal_string  (IdeConfiguration  *self,
                                                               const gchar       *key);
 void                  ide_configuration_set_internal_string  (IdeConfiguration  *self,
                                                               const gchar       *key,
                                                               const gchar       *value);
+const gchar * const  *ide_configuration_get_internal_strv    (IdeConfiguration  *self,
+                                                              const gchar       *key);
+void                  ide_configuration_set_internal_strv    (IdeConfiguration  *self,
+                                                              const gchar       *key,
+                                                              const gchar * const *value);
 gboolean              ide_configuration_get_internal_boolean (IdeConfiguration  *self,
                                                               const gchar       *key);
 void                  ide_configuration_set_internal_boolean (IdeConfiguration  *self,
diff --git a/libide/ide-enums.c.in b/libide/ide-enums.c.in
index 0f71244..f6aba69 100644
--- a/libide/ide-enums.c.in
+++ b/libide/ide-enums.c.in
@@ -5,6 +5,8 @@
 #include "ide-enums.h"
 
 #include "buffers/ide-buffer.h"
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-pipeline.h"
 #include "buildsystem/ide-build-result.h"
 #include "devices/ide-device.h"
 #include "diagnostics/ide-diagnostic.h"
diff --git a/libide/ide-types.h b/libide/ide-types.h
index 230ebaa..c601342 100644
--- a/libide/ide-types.h
+++ b/libide/ide-types.h
@@ -35,11 +35,13 @@ typedef struct _IdeBufferChangeMonitor         IdeBufferChangeMonitor;
 
 typedef struct _IdeBufferManager               IdeBufferManager;
 
-typedef struct _IdeBuilder                     IdeBuilder;
 typedef struct _IdeBuildCommand                IdeBuildCommand;
 typedef struct _IdeBuildCommandQueue           IdeBuildCommandQueue;
+typedef struct _IdeBuilder                     IdeBuilder;
 typedef struct _IdeBuildManager                IdeBuildManager;
+typedef struct _IdeBuildPipeline               IdeBuildPipeline;
 typedef struct _IdeBuildResult                 IdeBuildResult;
+typedef struct _IdeBuildStage                  IdeBuildStage;
 typedef struct _IdeBuildSystem                 IdeBuildSystem;
 typedef struct _IdeBuildTarget                 IdeBuildTarget;
 
@@ -132,6 +134,7 @@ typedef struct _IdeSymbolResolver              IdeSymbolResolver;
 typedef struct _IdeSymbolResolverInterface     IdeSymbolResolverInterface;
 
 typedef struct _IdeTransferManager             IdeTransferManager;
+typedef struct _IdeTransfer                    IdeTransfer;
 
 typedef struct _IdeUnsavedFiles                IdeUnsavedFiles;
 
diff --git a/libide/ide.h b/libide/ide.h
index 78aa113..b5fc4a3 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -38,8 +38,14 @@ G_BEGIN_DECLS
 #include "buildsystem/ide-build-command.h"
 #include "buildsystem/ide-build-command-queue.h"
 #include "buildsystem/ide-build-manager.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-pipeline-addin.h"
 #include "buildsystem/ide-build-result-addin.h"
 #include "buildsystem/ide-build-result.h"
+#include "buildsystem/ide-build-stage.h"
+#include "buildsystem/ide-build-stage-launcher.h"
+#include "buildsystem/ide-build-stage-mkdirs.h"
+#include "buildsystem/ide-build-stage-transfer.h"
 #include "buildsystem/ide-build-system.h"
 #include "buildsystem/ide-build-target.h"
 #include "buildsystem/ide-builder.h"
@@ -138,6 +144,7 @@ G_BEGIN_DECLS
 #include "tree/ide-tree-node.h"
 #include "tree/ide-tree-types.h"
 #include "tree/ide-tree.h"
+#include "util/ide-directory-reaper.h"
 #include "util/ide-file-manager.h"
 #include "util/ide-flatpak.h"
 #include "util/ide-glib.h"
diff --git a/libide/subprocess/ide-subprocess-launcher.c b/libide/subprocess/ide-subprocess-launcher.c
index 6e4a0e9..c39c7ca 100644
--- a/libide/subprocess/ide-subprocess-launcher.c
+++ b/libide/subprocess/ide-subprocess-launcher.c
@@ -659,17 +659,25 @@ ide_subprocess_launcher_overlay_environment (IdeSubprocessLauncher *self,
 
 /**
  * ide_subprocess_launcher_push_args:
- * @args: (array zero-terminated=1) (element-type utf8): the arguments
+ * @self: A #IdeSubprocessLauncher
+ * @args: (array zero-terminated=1) (element-type utf8) (nullable): the arguments
+ *
+ * This function is semantically identical to calling ide_subprocess_launcher_push_argv()
+ * for each element of @args.
+ *
+ * If @args is %NULL, this function does nothing.
  */
 void
 ide_subprocess_launcher_push_args (IdeSubprocessLauncher *self,
                                    const gchar * const   *args)
 {
   g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
-  g_return_if_fail (args != NULL);
 
-  for (guint i = 0; args [i] != NULL; i++)
-    ide_subprocess_launcher_push_argv (self, args [i]);
+  if (args != NULL)
+    {
+      for (guint i = 0; args [i] != NULL; i++)
+        ide_subprocess_launcher_push_argv (self, args [i]);
+    }
 }
 
 gchar *
diff --git a/libide/transfers/ide-transfer-manager.c b/libide/transfers/ide-transfer-manager.c
index 26d5b46..2f9aa61 100644
--- a/libide/transfers/ide-transfer-manager.c
+++ b/libide/transfers/ide-transfer-manager.c
@@ -33,8 +33,11 @@ struct _IdeTransferManager
   GPtrArray *transfers;
 };
 
-static void list_model_iface_init     (GListModelInterface *iface);
-static void ide_transfer_manager_pump (IdeTransferManager  *self);
+static void list_model_iface_init                 (GListModelInterface *iface);
+static void ide_transfer_manager_pump             (IdeTransferManager  *self);
+static void ide_transfer_manager_execute_complete (IdeTransferManager *self,
+                                                   GTask              *task,
+                                                   const GError       *reason);
 
 G_DEFINE_TYPE_EXTENDED (IdeTransferManager, ide_transfer_manager, IDE_TYPE_OBJECT, 0,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
@@ -131,9 +134,11 @@ ide_transfer_manager_execute_cb (GObject      *object,
 
       context = ide_object_get_context (IDE_OBJECT (self));
       ide_context_warning (context, "%s", error->message);
-    }
 
-  g_signal_emit (self, signals [TRANSFER_COMPLETED], 0, transfer);
+      g_signal_emit (self, signals [TRANSFER_FAILED], 0, transfer, error);
+    }
+  else
+    g_signal_emit (self, signals [TRANSFER_COMPLETED], 0, transfer);
 
   ide_transfer_manager_pump (self);
 
@@ -363,6 +368,23 @@ ide_transfer_manager_class_init (IdeTransferManagerClass *klass)
                   0,
                   NULL, NULL, NULL,
                   G_TYPE_NONE, 1, IDE_TYPE_TRANSFER);
+
+  /**
+   * IdeTransferManager::transfer-failed:
+   * @self: An #IdeTransferManager
+   * @transfer: An #IdeTransfer
+   * @reason: (in): The reason for the failure.
+   *
+   * This signal is emitted when a transfer has failed to complete
+   * successfully.
+   */
+  signals [TRANSFER_FAILED] =
+    g_signal_new ("transfer-failed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_TRANSFER, G_TYPE_ERROR);
 }
 
 static void
@@ -524,3 +546,114 @@ ide_transfer_manager_get_progress (IdeTransferManager *self)
 
   return total / (gdouble)self->transfers->len;
 }
+
+static void
+ide_transfer_manager_execute_transfer_completed (IdeTransferManager *self,
+                                                 IdeTransfer        *transfer,
+                                                 GTask              *task)
+{
+  IdeTransfer *task_data;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (G_IS_TASK (task));
+
+  task_data = g_task_get_task_data (task);
+
+  if (task_data == transfer)
+    ide_transfer_manager_execute_complete (self, task, NULL);
+}
+
+static void
+ide_transfer_manager_execute_transfer_failed (IdeTransferManager *self,
+                                              IdeTransfer        *transfer,
+                                              const GError       *reason,
+                                              GTask              *task)
+{
+  IdeTransfer *task_data;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (reason != NULL);
+  g_assert (G_IS_TASK (task));
+
+  task_data = g_task_get_task_data (task);
+
+  if (task_data == transfer)
+    ide_transfer_manager_execute_complete (self, task, reason);
+}
+
+static void
+ide_transfer_manager_execute_complete (IdeTransferManager *self,
+                                       GTask              *task,
+                                       const GError       *reason)
+{
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+  g_assert (G_IS_TASK (task));
+
+  g_signal_handlers_disconnect_by_func (self,
+                                        G_CALLBACK (ide_transfer_manager_execute_transfer_completed),
+                                        task);
+
+  g_signal_handlers_disconnect_by_func (self,
+                                        G_CALLBACK (ide_transfer_manager_execute_transfer_failed),
+                                        task);
+
+  if (reason != NULL)
+    g_task_return_error (task, g_error_copy (reason));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_transfer_manager_execute_async:
+ *
+ * This is a convenience function that will queue @transfer into the transfer
+ * manager and execute callback upon completion of the transfer. The success
+ * or failure #GError will be propagated to the caller via
+ * ide_transfer_manager_execute_finish().
+ */
+void
+ide_transfer_manager_execute_async (IdeTransferManager  *self,
+                                    IdeTransfer         *transfer,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_TRANSFER (transfer));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_transfer_manager_execute_async);
+  g_task_set_task_data (task, g_object_ref (transfer), g_object_unref);
+
+  g_signal_connect_data (self,
+                         "transfer-completed",
+                         G_CALLBACK (ide_transfer_manager_execute_transfer_completed),
+                         g_object_ref (task),
+                         (GClosureNotify)g_object_unref,
+                         G_CONNECT_SWAPPED);
+
+  g_signal_connect_data (self,
+                         "transfer-failed",
+                         G_CALLBACK (ide_transfer_manager_execute_transfer_failed),
+                         g_object_ref (task),
+                         (GClosureNotify)g_object_unref,
+                         G_CONNECT_SWAPPED);
+
+  ide_transfer_manager_queue (self, transfer);
+}
+
+gboolean
+ide_transfer_manager_execute_finish (IdeTransferManager  *self,
+                                     GAsyncResult        *result,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/libide/transfers/ide-transfer-manager.h b/libide/transfers/ide-transfer-manager.h
index 2fa41b6..1463c7d 100644
--- a/libide/transfers/ide-transfer-manager.h
+++ b/libide/transfers/ide-transfer-manager.h
@@ -29,17 +29,25 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeTransferManager, ide_transfer_manager, IDE, TRANSFER_MANAGER, IdeObject)
 
-gdouble  ide_transfer_manager_get_progress   (IdeTransferManager *self);
-gboolean ide_transfer_manager_get_has_active (IdeTransferManager *self);
-guint    ide_transfer_manager_get_max_active (IdeTransferManager *self);
-void     ide_transfer_manager_set_max_active (IdeTransferManager *self,
-                                              guint               max_active);
-void     ide_transfer_manager_cancel         (IdeTransferManager *self,
-                                              IdeTransfer        *transfer);
-void     ide_transfer_manager_cancel_all     (IdeTransferManager *self);
-void     ide_transfer_manager_clear          (IdeTransferManager *self);
-void     ide_transfer_manager_queue          (IdeTransferManager *self,
-                                              IdeTransfer        *transfer);
+gdouble  ide_transfer_manager_get_progress   (IdeTransferManager   *self);
+gboolean ide_transfer_manager_get_has_active (IdeTransferManager   *self);
+guint    ide_transfer_manager_get_max_active (IdeTransferManager   *self);
+void     ide_transfer_manager_set_max_active (IdeTransferManager   *self,
+                                              guint                 max_active);
+void     ide_transfer_manager_cancel         (IdeTransferManager   *self,
+                                              IdeTransfer          *transfer);
+void     ide_transfer_manager_cancel_all     (IdeTransferManager   *self);
+void     ide_transfer_manager_clear          (IdeTransferManager   *self);
+void     ide_transfer_manager_queue          (IdeTransferManager   *self,
+                                              IdeTransfer          *transfer);
+void     ide_transfer_manager_execute_async  (IdeTransferManager   *self,
+                                              IdeTransfer          *transfer,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+gboolean ide_transfer_manager_execute_finish (IdeTransferManager   *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
 
 G_END_DECLS
 
diff --git a/libide/transfers/ide-transfer.c b/libide/transfers/ide-transfer.c
index 4bd1a5a..d4d1832 100644
--- a/libide/transfers/ide-transfer.c
+++ b/libide/transfers/ide-transfer.c
@@ -128,5 +128,8 @@ ide_transfer_has_completed (IdeTransfer *self)
 {
   g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
 
+  if (IDE_TRANSFER_GET_IFACE (self)->has_completed)
+    return IDE_TRANSFER_GET_IFACE (self)->has_completed (self);
+
   return !!g_object_get_data (G_OBJECT (self), "IDE_TRANSFER_COMPLETED");
 }
diff --git a/libide/transfers/ide-transfer.h b/libide/transfers/ide-transfer.h
index a8cdcf0..7dfe7fe 100644
--- a/libide/transfers/ide-transfer.h
+++ b/libide/transfers/ide-transfer.h
@@ -38,6 +38,7 @@ struct _IdeTransferInterface
   gboolean (*execute_finish) (IdeTransfer          *self,
                               GAsyncResult         *result,
                               GError              **error);
+  gboolean (*has_completed)  (IdeTransfer          *self);
 };
 
 gdouble  ide_transfer_get_progress   (IdeTransfer          *self);
diff --git a/libide/util/ide-directory-reaper.c b/libide/util/ide-directory-reaper.c
new file mode 100644
index 0000000..dff24e3
--- /dev/null
+++ b/libide/util/ide-directory-reaper.c
@@ -0,0 +1,353 @@
+/* ide-directory-reaper.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-directory-reaper"
+
+#include "ide-directory-reaper.h"
+
+typedef enum
+{
+  PATTERN_FILE,
+  PATTERN_GLOB,
+} PatternType;
+
+typedef struct
+{
+  PatternType type;
+  GTimeSpan   min_age;
+  union {
+    struct {
+      GFile *directory;
+      gchar *glob;
+    } glob;
+    struct {
+      GFile *file;
+    } file;
+  };
+} Pattern;
+
+struct _IdeDirectoryReaper
+{
+  GObject  parent_instance;
+  GArray  *patterns;
+};
+
+G_DEFINE_TYPE (IdeDirectoryReaper, ide_directory_reaper, G_TYPE_OBJECT)
+
+static void
+clear_pattern (gpointer data)
+{
+  Pattern *p = data;
+
+  switch (p->type)
+    {
+    case PATTERN_GLOB:
+      g_clear_object (&p->glob.directory);
+      g_clear_pointer (&p->glob.glob, g_free);
+      break;
+
+    case PATTERN_FILE:
+      g_clear_object (&p->file.file);
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+static void
+ide_directory_reaper_finalize (GObject *object)
+{
+  IdeDirectoryReaper *self = (IdeDirectoryReaper *)object;
+
+  g_clear_pointer (&self->patterns, g_array_unref);
+
+  G_OBJECT_CLASS (ide_directory_reaper_parent_class)->finalize (object);
+}
+
+static void
+ide_directory_reaper_class_init (IdeDirectoryReaperClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_directory_reaper_finalize;
+}
+
+static void
+ide_directory_reaper_init (IdeDirectoryReaper *self)
+{
+  self->patterns = g_array_new (FALSE, FALSE, sizeof (Pattern));
+  g_array_set_clear_func (self->patterns, clear_pattern);
+}
+
+void
+ide_directory_reaper_add_directory (IdeDirectoryReaper *self,
+                                    GFile              *directory,
+                                    GTimeSpan           min_age)
+{
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (self));
+  g_return_if_fail (G_IS_FILE (directory));
+
+  ide_directory_reaper_add_glob (self, directory, NULL, min_age);
+}
+
+void
+ide_directory_reaper_add_glob (IdeDirectoryReaper *self,
+                               GFile              *directory,
+                               const gchar        *glob,
+                               GTimeSpan           min_age)
+{
+  Pattern p = { 0 };
+
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (self));
+  g_return_if_fail (G_IS_FILE (directory));
+
+  p.type = PATTERN_GLOB;
+  p.min_age = min_age;
+  p.glob.directory = g_object_ref (directory);
+  p.glob.glob = g_strdup (glob);
+
+  g_array_append_val (self->patterns, p);
+}
+
+void
+ide_directory_reaper_add_file (IdeDirectoryReaper *self,
+                               GFile              *file,
+                               GTimeSpan           min_age)
+{
+  Pattern p = { 0 };
+
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  p.type = PATTERN_FILE;
+  p.min_age = min_age;
+  p.file.file = g_object_ref (file);
+
+  g_array_append_val (self->patterns, p);
+}
+
+IdeDirectoryReaper *
+ide_directory_reaper_new (void)
+{
+  return g_object_new (IDE_TYPE_DIRECTORY_REAPER, NULL);
+}
+
+static gboolean
+has_expired (guint64 mtime,
+             guint64 min_age)
+{
+  guint64 now = g_get_real_time () / G_USEC_PER_SEC;
+
+  if (now > min_age)
+    return (now - min_age) > mtime;
+
+  return FALSE;
+}
+
+static void
+ide_directory_reaper_execute_worker (GTask        *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  GArray *patterns = task_data;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (IDE_IS_DIRECTORY_REAPER (source_object));
+  g_assert (patterns != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (guint i = 0; i < patterns->len; i++)
+    {
+      const Pattern *p = &g_array_index (patterns, Pattern, i);
+      g_autoptr(GFileInfo) info = NULL;
+      g_autoptr(GPatternSpec) spec = NULL;
+      g_autoptr(GFileEnumerator) enumerator = NULL;
+      g_autoptr(GError) error = NULL;
+      guint64 v64;
+
+      switch (p->type)
+        {
+        case PATTERN_FILE:
+
+          info = g_file_query_info (p->file.file,
+                                    G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                                    G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                    cancellable,
+                                    &error);
+
+          if (info == NULL)
+            {
+              if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+                g_warning ("%s", error->message);
+              break;
+            }
+
+          v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+
+          if (has_expired (v64, p->min_age))
+            {
+              if (!g_file_delete (p->file.file, cancellable, &error))
+                g_warning ("%s", error->message);
+            }
+
+          break;
+
+        case PATTERN_GLOB:
+
+          spec = g_pattern_spec_new (p->glob.glob);
+
+          if (spec == NULL)
+            {
+              g_warning ("Invalid pattern spec \"%s\"", p->glob.glob);
+              break;
+            }
+
+          enumerator = g_file_enumerate_children (p->glob.directory,
+                                                  G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                                  G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                                                  G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                                  cancellable,
+                                                  &error);
+
+          if (enumerator == NULL)
+            {
+              if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+                g_warning ("%s", error->message);
+              break;
+            }
+
+          while (NULL != (info = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
+            {
+              const gchar *name;
+
+              name = g_file_info_get_name (info);
+              v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+
+              if (has_expired (v64, p->min_age))
+                {
+                  g_autoptr(GFile) file = g_file_get_child (p->glob.directory, name);
+
+                  if (!g_file_delete (file, cancellable, &error))
+                    {
+                      g_warning ("%s", error->message);
+                      g_clear_error (&error);
+                    }
+                }
+
+              g_clear_object (&info);
+            }
+
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static GArray *
+ide_directory_reaper_copy_state (IdeDirectoryReaper *self)
+{
+  g_autoptr(GArray) copy = NULL;
+
+  g_assert (IDE_IS_DIRECTORY_REAPER (self));
+  g_assert (self->patterns != NULL);
+
+  copy = g_array_new (FALSE, FALSE, sizeof (Pattern));
+  g_array_set_clear_func (copy, clear_pattern);
+
+  for (guint i = 0; i < self->patterns->len; i++)
+    {
+      Pattern p = g_array_index (self->patterns, Pattern, i);
+
+      switch (p.type)
+        {
+        case PATTERN_GLOB:
+          p.glob.directory = g_object_ref (p.glob.directory);
+          p.glob.glob = g_strdup (p.glob.glob);
+          break;
+
+        case PATTERN_FILE:
+          p.file.file = g_object_ref (p.file.file);
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      g_array_append_val (copy, p);
+    }
+
+  return g_steal_pointer (&copy);
+}
+
+void
+ide_directory_reaper_execute_async (IdeDirectoryReaper  *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GArray) copy = NULL;
+
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  copy = ide_directory_reaper_copy_state (self);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_directory_reaper_execute_async);
+  g_task_set_task_data (task, g_steal_pointer (&copy), (GDestroyNotify)g_array_unref);
+  g_task_run_in_thread (task, ide_directory_reaper_execute_worker);
+}
+
+gboolean
+ide_directory_reaper_execute_finish (IdeDirectoryReaper  *self,
+                                     GAsyncResult        *result,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_DIRECTORY_REAPER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+ide_directory_reaper_execute (IdeDirectoryReaper  *self,
+                              GCancellable        *cancellable,
+                              GError             **error)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GArray) copy = NULL;
+
+  g_return_val_if_fail (IDE_IS_DIRECTORY_REAPER (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  copy = ide_directory_reaper_copy_state (self);
+
+  task = g_task_new (self, cancellable, NULL, NULL);
+  g_task_set_source_tag (task, ide_directory_reaper_execute);
+  g_task_set_task_data (task, g_steal_pointer (&copy), (GDestroyNotify)g_array_unref);
+  g_task_run_in_thread_sync (task, ide_directory_reaper_execute_worker);
+
+  return g_task_propagate_boolean (task, error);
+}
diff --git a/libide/util/ide-directory-reaper.h b/libide/util/ide-directory-reaper.h
new file mode 100644
index 0000000..9602a1a
--- /dev/null
+++ b/libide/util/ide-directory-reaper.h
@@ -0,0 +1,54 @@
+/* ide-directory-reaper.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_DIRECTORY_REAPER_H
+#define IDE_DIRECTORY_REAPER_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIRECTORY_REAPER (ide_directory_reaper_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDirectoryReaper, ide_directory_reaper, IDE, DIRECTORY_REAPER, GObject)
+
+IdeDirectoryReaper *ide_directory_reaper_new               (void);
+void                ide_directory_reaper_add_directory     (IdeDirectoryReaper   *self,
+                                                            GFile                *directory,
+                                                            GTimeSpan             min_age);
+void                ide_directory_reaper_add_file          (IdeDirectoryReaper   *self,
+                                                            GFile                *file,
+                                                            GTimeSpan             min_age);
+void                ide_directory_reaper_add_glob          (IdeDirectoryReaper   *self,
+                                                            GFile                *directory,
+                                                            const gchar          *glob,
+                                                            GTimeSpan             min_age);
+gboolean            ide_directory_reaper_execute           (IdeDirectoryReaper   *self,
+                                                            GCancellable         *cancellable,
+                                                            GError              **error);
+void                ide_directory_reaper_execute_async     (IdeDirectoryReaper   *self,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+gboolean            ide_directory_reaper_execute_finish    (IdeDirectoryReaper   *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+
+G_END_DECLS
+
+#endif /* IDE_DIRECTORY_REAPER_H */
diff --git a/plugins/autotools/Makefile.am b/plugins/autotools/Makefile.am
index 09e4ebb..9f87046 100644
--- a/plugins/autotools/Makefile.am
+++ b/plugins/autotools/Makefile.am
@@ -8,6 +8,8 @@ libautotools_plugin_la_SOURCES = \
        autotools-plugin.c \
        ide-autotools-application-addin.c \
        ide-autotools-application-addin.h \
+       ide-autotools-autogen-stage.c \
+       ide-autotools-autogen-stage.h \
        ide-autotools-builder.c \
        ide-autotools-builder.h \
        ide-autotools-build-system.c \
@@ -16,6 +18,8 @@ libautotools_plugin_la_SOURCES = \
        ide-autotools-build-target.h \
        ide-autotools-build-task.c \
        ide-autotools-build-task.h \
+       ide-autotools-pipeline-addin.c \
+       ide-autotools-pipeline-addin.h \
        ide-autotools-project-miner.c \
        ide-autotools-project-miner.h \
        ide-makecache.c \
diff --git a/plugins/autotools/autotools-plugin.c b/plugins/autotools/autotools-plugin.c
index 9e15698..c1f94e7 100644
--- a/plugins/autotools/autotools-plugin.c
+++ b/plugins/autotools/autotools-plugin.c
@@ -21,12 +21,14 @@
 
 #include "ide-autotools-application-addin.h"
 #include "ide-autotools-build-system.h"
+#include "ide-autotools-pipeline-addin.h"
 #include "ide-autotools-project-miner.h"
 
 void
 peas_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module, IDE_TYPE_APPLICATION_ADDIN, 
IDE_TYPE_AUTOTOOLS_APPLICATION_ADDIN);
+  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN, 
IDE_TYPE_AUTOTOOLS_PIPELINE_ADDIN);
   peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_SYSTEM, 
IDE_TYPE_AUTOTOOLS_BUILD_SYSTEM);
   peas_object_module_register_extension_type (module, IDE_TYPE_PROJECT_MINER, 
IDE_TYPE_AUTOTOOLS_PROJECT_MINER);
 }
diff --git a/plugins/autotools/ide-autotools-application-addin.c 
b/plugins/autotools/ide-autotools-application-addin.c
index 51917fd..ac55f6a 100644
--- a/plugins/autotools/ide-autotools-application-addin.c
+++ b/plugins/autotools/ide-autotools-application-addin.c
@@ -38,6 +38,10 @@ ide_autotools_application_addin_load (IdeApplicationAddin *addin,
   g_assert (IDE_IS_AUTOTOOLS_APPLICATION_ADDIN (addin));
   g_assert (IDE_IS_APPLICATION (application));
 
+  /*
+   * TODO: Move this to an IdeDirectoryReaper
+   */
+
   path = g_build_filename (g_get_user_cache_dir (),
                            "gnome-builder",
                            "makecache",
diff --git a/plugins/autotools/ide-autotools-autogen-stage.c b/plugins/autotools/ide-autotools-autogen-stage.c
new file mode 100644
index 0000000..99dc54e
--- /dev/null
+++ b/plugins/autotools/ide-autotools-autogen-stage.c
@@ -0,0 +1,179 @@
+/* ide-autotools-autogen-stage.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-autotools-autogen-stage"
+
+#include "ide-autotools-autogen-stage.h"
+
+struct _IdeAutotoolsAutogenStage
+{
+  IdeBuildStage parent_instance;
+
+  gchar *srcdir;
+};
+
+G_DEFINE_TYPE (IdeAutotoolsAutogenStage, ide_autotools_autogen_stage, IDE_TYPE_BUILD_STAGE)
+
+enum {
+  PROP_0,
+  PROP_SRCDIR,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_autotools_autogen_stage_wait_check_cb (GObject      *object,
+                                           GAsyncResult *result,
+                                           gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_autotools_autogen_stage_execute_async (IdeBuildStage       *stage,
+                                           IdeBuildPipeline    *pipeline,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeAutotoolsAutogenStage *self = (IdeAutotoolsAutogenStage *)stage;
+  g_autofree gchar *autogen_path = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_AUTOTOOLS_AUTOGEN_STAGE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_autotools_autogen_stage_execute_async);
+
+  autogen_path = g_build_filename (self->srcdir, "autogen.sh", NULL);
+
+  launcher = ide_build_pipeline_create_launcher (pipeline, &error);
+
+  if (launcher == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  ide_subprocess_launcher_set_cwd (launcher, self->srcdir);
+
+  if (g_file_test (autogen_path, G_FILE_TEST_IS_REGULAR))
+    {
+      ide_subprocess_launcher_push_argv (launcher, autogen_path);
+      ide_subprocess_launcher_setenv (launcher, "NOCONFIGURE", "1", TRUE);
+    }
+  else
+    {
+      ide_subprocess_launcher_push_argv (launcher, "autoreconf");
+      ide_subprocess_launcher_push_argv (launcher, "-fiv");
+    }
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  ide_build_stage_log_subprocess (stage, subprocess);
+
+  ide_subprocess_wait_check_async (subprocess,
+                                   cancellable,
+                                   ide_autotools_autogen_stage_wait_check_cb,
+                                   g_steal_pointer (&task));
+}
+
+static gboolean
+ide_autotools_autogen_stage_execute_finish (IdeBuildStage  *stage,
+                                            GAsyncResult   *result,
+                                            GError        **error)
+{
+  g_assert (IDE_IS_AUTOTOOLS_AUTOGEN_STAGE (stage));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_autotools_autogen_stage_finalize (GObject *object)
+{
+  IdeAutotoolsAutogenStage *self = (IdeAutotoolsAutogenStage *)object;
+
+  g_clear_pointer (&self->srcdir, g_free);
+
+  G_OBJECT_CLASS (ide_autotools_autogen_stage_parent_class)->finalize (object);
+}
+
+static void
+ide_autotools_autogen_stage_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  IdeAutotoolsAutogenStage *self = (IdeAutotoolsAutogenStage *)object;
+
+  switch (prop_id)
+    {
+    case PROP_SRCDIR:
+      self->srcdir = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_autotools_autogen_stage_class_init (IdeAutotoolsAutogenStageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_autotools_autogen_stage_finalize;
+  object_class->set_property = ide_autotools_autogen_stage_set_property;
+
+  stage_class->execute_async = ide_autotools_autogen_stage_execute_async;
+  stage_class->execute_finish = ide_autotools_autogen_stage_execute_finish;
+
+  properties [PROP_SRCDIR] =
+    g_param_spec_string ("srcdir", NULL, NULL, NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_autotools_autogen_stage_init (IdeAutotoolsAutogenStage *self)
+{
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/autotools/ide-autotools-autogen-stage.h
similarity index 62%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to plugins/autotools/ide-autotools-autogen-stage.h
index 2a77a0b..5dd2c6c 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/autotools/ide-autotools-autogen-stage.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* ide-autotools-autogen-stage.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef IDE_AUTOTOOLS_AUTOGEN_STAGE_H
+#define IDE_AUTOTOOLS_AUTOGEN_STAGE_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define IDE_TYPE_AUTOTOOLS_AUTOGEN_STAGE (ide_autotools_autogen_stage_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (IdeAutotoolsAutogenStage, ide_autotools_autogen_stage, IDE, AUTOTOOLS_AUTOGEN_STAGE, 
IdeBuildStage)
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* IDE_AUTOTOOLS_AUTOGEN_STAGE_H */
diff --git a/plugins/autotools/ide-autotools-pipeline-addin.c 
b/plugins/autotools/ide-autotools-pipeline-addin.c
new file mode 100644
index 0000000..14c9c96
--- /dev/null
+++ b/plugins/autotools/ide-autotools-pipeline-addin.c
@@ -0,0 +1,259 @@
+/* ide-autotools-pipeline-addin.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-autotools-pipeline-addin"
+
+#include "ide-autotools-autogen-stage.h"
+#include "ide-autotools-build-system.h"
+#include "ide-autotools-pipeline-addin.h"
+
+static gboolean
+register_autoreconf_stage (IdeAutotoolsPipelineAddin  *self,
+                           IdeBuildPipeline           *pipeline,
+                           GError                    **error)
+{
+  g_autofree gchar *configure_path = NULL;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeContext *context;
+  const gchar *srcdir;
+  gboolean completed;
+  guint stage_id;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  configure_path = ide_build_pipeline_build_srcdir_path (pipeline, "configure", NULL);
+  completed = g_file_test (configure_path, G_FILE_TEST_IS_REGULAR);
+  srcdir = ide_build_pipeline_get_srcdir (pipeline);
+
+  stage = g_object_new (IDE_TYPE_AUTOTOOLS_AUTOGEN_STAGE,
+                        "completed", completed,
+                        "context", context,
+                        "srcdir", srcdir,
+                        NULL);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_AUTOGEN, 0, stage);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static gboolean
+register_configure_stage (IdeAutotoolsPipelineAddin  *self,
+                          IdeBuildPipeline           *pipeline,
+                          GError                    **error)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeConfiguration *configuration;
+  g_autofree gchar *configure_path = NULL;
+  g_autofree gchar *makefile_path = NULL;
+  const gchar *config_opts;
+  gboolean completed = TRUE;
+  guint stage_id;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  launcher = ide_build_pipeline_create_launcher (pipeline, error);
+
+  if (launcher == NULL)
+    return FALSE;
+
+  configure_path = ide_build_pipeline_build_srcdir_path (pipeline, "configure", NULL);
+  makefile_path = ide_build_pipeline_build_builddir_path (pipeline, "Makefile", NULL);
+  ide_subprocess_launcher_push_argv (launcher, configure_path);
+  ide_subprocess_launcher_set_cwd (launcher, ide_build_pipeline_get_builddir (pipeline));
+
+  /*
+   * Parse the configure options as defined in the build configuration and append
+   * them to configure.
+   */
+
+  configuration = ide_build_pipeline_get_configuration (pipeline);
+  config_opts = ide_configuration_get_config_opts (configuration);
+
+  if (config_opts != NULL)
+    {
+      g_auto(GStrv) argv = NULL;
+      gint argc;
+
+      if (!g_shell_parse_argv (config_opts, &argc, &argv, error))
+        return FALSE;
+
+      for (guint i = 0; i < argc; i++)
+        ide_subprocess_launcher_push_argv (launcher, argv[i]);
+    }
+
+  /*
+   * If the Makefile exists within the builddir, we will assume the
+   * project has been initially configured correctly. Otherwise, every
+   * time the user opens the project they have to go through a full
+   * re-configure and build.
+   *
+   * Should the user need to perform an autogen, a manual rebuild is
+   * easily achieved so this seems to be the sensible default.
+   *
+   * If we were to do this "correctly", we would look at config.status to
+   * match the "ac_cs_config" variable to what we set. However, that is
+   * influenced by environment variables, so its a bit non-trivial.
+   */
+  completed = g_file_test (makefile_path, G_FILE_TEST_IS_REGULAR);
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                        "completed", completed,
+                        "context", ide_object_get_context (IDE_OBJECT (self)),
+                        "launcher", launcher,
+                        NULL);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_AUTOGEN, 0, stage);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+G_GNUC_NULL_TERMINATED static gboolean
+register_make_stage (IdeAutotoolsPipelineAddin  *self,
+                     IdeBuildPipeline           *pipeline,
+                     IdeRuntime                 *runtime,
+                     const gchar                *log_file,
+                     IdeBuildPhase               phase,
+                     GError                    **error,
+                     const gchar                *first_target,
+                     ...)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeConfiguration *configuration;
+  g_autofree gchar *j = NULL;
+  const gchar *make = "make";
+  IdeContext *context;
+  guint stage_id;
+  gint parallel;
+  va_list args;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_RUNTIME (runtime));
+
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+
+  launcher = ide_build_pipeline_create_launcher (pipeline, error);
+
+  if (launcher == NULL)
+    return FALSE;
+
+  if (ide_runtime_contains_program_in_path (runtime, "gmake", NULL))
+    make = "gmake";
+
+  parallel = ide_configuration_get_parallelism (configuration);
+
+  if (parallel == -1)
+    j = g_strdup_printf ("-j%u", g_get_num_processors () + 1);
+  else if (parallel == 0)
+    j = g_strdup_printf ("-j%u", g_get_num_processors ());
+  else
+    j = g_strdup_printf ("-j%u", parallel);
+
+  ide_subprocess_launcher_set_cwd (launcher, ide_build_pipeline_get_builddir (pipeline));
+
+  ide_subprocess_launcher_push_argv (launcher, make);
+  ide_subprocess_launcher_push_argv (launcher, j);
+
+  va_start (args, first_target);
+  do
+    {
+      ide_subprocess_launcher_push_argv (launcher, first_target);
+    }
+  while (NULL != (first_target = va_arg (args, const gchar *)));
+  va_end (args);
+
+  stage = ide_build_stage_launcher_new (context, launcher);
+
+  if (log_file != NULL)
+    ide_build_stage_set_stdout_path (stage, log_file);
+
+  stage_id = ide_build_pipeline_connect (pipeline, phase, 0, stage);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static void
+ide_autotools_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                                   IdeBuildPipeline      *pipeline)
+{
+  IdeAutotoolsPipelineAddin *self = (IdeAutotoolsPipelineAddin *)addin;
+  g_autofree gchar *makecache = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeConfiguration *config;
+  IdeBuildSystem *build_system;
+  IdeContext *context;
+  IdeRuntime *runtime;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  context = ide_object_get_context (IDE_OBJECT (addin));
+  build_system = ide_context_get_build_system (context);
+  config = ide_build_pipeline_get_configuration (pipeline);
+  runtime = ide_configuration_get_runtime (config);
+
+  if (!IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system))
+    return;
+
+  makecache = ide_build_pipeline_build_builddir_path (pipeline, "Makecache", NULL);
+
+  if (!register_autoreconf_stage (self, pipeline, &error) ||
+      !register_configure_stage (self, pipeline, &error) ||
+      !register_make_stage (self, pipeline, runtime, NULL, IDE_BUILD_PHASE_BUILD, &error, "all", NULL) ||
+      !register_make_stage (self, pipeline, runtime, makecache, 
IDE_BUILD_PHASE_CONFIGURE|IDE_BUILD_PHASE_AFTER, &error, "-n", "-p", "-s", NULL) ||
+      !register_make_stage (self, pipeline, runtime, NULL, IDE_BUILD_PHASE_INSTALL, &error, "install", NULL))
+    {
+      g_assert (error != NULL);
+      g_warning ("Failed to create autotools launcher: %s", error->message);
+      return;
+    }
+}
+
+/* GObject Boilerplate */
+
+static void
+addin_iface_init (IdeBuildPipelineAddinInterface *iface)
+{
+  iface->load = ide_autotools_pipeline_addin_load;
+}
+
+struct _IdeAutotoolsPipelineAddin { IdeObject parent; };
+
+G_DEFINE_TYPE_WITH_CODE (IdeAutotoolsPipelineAddin, ide_autotools_pipeline_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN, addin_iface_init))
+
+static void
+ide_autotools_pipeline_addin_class_init (IdeAutotoolsPipelineAddinClass *klass)
+{
+}
+
+static void
+ide_autotools_pipeline_addin_init (IdeAutotoolsPipelineAddin *self)
+{
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/autotools/ide-autotools-pipeline-addin.h
similarity index 62%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to plugins/autotools/ide-autotools-pipeline-addin.h
index 2a77a0b..d3535f3 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/autotools/ide-autotools-pipeline-addin.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* ide-autotools-pipeline-addin.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef IDE_AUTOTOOLS_PIPELINE_ADDIN_H
+#define IDE_AUTOTOOLS_PIPELINE_ADDIN_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define IDE_TYPE_AUTOTOOLS_PIPELINE_ADDIN (ide_autotools_pipeline_addin_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (IdeAutotoolsPipelineAddin, ide_autotools_pipeline_addin, IDE, 
AUTOTOOLS_PIPELINE_ADDIN, IdeObject)
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* IDE_AUTOTOOLS_PIPELINE_ADDIN_H */
diff --git a/plugins/flatpak/Makefile.am b/plugins/flatpak/Makefile.am
index a5324bd..fc683bd 100644
--- a/plugins/flatpak/Makefile.am
+++ b/plugins/flatpak/Makefile.am
@@ -10,23 +10,29 @@ plugin_LTLIBRARIES = libflatpak-plugin.la
 dist_plugin_DATA = flatpak.plugin
 
 libflatpak_plugin_la_SOURCES = \
-       gbp-flatpak-runtime-provider.c \
-       gbp-flatpak-runtime-provider.h \
-       gbp-flatpak-runtime.c \
-       gbp-flatpak-runtime.h \
-       gbp-flatpak-subprocess-launcher.c \
-       gbp-flatpak-subprocess-launcher.h \
-       gbp-flatpak-plugin.c \
-       gbp-flatpak-runner.c \
-       gbp-flatpak-runner.h \
        gbp-flatpak-application-addin.c \
        gbp-flatpak-application-addin.h \
        gbp-flatpak-clone-widget.c \
        gbp-flatpak-clone-widget.h \
        gbp-flatpak-genesis-addin.c \
        gbp-flatpak-genesis-addin.h \
+       gbp-flatpak-pipeline-addin.c \
+       gbp-flatpak-pipeline-addin.h \
+       gbp-flatpak-plugin.c \
+       gbp-flatpak-runner.c \
+       gbp-flatpak-runner.h \
+       gbp-flatpak-runtime-provider.c \
+       gbp-flatpak-runtime-provider.h \
+       gbp-flatpak-runtime.c \
+       gbp-flatpak-runtime.h \
        gbp-flatpak-sources.c \
        gbp-flatpak-sources.h \
+       gbp-flatpak-subprocess-launcher.c \
+       gbp-flatpak-subprocess-launcher.h \
+       gbp-flatpak-transfer.c \
+       gbp-flatpak-transfer.h \
+       gbp-flatpak-util.c \
+       gbp-flatpak-util.h \
        $(NULL)
 
 nodist_libflatpak_plugin_la_SOURCES = \
diff --git a/plugins/flatpak/gbp-flatpak-pipeline-addin.c b/plugins/flatpak/gbp-flatpak-pipeline-addin.c
new file mode 100644
index 0000000..9bc4255
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-pipeline-addin.c
@@ -0,0 +1,485 @@
+/* gbp-flatpak-pipeline-addin.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-flatpak-pipeline-addin"
+
+#include "gbp-flatpak-pipeline-addin.h"
+#include "gbp-flatpak-runtime.h"
+#include "gbp-flatpak-transfer.h"
+#include "gbp-flatpak-util.h"
+
+enum {
+  PREPARE_MKDIRS,
+  PREPARE_REMOTES,
+  PREPARE_BUILD_INIT,
+};
+
+static IdeSubprocessLauncher *
+create_subprocess_launcher (void)
+{
+  IdeSubprocessLauncher *launcher;
+
+  launcher = ide_subprocess_launcher_new (0);
+  ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
+
+  return launcher;
+}
+
+static gboolean
+register_mkdirs_stage (GbpFlatpakPipelineAddin  *self,
+                       IdeBuildPipeline         *pipeline,
+                       IdeContext               *context,
+                       GError                  **error)
+{
+  g_autoptr(IdeBuildStage) mkdirs = NULL;
+  IdeConfiguration *config;
+  g_autofree gchar *repo_dir = NULL;
+  g_autofree gchar *staging_dir = NULL;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  mkdirs = ide_build_stage_mkdirs_new (context);
+
+  repo_dir = gbp_flatpak_get_repo_dir (config);
+  staging_dir = gbp_flatpak_get_staging_dir (config);
+
+  ide_build_stage_mkdirs_add_path (IDE_BUILD_STAGE_MKDIRS (mkdirs), repo_dir, TRUE, 0750);
+  ide_build_stage_mkdirs_add_path (IDE_BUILD_STAGE_MKDIRS (mkdirs), staging_dir, TRUE, 0750);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_PREPARE, PREPARE_MKDIRS, mkdirs);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static gboolean
+register_remotes_stage (GbpFlatpakPipelineAddin  *self,
+                        IdeBuildPipeline         *pipeline,
+                        IdeContext               *context,
+                        GError                  **error)
+{
+  g_autoptr(IdeBuildStage) stage = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  IdeConfiguration *config;
+  const gchar *branch;
+  const gchar *platform;
+  const gchar *sdk;
+  const gchar *repo_name = NULL;
+  const gchar *repo_path = NULL;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  platform = ide_configuration_get_internal_string (config, "flatpak-platform");
+  sdk = ide_configuration_get_internal_string (config, "flatpak-sdk");
+  branch = ide_configuration_get_internal_string (config, "flatpak-branch");
+
+  if (ide_str_equal0 (platform, "org.gnome.Platform") ||
+      ide_str_equal0 (platform, "org.gnome.Sdk") ||
+      ide_str_equal0 (sdk, "org.gnome.Platform") ||
+      ide_str_equal0 (sdk, "org.gnome.Sdk"))
+    {
+      if (ide_str_equal0 (branch, "master"))
+        {
+          repo_name = "gnome-nightly";
+          repo_path = "https://sdk.gnome.org/gnome-nightly.flatpakrepo";;
+        }
+      else
+        {
+          repo_name = "gnome";
+          repo_path = "https://sdk.gnome.org/gnome.flatpakrepo";;
+        }
+    }
+
+  if (repo_name == NULL || repo_path == NULL)
+    return TRUE;
+
+  launcher = create_subprocess_launcher ();
+
+  ide_subprocess_launcher_push_argv (launcher, "flatpak");
+  ide_subprocess_launcher_push_argv (launcher, "remote-add");
+  ide_subprocess_launcher_push_argv (launcher, "--user");
+  ide_subprocess_launcher_push_argv (launcher, "--if-not-exists");
+  ide_subprocess_launcher_push_argv (launcher, "--from");
+  ide_subprocess_launcher_push_argv (launcher, repo_name);
+  ide_subprocess_launcher_push_argv (launcher, repo_path);
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                        "launcher", launcher,
+                        "context", context,
+                        NULL);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_PREPARE, PREPARE_REMOTES, stage);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static gboolean
+register_download_stage (GbpFlatpakPipelineAddin  *self,
+                         IdeBuildPipeline         *pipeline,
+                         IdeContext               *context,
+                         GError                  **error)
+{
+  IdeConfiguration *config;
+  const gchar *items[2] = { NULL };
+  const gchar *platform;
+  const gchar *sdk;
+  const gchar *branch;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+  platform = ide_configuration_get_internal_string (config, "flatpak-platform");
+  sdk = ide_configuration_get_internal_string (config, "flatpak-sdk");
+  branch = ide_configuration_get_internal_string (config, "flatpak-branch");
+
+  items[0] = platform;
+  items[1] = sdk;
+
+  for (guint i = 0; i < G_N_ELEMENTS (items); i++)
+    {
+      g_autoptr(IdeBuildStage) stage = NULL;
+      g_autoptr(IdeTransfer) transfer = NULL;
+      const gchar *id = items[i];
+
+      if (id == NULL)
+        continue;
+
+      transfer = g_object_new (GBP_TYPE_FLATPAK_TRANSFER,
+                               "context", context,
+                               "id", id,
+                               "branch", branch,
+                               NULL);
+
+      stage = g_object_new (IDE_TYPE_BUILD_STAGE_TRANSFER,
+                            "context", context,
+                            "transfer", transfer,
+                            NULL);
+
+      stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_DOWNLOADS, i, stage);
+      ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+    }
+
+  return TRUE;
+}
+
+static void
+check_if_file_exists (IdeBuildStage    *stage,
+                      IdeBuildPipeline *pipeline,
+                      GCancellable     *cancellable,
+                      const gchar      *file_path)
+{
+  gboolean exists;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (file_path != NULL);
+
+  exists = g_file_test (file_path, G_FILE_TEST_IS_REGULAR);
+  ide_build_stage_set_completed (stage, exists);
+}
+
+static gboolean
+register_build_init_stage (GbpFlatpakPipelineAddin  *self,
+                           IdeBuildPipeline         *pipeline,
+                           IdeContext               *context,
+                           GError                  **error)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  g_autofree gchar *staging_dir = NULL;
+  g_autofree gchar *manifest_path = NULL;
+  IdeConfiguration *config;
+  const gchar *app_id;
+  const gchar *platform;
+  const gchar *sdk;
+  const gchar *branch;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  launcher = create_subprocess_launcher ();
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  staging_dir = gbp_flatpak_get_staging_dir (config);
+  platform = ide_configuration_get_internal_string (config, "flatpak-platform");
+  app_id = ide_configuration_get_app_id (config);
+  sdk = ide_configuration_get_internal_string (config, "flatpak-sdk");
+  branch = ide_configuration_get_internal_string (config, "flatpak-branch");
+
+  manifest_path = g_build_filename (staging_dir, "manifest", NULL);
+
+  ide_subprocess_launcher_push_argv (launcher, "flatpak");
+  ide_subprocess_launcher_push_argv (launcher, "build-init");
+  ide_subprocess_launcher_push_argv (launcher, staging_dir);
+  ide_subprocess_launcher_push_argv (launcher, app_id);
+  ide_subprocess_launcher_push_argv (launcher, sdk);
+  ide_subprocess_launcher_push_argv (launcher, platform);
+  ide_subprocess_launcher_push_argv (launcher, branch);
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE,
+                        "context", context,
+                        "launcher", launcher,
+                        NULL);
+
+  /*
+   * We want to avoid calling build-init if it has already been called.
+   * To check if this has happened, we just look for the manifest file
+   * located in the directory that had build-init called.
+   */
+  g_signal_connect_data (stage,
+                         "query",
+                         G_CALLBACK (check_if_file_exists),
+                         g_steal_pointer (&manifest_path),
+                         (GClosureNotify)g_free,
+                         G_CONNECT_SWAPPED);
+
+  stage_id = ide_build_pipeline_connect (pipeline,
+                                         IDE_BUILD_PHASE_PREPARE,
+                                         PREPARE_BUILD_INIT,
+                                         stage);
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static gboolean
+register_dependencies_stage (GbpFlatpakPipelineAddin  *self,
+                             IdeBuildPipeline         *pipeline,
+                             IdeContext               *context,
+                             GError                  **error)
+{
+  g_autoptr(IdeBuildStage) stage = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autofree gchar *staging_dir = NULL;
+  g_autofree gchar *stop_at_option = NULL;
+  IdeConfiguration *config;
+  const gchar *manifest_path;
+  const gchar *primary_module;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  primary_module = ide_configuration_get_internal_string (config, "flatpak-module");
+  manifest_path = ide_configuration_get_internal_string (config, "flatpak-manifest");
+
+  /* If there is no manifest, then there are no dependencies
+   * to build for this configuration.
+   */
+  if (manifest_path == NULL)
+    return TRUE;
+
+  staging_dir = gbp_flatpak_get_staging_dir (config);
+
+  launcher = create_subprocess_launcher ();
+
+  ide_subprocess_launcher_push_argv (launcher, "flatpak-builder");
+  ide_subprocess_launcher_push_argv (launcher, "--ccache");
+  ide_subprocess_launcher_push_argv (launcher, "--force-clean");
+  stop_at_option = g_strdup_printf ("--stop-at=%s", primary_module);
+  ide_subprocess_launcher_push_argv (launcher, stop_at_option);
+  ide_subprocess_launcher_push_argv (launcher, staging_dir);
+  ide_subprocess_launcher_push_argv (launcher, manifest_path);
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE,
+                        "context", context,
+                        "launcher", launcher,
+                        NULL);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_DEPENDENCIES, 0, stage);
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static gboolean
+register_build_finish_stage (GbpFlatpakPipelineAddin  *self,
+                             IdeBuildPipeline         *pipeline,
+                             IdeContext               *context,
+                             GError                  **error)
+{
+  const gchar * const *finish_args;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autofree gchar *staging_dir = NULL;
+  g_autofree gchar *export_path = NULL;
+  IdeConfiguration *config;
+  const gchar *manifest_path;
+  const gchar *command;
+  guint stage_id;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  manifest_path = ide_configuration_get_internal_string (config, "flatpak-manifest");
+  command = ide_configuration_get_internal_string (config, "flatpak-command");
+  finish_args = ide_configuration_get_internal_strv (config, "flatpak-finish-args");
+
+  /* If there is no manifest, then there are no dependencies
+   * to build for this configuration.
+   */
+  if (manifest_path == NULL)
+    return TRUE;
+
+  staging_dir = gbp_flatpak_get_staging_dir (config);
+
+  launcher = create_subprocess_launcher ();
+
+  ide_subprocess_launcher_push_argv (launcher, "flatpak");
+  ide_subprocess_launcher_push_argv (launcher, "build-finish");
+
+  /*
+   * The --command argument allows the manifest to specify which binary in the
+   * path (/app/bin) should be used as the application binary. By default, the
+   * first binary found in /app/bin is used. However, for applications that
+   * contain supplimental binaries, they may need to specify which is primary.
+   */
+  if (!ide_str_empty0 (command))
+    {
+      g_autofree gchar *command_option = NULL;
+
+      command_option = g_strdup_printf ("--command=%s", command);
+      ide_subprocess_launcher_push_argv (launcher, command_option);
+    }
+
+  /*
+   * The finish args include things like --share=network. These specify which
+   * sandboxing features are necessary, what host files may need to be mapped
+   * in, which D-Bus services to allow, and more.
+   */
+  ide_subprocess_launcher_push_args (launcher, finish_args);
+
+  /*
+   * The staging directory is the location we did build-init with (or which
+   * the flatpak-builder was using for building).
+   */
+  ide_subprocess_launcher_push_argv (launcher, staging_dir);
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE,
+                        "context", context,
+                        "launcher", launcher,
+                        NULL);
+
+  /*
+   * If the export directory is found, we already performed the build-finish
+   * and we do not need to run this operation again. So check if the file
+   * exists and update IdeBuildStage:completed.
+   */
+  export_path = g_build_filename (staging_dir, "export", NULL);
+  g_signal_connect_data (stage,
+                         "query",
+                         G_CALLBACK (check_if_file_exists),
+                         g_steal_pointer (&export_path),
+                         (GClosureNotify)g_free,
+                         G_CONNECT_SWAPPED);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_EXPORT, 0, stage);
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static void
+gbp_flatpak_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                                 IdeBuildPipeline      *pipeline)
+{
+  GbpFlatpakPipelineAddin *self = (GbpFlatpakPipelineAddin *)addin;
+  g_autoptr(GError) error = NULL;
+  IdeConfiguration *config;
+  IdeContext *context;
+  IdeRuntime *runtime;
+
+  g_assert (GBP_IS_FLATPAK_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  /* TODO: Once we have GbpFlatpakConfiguration, we can check for
+   *       that (and it should only allow for valid flatpak runtimes).
+   */
+
+  runtime = ide_configuration_get_runtime (config);
+
+  if (!GBP_IS_FLATPAK_RUNTIME (runtime))
+    return;
+
+  /*
+   * TODO: We should add the ability to mark a pipeline as broken, if we
+   *       detect something that is alarming. That will prevent builds from
+   *       occuring altogether and allow us to present issues within the UI.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!register_mkdirs_stage (self, pipeline, context, &error) ||
+      !register_remotes_stage (self, pipeline, context, &error) ||
+      !register_build_init_stage (self, pipeline, context, &error) ||
+      !register_download_stage (self, pipeline, context, &error) ||
+      !register_dependencies_stage (self, pipeline, context, &error) ||
+      !register_build_finish_stage (self, pipeline, context, &error))
+    g_warning ("%s", error->message);
+}
+
+/* GObject boilerplate */
+
+static void
+build_pipeline_addin_iface_init (IdeBuildPipelineAddinInterface *iface)
+{
+  iface->load = gbp_flatpak_pipeline_addin_load;
+}
+
+struct _GbpFlatpakPipelineAddin { IdeObject parent_instance; };
+
+G_DEFINE_TYPE_WITH_CODE (GbpFlatpakPipelineAddin, gbp_flatpak_pipeline_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                                build_pipeline_addin_iface_init))
+
+static void
+gbp_flatpak_pipeline_addin_class_init (GbpFlatpakPipelineAddinClass *klass)
+{
+}
+
+static void
+gbp_flatpak_pipeline_addin_init (GbpFlatpakPipelineAddin *self)
+{
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/flatpak/gbp-flatpak-pipeline-addin.h
similarity index 64%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to plugins/flatpak/gbp-flatpak-pipeline-addin.h
index 2a77a0b..c6abf38 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/flatpak/gbp-flatpak-pipeline-addin.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* gbp-flatpak-pipeline-addin.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef GBP_FLATPAK_PIPELINE_ADDIN_H
+#define GBP_FLATPAK_PIPELINE_ADDIN_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define GBP_TYPE_FLATPAK_PIPELINE_ADDIN (gbp_flatpak_pipeline_addin_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (GbpFlatpakPipelineAddin, gbp_flatpak_pipeline_addin, GBP, FLATPAK_PIPELINE_ADDIN, 
IdeObject)
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* GBP_FLATPAK_PIPELINE_ADDIN_H */
diff --git a/plugins/flatpak/gbp-flatpak-runtime.c b/plugins/flatpak/gbp-flatpak-runtime.c
index c65e1d6..ac3d5df 100644
--- a/plugins/flatpak/gbp-flatpak-runtime.c
+++ b/plugins/flatpak/gbp-flatpak-runtime.c
@@ -958,6 +958,7 @@ gbp_flatpak_runtime_prepare_configuration (IdeRuntime       *runtime,
                                            IdeConfiguration *configuration)
 {
   GbpFlatpakRuntime* self = (GbpFlatpakRuntime *)runtime;
+  g_autofree gchar *manifest_path = NULL;
 
   g_assert (GBP_IS_FLATPAK_RUNTIME (self));
   g_assert (IDE_IS_CONFIGURATION (configuration));
@@ -968,8 +969,77 @@ gbp_flatpak_runtime_prepare_configuration (IdeRuntime       *runtime,
         ide_configuration_set_app_id (configuration, self->app_id);
     }
 
+  if (self->manifest != NULL)
+    manifest_path = g_file_get_path (self->manifest);
+
   ide_configuration_set_prefix (configuration, "/app");
+
+  /*
+   * TODO: Move this to a GbpFlatpakConfiguration
+   *
+   * Parse some stuff to use later when building.
+   * This really belongs in an IdeConfiguration subclass.
+   */
+
   ide_configuration_set_internal_string (configuration, "flatpak-repo-name", FLATPAK_REPO_NAME);
+  ide_configuration_set_internal_string (configuration, "flatpak-sdk", self->sdk);
+  ide_configuration_set_internal_string (configuration, "flatpak-runtime", self->platform);
+  ide_configuration_set_internal_string (configuration, "flatpak-branch", self->branch);
+  ide_configuration_set_internal_string (configuration, "flatpak-module", self->primary_module);
+  ide_configuration_set_internal_string (configuration, "flatpak-manifest", manifest_path);
+
+  {
+    g_autoptr(JsonParser) parser = NULL;
+    g_autoptr(GError) error = NULL;
+
+    parser = json_parser_new ();
+
+    if (json_parser_load_from_file (parser, manifest_path, &error))
+      {
+        JsonNode *root;
+        JsonNode *member;
+        JsonObject *root_object;
+        JsonArray *ar;
+
+        if (NULL != (root = json_parser_get_root (parser)) &&
+            JSON_NODE_HOLDS_OBJECT (root) &&
+            NULL != (root_object = json_node_get_object (root)))
+          {
+            if (json_object_has_member (root_object, "command"))
+              ide_configuration_set_internal_string (configuration,
+                                                     "flatpak-command",
+                                                     json_object_get_string_member (root_object, "command"));
+
+            if (json_object_has_member (root_object, "finish-args") &&
+                NULL != (member = json_object_get_member (root_object, "finish-args")) &&
+                JSON_NODE_HOLDS_ARRAY (member) &&
+                NULL != (ar = json_node_get_array (member)))
+              {
+                g_autoptr(GPtrArray) finish_args = NULL;
+                guint length = json_array_get_length (ar);
+
+                finish_args = g_ptr_array_sized_new (length + 1);
+
+                for (guint i = 0; i < length; i++)
+                  {
+                    JsonNode *ele = json_array_get_element (ar, i);
+                    const gchar *str = json_node_get_string (ele);
+
+                    if (str != NULL)
+                      g_ptr_array_add (finish_args, (gchar *)str);
+                  }
+
+                g_ptr_array_add (finish_args, NULL);
+
+                ide_configuration_set_internal_strv (configuration,
+                                                     "flatpak-finish-args",
+                                                     (const gchar * const *)finish_args->pdata);
+              }
+          }
+      }
+    else
+      g_warning ("Failure to parse Flatpak Manifest: %s", error->message);
+  }
 }
 
 static void
diff --git a/plugins/flatpak/gbp-flatpak-transfer.c b/plugins/flatpak/gbp-flatpak-transfer.c
new file mode 100644
index 0000000..39687b2
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-transfer.c
@@ -0,0 +1,477 @@
+/* gbp-flatpak-transfer.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-flatpak-transfer"
+
+#include <flatpak.h>
+#include <glib/gi18n.h>
+
+#include "gbp-flatpak-transfer.h"
+
+struct _GbpFlatpakTransfer
+{
+  IdeObject parent_instance;
+
+  gchar   *id;
+  gchar   *arch;
+  gchar   *branch;
+
+  guint    force_update : 1;
+
+  GMutex   mutex;
+  gchar   *status;
+  gdouble  progress;
+};
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_ARCH,
+  PROP_BRANCH,
+  PROP_FORCE_UPDATE,
+  PROP_TITLE,
+  PROP_ICON_NAME,
+  PROP_PROGRESS,
+  PROP_STATUS,
+  N_PROPS
+};
+
+static void transfer_iface_init (IdeTransferInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpFlatpakTransfer, gbp_flatpak_transfer, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TRANSFER, transfer_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+progress_callback (const gchar *status,
+                   guint        progress,
+                   gboolean     estimating,
+                   gpointer     user_data)
+{
+  GbpFlatpakTransfer *self = user_data;
+
+  g_assert (GBP_IS_FLATPAK_TRANSFER (self));
+
+  g_mutex_lock (&self->mutex);
+  g_free (self->status);
+  self->status = g_strdup (status);
+  self->progress = progress / 100.0;
+  g_mutex_unlock (&self->mutex);
+
+  ide_object_notify_in_main (self, properties[PROP_PROGRESS]);
+  ide_object_notify_in_main (self, properties[PROP_STATUS]);
+}
+
+static gboolean
+update_installation (GbpFlatpakTransfer   *self,
+                     FlatpakInstallation  *installation,
+                     GCancellable         *cancellable,
+                     GError              **error)
+{
+  g_autoptr(FlatpakInstalledRef) ref = NULL;
+
+  g_assert (GBP_IS_FLATPAK_TRANSFER (self));
+  g_assert (FLATPAK_IS_INSTALLATION (installation));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ref = flatpak_installation_update (installation,
+                                     FLATPAK_UPDATE_FLAGS_NONE,
+                                     FLATPAK_REF_KIND_RUNTIME,
+                                     self->id,
+                                     self->arch,
+                                     self->branch,
+                                     progress_callback,
+                                     self,
+                                     cancellable,
+                                     error);
+
+  return ref != NULL;
+}
+
+static gboolean
+install_from_remote (GbpFlatpakTransfer   *self,
+                     FlatpakInstallation  *installation,
+                     FlatpakRemote        *remote,
+                     GCancellable         *cancellable,
+                     GError              **error)
+{
+  g_autoptr(FlatpakInstalledRef) ref = NULL;
+
+  g_assert (GBP_IS_FLATPAK_TRANSFER (self));
+  g_assert (FLATPAK_IS_INSTALLATION (installation));
+  g_assert (FLATPAK_IS_REMOTE (remote));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ref = flatpak_installation_install (installation,
+                                      flatpak_remote_get_name (remote),
+                                      FLATPAK_REF_KIND_RUNTIME,
+                                      self->id,
+                                      self->arch,
+                                      self->branch,
+                                      progress_callback,
+                                      self,
+                                      cancellable,
+                                      error);
+
+  return ref != NULL;
+}
+
+static void
+gbp_flatpak_transfer_execute_worker (GTask        *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  GbpFlatpakTransfer *self = source_object;
+  FlatpakInstallation *installations[2] = { NULL };
+  g_autoptr(FlatpakInstallation) user = NULL;
+  g_autoptr(FlatpakInstallation) system = NULL;
+
+  g_assert (GBP_IS_FLATPAK_TRANSFER (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * Load the installations.
+   */
+
+  installations[0] = user = flatpak_installation_new_user (cancellable, NULL);
+  installations[1] = system = flatpak_installation_new_system (cancellable, NULL);
+
+  /*
+   * Locate the id within a previous installation;
+   */
+
+  for (guint i = 0; i < G_N_ELEMENTS (installations); i++)
+    {
+      FlatpakInstallation *installation = installations[i];
+      g_autoptr(GError) error = NULL;
+      g_autoptr(GPtrArray) refs = NULL;
+
+      if (installation == NULL)
+        continue;
+
+      refs = flatpak_installation_list_installed_refs (installation, cancellable, &error);
+
+      if (error != NULL)
+        {
+          g_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+
+      for (guint j = 0; j < refs->len; j++)
+        {
+          FlatpakInstalledRef *ref = g_ptr_array_index (refs, i);
+          const gchar *id;
+          const gchar *arch;
+          const gchar *branch;
+
+          g_assert (FLATPAK_IS_INSTALLED_REF (ref));
+
+          id = flatpak_ref_get_name (FLATPAK_REF (ref));
+          arch = flatpak_ref_get_arch (FLATPAK_REF (ref));
+          branch = flatpak_ref_get_branch (FLATPAK_REF (ref));
+
+          if (g_strcmp0 (self->id, id) == 0 &&
+              g_strcmp0 (self->branch, branch) == 0 &&
+              g_strcmp0 (self->arch, arch) == 0)
+            {
+              if (!self->force_update)
+                {
+                  g_task_return_boolean (task, TRUE);
+                  return;
+                }
+
+              if (!update_installation (self, installation, cancellable, &error))
+                g_task_return_error (task, g_steal_pointer (&error));
+              else
+                g_task_return_boolean (task, TRUE);
+
+              return;
+            }
+        }
+    }
+
+  /*
+   * We didn't locate the id under a previous installation, so we need to
+   * locate a remote that has the matching ref and install it from that.
+   */
+
+  for (guint i = 0; i < G_N_ELEMENTS (installations); i++)
+    {
+      FlatpakInstallation *installation = installations[i];
+      g_autoptr(GPtrArray) remotes = NULL;
+      g_autoptr(GError) error = NULL;
+
+      if (installation == NULL)
+        continue;
+
+      remotes = flatpak_installation_list_remotes (installation, cancellable, &error);
+
+      if (error != NULL)
+        {
+          g_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+
+      for (guint j = 0; j < remotes->len; j++)
+        {
+          FlatpakRemote *remote = g_ptr_array_index (remotes, j);
+          g_autoptr(GPtrArray) refs = NULL;
+
+          g_assert (FLATPAK_IS_REMOTE (remote));
+
+          refs = flatpak_installation_list_remote_refs_sync (installation,
+                                                             flatpak_remote_get_name (remote),
+                                                             cancellable,
+                                                             &error);
+
+          if (error != NULL)
+            {
+              g_task_return_error (task, g_steal_pointer (&error));
+              return;
+            }
+
+          for (guint k = 0; k < refs->len; k++)
+            {
+              FlatpakRemoteRef *ref = g_ptr_array_index (refs, k);
+              const gchar *id;
+              const gchar *arch;
+              const gchar *branch;
+
+              g_assert (FLATPAK_IS_REMOTE_REF (ref));
+
+              id = flatpak_ref_get_name (FLATPAK_REF (ref));
+              arch = flatpak_ref_get_arch (FLATPAK_REF (ref));
+              branch = flatpak_ref_get_branch (FLATPAK_REF (ref));
+
+              if (g_strcmp0 (self->id, id) == 0 &&
+                  g_strcmp0 (self->branch, branch) == 0 &&
+                  g_strcmp0 (self->arch, arch) == 0)
+                {
+                  if (install_from_remote (self, installation, remote, cancellable, &error))
+                    g_task_return_boolean (task, TRUE);
+                  else
+                    g_task_return_error (task, g_steal_pointer (&error));
+                  return;
+                }
+            }
+        }
+    }
+
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_FOUND,
+                           /* Translators: %s is the id of the runtime such as org.gnome.Sdk */
+                           _("Failed to locate %s"),
+                           self->id);
+}
+
+static void
+gbp_flatpak_transfer_execute_async (IdeTransfer         *transfer,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  GbpFlatpakTransfer *self = (GbpFlatpakTransfer *)transfer;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (GBP_IS_FLATPAK_TRANSFER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, gbp_flatpak_transfer_execute_async);
+  g_task_run_in_thread (task, gbp_flatpak_transfer_execute_worker);
+}
+
+static gboolean
+gbp_flatpak_transfer_execute_finish (IdeTransfer   *transfer,
+                                     GAsyncResult  *result,
+                                     GError       **error)
+{
+  g_assert (GBP_IS_FLATPAK_TRANSFER (transfer));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+transfer_iface_init (IdeTransferInterface *iface)
+{
+  iface->execute_async = gbp_flatpak_transfer_execute_async;
+  iface->execute_finish = gbp_flatpak_transfer_execute_finish;
+}
+
+static void
+gbp_flatpak_transfer_finalize (GObject *object)
+{
+  GbpFlatpakTransfer *self = (GbpFlatpakTransfer *)object;
+
+  g_clear_pointer (&self->id, g_free);
+  g_clear_pointer (&self->arch, g_free);
+  g_clear_pointer (&self->branch, g_free);
+  g_mutex_clear (&self->mutex);
+
+  G_OBJECT_CLASS (gbp_flatpak_transfer_parent_class)->finalize (object);
+}
+
+static void
+gbp_flatpak_transfer_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GbpFlatpakTransfer *self = GBP_FLATPAK_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_STATUS:
+      g_mutex_lock (&self->mutex);
+      g_value_set_string (value, self->status);
+      g_mutex_unlock (&self->mutex);
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, g_strdup_printf (_("Installing %s"), self->id));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, "folder-download-symbolic");
+      break;
+
+    case PROP_PROGRESS:
+      g_mutex_lock (&self->mutex);
+      g_value_set_double (value, self->progress);
+      g_mutex_unlock (&self->mutex);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_flatpak_transfer_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GbpFlatpakTransfer *self = GBP_FLATPAK_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      self->id = g_value_dup_string (value);
+      break;
+
+    case PROP_ARCH:
+      self->arch = g_value_dup_string (value);
+      if (self->arch == NULL)
+        self->arch = g_strdup (flatpak_get_default_arch ());
+      break;
+
+    case PROP_BRANCH:
+      self->branch = g_value_dup_string (value);
+      if (self->branch == NULL)
+        self->branch = g_strdup ("stable");
+      break;
+
+    case PROP_FORCE_UPDATE:
+      self->force_update = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_flatpak_transfer_class_init (GbpFlatpakTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_flatpak_transfer_finalize;
+  object_class->get_property = gbp_flatpak_transfer_get_property;
+  object_class->set_property = gbp_flatpak_transfer_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id", NULL, NULL, NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_ARCH] =
+    g_param_spec_string ("arch", NULL, NULL, NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_BRANCH] =
+    g_param_spec_string ("branch", NULL, NULL, NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  
+  properties [PROP_FORCE_UPDATE] =
+    g_param_spec_boolean ("force-update", NULL, NULL, FALSE,
+                          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_STATUS] =
+    g_param_spec_string ("status", NULL, NULL, NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title", NULL, NULL, NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name", NULL, NULL, NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress", NULL, NULL, 0.0, 100.0, 0.0,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_flatpak_transfer_init (GbpFlatpakTransfer *self)
+{
+  g_mutex_init (&self->mutex);
+}
+
+GbpFlatpakTransfer *
+gbp_flatpak_transfer_new (IdeContext  *context,
+                          const gchar *id,
+                          const gchar *arch,
+                          const gchar *branch,
+                          gboolean     force_update)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  if (arch == NULL)
+    arch = flatpak_get_default_arch ();
+
+  if (branch == NULL)
+    branch = "stable";
+
+  return g_object_new (GBP_TYPE_FLATPAK_TRANSFER,
+                       "context", context,
+                       "id", id,
+                       "arch", arch,
+                       "branch", branch,
+                       "force-update", force_update,
+                       NULL);
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/flatpak/gbp-flatpak-transfer.h
similarity index 50%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to plugins/flatpak/gbp-flatpak-transfer.h
index 2a77a0b..ba8390e 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/flatpak/gbp-flatpak-transfer.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* gbp-flatpak-transfer.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,23 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef GBP_FLATPAK_TRANSFER_H
+#define GBP_FLATPAK_TRANSFER_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define GBP_TYPE_FLATPAK_TRANSFER (gbp_flatpak_transfer_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (GbpFlatpakTransfer, gbp_flatpak_transfer, GBP, FLATPAK_TRANSFER, IdeObject)
+
+GbpFlatpakTransfer *gbp_flatpak_transfer_new (IdeContext  *context,
+                                              const gchar *id,
+                                              const gchar *arch,
+                                              const gchar *branch,
+                                              gboolean     force_update);
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* GBP_FLATPAK_TRANSFER_H */
diff --git a/plugins/flatpak/gbp-flatpak-util.c b/plugins/flatpak/gbp-flatpak-util.c
new file mode 100644
index 0000000..c67a1bd
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-util.c
@@ -0,0 +1,69 @@
+/* gbp-flatpak-util.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-flatpak-util"
+
+#include "gbp-flatpak-util.h"
+
+gchar *
+gbp_flatpak_get_repo_dir (IdeConfiguration *configuration)
+{
+  const gchar *project_id;
+  const gchar *runtime_id;
+  IdeContext *context;
+  IdeProject *project;
+
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  runtime_id = ide_configuration_get_runtime_id (configuration);
+  context = ide_object_get_context (IDE_OBJECT (configuration));
+  project = ide_context_get_project (context);
+  project_id = ide_project_get_id (project);
+
+  return g_build_filename (g_get_user_cache_dir (),
+                           "gnome-builder",
+                           "flatpak",
+                           "repos",
+                           project_id,
+                           runtime_id,
+                           NULL);
+}
+
+gchar *
+gbp_flatpak_get_staging_dir (IdeConfiguration *configuration)
+{
+  const gchar *project_id;
+  const gchar *runtime_id;
+  IdeContext *context;
+  IdeProject *project;
+
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  runtime_id = ide_configuration_get_runtime_id (configuration);
+  context = ide_object_get_context (IDE_OBJECT (configuration));
+  project = ide_context_get_project (context);
+  project_id = ide_project_get_id (project);
+
+  return g_build_filename (g_get_user_cache_dir (),
+                           "gnome-builder",
+                           "flatpak",
+                           "staging",
+                           project_id,
+                           runtime_id,
+                           NULL);
+}
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/flatpak/gbp-flatpak-util.h
similarity index 63%
copy from plugins/gcc/gbp-gcc-build-result-addin.h
copy to plugins/flatpak/gbp-flatpak-util.h
index 2a77a0b..f3afc22 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/flatpak/gbp-flatpak-util.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* gbp-flatpak-util.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,16 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef GBP_FLATPAK_UTIL_H
+#define GBP_FLATPAK_UTIL_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
-
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+gchar *gbp_flatpak_get_repo_dir    (IdeConfiguration *configuration);
+gchar *gbp_flatpak_get_staging_dir (IdeConfiguration *configuration);
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* GBP_FLATPAK_UTIL_H */
diff --git a/plugins/gcc/Makefile.am b/plugins/gcc/Makefile.am
index b772044..1e269c4 100644
--- a/plugins/gcc/Makefile.am
+++ b/plugins/gcc/Makefile.am
@@ -7,8 +7,8 @@ plugin_LTLIBRARIES = libgcc-plugin.la
 dist_plugin_DATA = gcc.plugin
 
 libgcc_plugin_la_SOURCES = \
-       gbp-gcc-build-result-addin.c \
-       gbp-gcc-build-result-addin.h \
+       gbp-gcc-pipeline-addin.c \
+       gbp-gcc-pipeline-addin.h \
        gbp-gcc-plugin.c
 
 libgcc_plugin_la_CFLAGS = $(PLUGIN_CFLAGS)
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.c b/plugins/gcc/gbp-gcc-pipeline-addin.c
similarity index 68%
rename from plugins/gcc/gbp-gcc-build-result-addin.c
rename to plugins/gcc/gbp-gcc-pipeline-addin.c
index ae295ff..b3fd604 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.c
+++ b/plugins/gcc/gbp-gcc-pipeline-addin.c
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.c
+/* gbp-gcc-pipeline-addin.c
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,12 +16,11 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <string.h>
-
-#include "egg-signal-group.h"
+#define G_LOG_DOMAIN "gbp-gcc-pipeline-addin"
 
-#include "gbp-gcc-build-result-addin.h"
+#include <string.h>
 
+#include "gbp-gcc-pipeline-addin.h"
 
 #define ERROR_FORMAT_REGEX              \
   "(?<filename>[a-zA-Z0-9\\-\\.\\/]+):" \
@@ -30,21 +29,16 @@
   "(?<level>[\\w\\s]+): "               \
   "(?<message>.*)"
 
-struct _GbpGccBuildResultAddin
+struct _GbpGccPipelineAddin
 {
-  IdeObject       parent_instance;
+  GObject           parent_instance;
 
-  EggSignalGroup *signals;
-  gchar          *current_dir;
-  gchar          *top_dir;
+  IdeBuildPipeline *pipeline;
+  gchar            *current_dir;
+  gchar            *top_dir;
+  guint             observer_id;
 };
 
-static void build_result_addin_iface_init (IdeBuildResultAddinInterface *iface);
-
-G_DEFINE_TYPE_EXTENDED (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, IDE_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_RESULT_ADDIN,
-                                               build_result_addin_iface_init))
-
 static GRegex *errfmt;
 
 static IdeDiagnosticSeverity
@@ -79,8 +73,8 @@ parse_severity (const gchar *str)
 }
 
 static IdeDiagnostic *
-create_diagnostic (GbpGccBuildResultAddin *self,
-                   GMatchInfo             *match_info)
+create_diagnostic (GbpGccPipelineAddin *self,
+                   GMatchInfo          *match_info)
 {
   g_autofree gchar *filename = NULL;
   g_autofree gchar *line = NULL;
@@ -97,7 +91,7 @@ create_diagnostic (GbpGccBuildResultAddin *self,
     IdeDiagnosticSeverity severity;
   } parsed;
 
-  g_assert (GBP_IS_GCC_BUILD_RESULT_ADDIN (self));
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
   g_assert (match_info != NULL);
 
   message = g_match_info_fetch_named (match_info, "message");
@@ -123,8 +117,6 @@ create_diagnostic (GbpGccBuildResultAddin *self,
 
   parsed.severity = parse_severity (level);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-
   if (!g_path_is_absolute (filename) && self->current_dir != NULL)
     {
       const gchar *basedir = self->current_dir;
@@ -142,6 +134,8 @@ create_diagnostic (GbpGccBuildResultAddin *self,
       filename = path;
     }
 
+  context = ide_object_get_context (IDE_OBJECT (self->pipeline));
+
   if (!g_path_is_absolute (filename))
     {
       g_autoptr(GFile) child = NULL;
@@ -167,16 +161,19 @@ create_diagnostic (GbpGccBuildResultAddin *self,
 }
 
 static void
-gbp_gcc_build_result_addin_log (GbpGccBuildResultAddin *self,
-                                IdeBuildResultLog       log,
-                                const gchar            *message,
-                                IdeBuildResult         *result)
+log_observer (IdeBuildLogStream  stream,
+              const gchar       *message,
+              gssize             message_len,
+              gpointer           user_data)
 {
+  GbpGccPipelineAddin *self = user_data;
   GMatchInfo *match_info = NULL;
   const gchar *enterdir;
 
-  g_assert (GBP_IS_GCC_BUILD_RESULT_ADDIN (self));
-  g_assert (IDE_IS_BUILD_RESULT (result));
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
+  g_assert (message != NULL);
+  g_assert (message_len >= 0);
+  g_assert (message[message_len] == '\0');
 
 #define ENTERING_DIRECTORY_BEGIN "Entering directory '"
 #define ENTERING_DIRECTORY_END   "'\n"
@@ -205,13 +202,10 @@ gbp_gcc_build_result_addin_log (GbpGccBuildResultAddin *self,
 
   if (g_regex_match (errfmt, message, 0, &match_info))
     {
-      IdeDiagnostic *diagnostic;
+      g_autoptr(IdeDiagnostic) diagnostic = NULL;
 
       if (NULL != (diagnostic = create_diagnostic (self, match_info)))
-        {
-          ide_build_result_emit_diagnostic (result, diagnostic);
-          ide_diagnostic_unref (diagnostic);
-        }
+        ide_build_pipeline_emit_diagnostic (self->pipeline, diagnostic);
     }
 
   g_match_info_free (match_info);
@@ -221,47 +215,52 @@ gbp_gcc_build_result_addin_log (GbpGccBuildResultAddin *self,
 }
 
 static void
-gbp_gcc_build_result_addin_class_init (GbpGccBuildResultAddinClass *klass)
+gbp_gcc_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                             IdeBuildPipeline      *pipeline)
 {
-  errfmt = g_regex_new (ERROR_FORMAT_REGEX, G_REGEX_OPTIMIZE | G_REGEX_CASELESS, 0, NULL);
-  g_assert (errfmt != NULL);
+  GbpGccPipelineAddin *self = (GbpGccPipelineAddin *)addin;
+
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
+
+  self->pipeline = pipeline;
+  self->observer_id = ide_build_pipeline_add_log_observer (pipeline, log_observer, self, NULL);
 }
 
 static void
-gbp_gcc_build_result_addin_init (GbpGccBuildResultAddin *self)
+gbp_gcc_pipeline_addin_unload (IdeBuildPipelineAddin *addin,
+                               IdeBuildPipeline      *pipeline)
 {
-  self->signals = egg_signal_group_new (IDE_TYPE_BUILD_RESULT);
+  GbpGccPipelineAddin *self = (GbpGccPipelineAddin *)addin;
 
-  egg_signal_group_connect_object (self->signals,
-                                   "log",
-                                   G_CALLBACK (gbp_gcc_build_result_addin_log),
-                                   self,
-                                   G_CONNECT_SWAPPED);
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
+
+  ide_build_pipeline_remove_log_observer (pipeline, self->observer_id);
+  self->observer_id = 0;
+
+  self->pipeline = NULL;
+
+  g_clear_pointer (&self->current_dir, g_free);
+  g_clear_pointer (&self->top_dir, g_free);
 }
 
 static void
-gbp_gcc_build_result_addin_load (IdeBuildResultAddin *addin,
-                                 IdeBuildResult      *result)
+addin_iface_init (IdeBuildPipelineAddinInterface *iface)
 {
-  GbpGccBuildResultAddin *self = (GbpGccBuildResultAddin *)addin;
-
-  egg_signal_group_set_target (self->signals, result);
+  iface->load = gbp_gcc_pipeline_addin_load;
+  iface->unload = gbp_gcc_pipeline_addin_unload;
 }
 
+G_DEFINE_TYPE_WITH_CODE (GbpGccPipelineAddin, gbp_gcc_pipeline_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN, addin_iface_init))
+
 static void
-gbp_gcc_build_result_addin_unload (IdeBuildResultAddin *addin,
-                                   IdeBuildResult      *result)
+gbp_gcc_pipeline_addin_class_init (GbpGccPipelineAddinClass *klass)
 {
-  GbpGccBuildResultAddin *self = (GbpGccBuildResultAddin *)addin;
-
-  egg_signal_group_set_target (self->signals, NULL);
-  g_clear_pointer (&self->current_dir, g_free);
-  g_clear_pointer (&self->top_dir, g_free);
+  errfmt = g_regex_new (ERROR_FORMAT_REGEX, G_REGEX_OPTIMIZE | G_REGEX_CASELESS, 0, NULL);
+  g_assert (errfmt != NULL);
 }
 
 static void
-build_result_addin_iface_init (IdeBuildResultAddinInterface *iface)
+gbp_gcc_pipeline_addin_init (GbpGccPipelineAddin *self)
 {
-  iface->load = gbp_gcc_build_result_addin_load;
-  iface->unload = gbp_gcc_build_result_addin_unload;
 }
diff --git a/plugins/gcc/gbp-gcc-build-result-addin.h b/plugins/gcc/gbp-gcc-pipeline-addin.h
similarity index 63%
rename from plugins/gcc/gbp-gcc-build-result-addin.h
rename to plugins/gcc/gbp-gcc-pipeline-addin.h
index 2a77a0b..ecd443d 100644
--- a/plugins/gcc/gbp-gcc-build-result-addin.h
+++ b/plugins/gcc/gbp-gcc-pipeline-addin.h
@@ -1,6 +1,6 @@
-/* gbp-gcc-build-result-addin.h
+/* gbp-gcc-pipeline-addin.h
  *
- * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef GBP_GCC_BUILD_RESULT_ADDIN_H
-#define GBP_GCC_BUILD_RESULT_ADDIN_H
+#ifndef GBP_GCC_PIPELINE_ADDIN_H
+#define GBP_GCC_PIPELINE_ADDIN_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define GBP_TYPE_GCC_BUILD_RESULT_ADDIN (gbp_gcc_build_result_addin_get_type())
+#define GBP_TYPE_GCC_PIPELINE_ADDIN (gbp_gcc_pipeline_addin_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpGccBuildResultAddin, gbp_gcc_build_result_addin, GBP, GCC_BUILD_RESULT_ADDIN, 
IdeObject)
+G_DECLARE_FINAL_TYPE (GbpGccPipelineAddin, gbp_gcc_pipeline_addin, GBP, GCC_PIPELINE_ADDIN, GObject)
 
 G_END_DECLS
 
-#endif /* GBP_GCC_BUILD_RESULT_ADDIN_H */
+#endif /* GBP_GCC_PIPELINE_ADDIN_H */
diff --git a/plugins/gcc/gbp-gcc-plugin.c b/plugins/gcc/gbp-gcc-plugin.c
index 5504323..d3ea3b2 100644
--- a/plugins/gcc/gbp-gcc-plugin.c
+++ b/plugins/gcc/gbp-gcc-plugin.c
@@ -19,12 +19,10 @@
 #include <ide.h>
 #include <libpeas/peas.h>
 
-#include "gbp-gcc-build-result-addin.h"
+#include "gbp-gcc-pipeline-addin.h"
 
 void
 peas_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_BUILD_RESULT_ADDIN,
-                                              GBP_TYPE_GCC_BUILD_RESULT_ADDIN);
+  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN, 
GBP_TYPE_GCC_PIPELINE_ADDIN);
 }
diff --git a/plugins/todo/todo_plugin/__init__.py b/plugins/todo/todo_plugin/__init__.py
index 0bc0096..2afd70c 100644
--- a/plugins/todo/todo_plugin/__init__.py
+++ b/plugins/todo/todo_plugin/__init__.py
@@ -89,6 +89,9 @@ class TodoWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
         # can be navigated to quickly.
         self.mine(file, prepend=True)
 
+    def _is_ignored_pattern(self, name):
+        return name.endswith('.m4') or name.endswith('.in')
+
     def _post_from_main(self, args):
         items, prepend = args
 
@@ -97,7 +100,7 @@ class TodoWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
 
         for item in items:
             file = item.props.file
-            if vcs.is_ignored(file) or file.get_basename().endswith('.m4'):
+            if vcs.is_ignored(file) or self._is_ignored_pattern(file.get_basename()):
                 continue
             self.panel.add_item(item, prepend=prepend)
 
diff --git a/tests/Makefile.am b/tests/Makefile.am
index e62afa8..d26154b 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -126,6 +126,12 @@ test_ide_builder_CFLAGS = $(tests_cflags)
 test_ide_builder_LDADD = $(tests_libs)
 
 
+TESTS += test-ide-build-pipeline
+test_ide_build_pipeline_SOURCES = test-ide-build-pipeline.c
+test_ide_build_pipeline_CFLAGS = $(tests_cflags)
+test_ide_build_pipeline_LDADD = $(tests_libs)
+
+
 TESTS += test-ide-doap
 test_ide_doap_SOURCES = test-ide-doap.c
 test_ide_doap_CFLAGS = $(tests_cflags)
diff --git a/tests/data/project1/.gitignore b/tests/data/project1/.gitignore
index 4852292..cbd86cc 100644
--- a/tests/data/project1/.gitignore
+++ b/tests/data/project1/.gitignore
@@ -15,4 +15,5 @@ build-aux/m4/lt~obsolete.m4
 build-aux/missing
 build/
 config.h.in
+config.h.in~
 configure
diff --git a/tests/test-ide-build-pipeline.c b/tests/test-ide-build-pipeline.c
new file mode 100644
index 0000000..fafa554
--- /dev/null
+++ b/tests/test-ide-build-pipeline.c
@@ -0,0 +1,117 @@
+/* test-ide-build-pipeline.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <ide.h>
+
+#include "application/ide-application-tests.h"
+
+static void
+execute_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+  gint r;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  r = ide_build_pipeline_execute_finish (pipeline, result, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (r, ==, TRUE);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+context_loaded (GObject      *object,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeBuildPipeline) pipeline = NULL;
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+
+  context = ide_context_new_finish (result, &error);
+  g_assert_no_error (error);
+  g_assert (context != NULL);
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config_manager = ide_context_get_configuration_manager (context);
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (config_manager));
+
+  config = ide_configuration_manager_get_current (config_manager);
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  pipeline = g_object_new (IDE_TYPE_BUILD_PIPELINE,
+                           "context", context,
+                           "configuration", config,
+                           NULL);
+
+  ide_build_pipeline_request_phase (pipeline, IDE_BUILD_PHASE_BUILD);
+
+  ide_build_pipeline_execute_async (pipeline,
+                                    NULL,
+                                    execute_cb,
+                                    g_steal_pointer (&task));
+}
+
+static void
+test_build_pipeline (GCancellable        *cancellable,
+                     GAsyncReadyCallback  callback,
+                     gpointer             user_data)
+{
+  g_autoptr(GFile) project_file = NULL;
+  g_autofree gchar *path = NULL;
+  const gchar *srcdir = g_getenv ("G_TEST_SRCDIR");
+  g_autoptr(GTask) task = NULL;
+
+  task = g_task_new (NULL, cancellable, callback, user_data);
+
+  path = g_build_filename (srcdir, "data", "project1", NULL);
+  project_file = g_file_new_for_path (path);
+
+  ide_context_new_async (project_file,
+                         cancellable,
+                         context_loaded,
+                         g_steal_pointer (&task));
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  IdeApplication *app;
+  gint ret;
+
+  g_test_init (&argc, &argv, NULL);
+
+  ide_log_init (TRUE, NULL);
+  ide_log_set_verbosity (4);
+
+  app = ide_application_new ();
+  ide_application_add_test (app, "/Ide/BuildPipeline/basic", test_build_pipeline, NULL);
+  ret = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+
+  return ret;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]