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



commit 398de14bb5f78d50cdaf95d92e5f7a25266a274d
Author: Christian Hergert <chergert redhat com>
Date:   Wed Feb 1 17:01:19 2017 -0800

    wip: start on build pipeline/stages

 configure.ac                                       |    2 -
 libide/Makefile.am                                 |   33 +-
 libide/application/ide-application-private.h       |    2 +
 libide/application/ide-application.c               |   24 +
 libide/application/ide-application.h               |    3 +
 libide/buildsystem/ide-build-command-queue.c       |  299 ---
 libide/buildsystem/ide-build-command-queue.h       |   53 -
 libide/buildsystem/ide-build-command.c             |  403 ----
 libide/buildsystem/ide-build-command.h             |   88 -
 libide/buildsystem/ide-build-log-private.h         |   47 +
 libide/buildsystem/ide-build-log.c                 |  242 ++
 .../{ide-simple-builder.h => ide-build-log.h}      |   34 +-
 libide/buildsystem/ide-build-manager.c             | 1242 ++++++-----
 libide/buildsystem/ide-build-manager.h             |   53 +-
 libide/buildsystem/ide-build-pipeline-addin.c      |  109 +
 libide/buildsystem/ide-build-pipeline-addin.h      |   51 +
 libide/buildsystem/ide-build-pipeline.c            | 2494 ++++++++++++++++++++
 libide/buildsystem/ide-build-pipeline.h            |  128 +
 libide/buildsystem/ide-build-result-addin.c        |   60 -
 libide/buildsystem/ide-build-result-addin.h        |   47 -
 libide/buildsystem/ide-build-result.c              |  868 -------
 libide/buildsystem/ide-build-result.h              |   89 -
 libide/buildsystem/ide-build-stage-launcher.c      |  456 ++++
 libide/buildsystem/ide-build-stage-launcher.h      |   57 +
 libide/buildsystem/ide-build-stage-mkdirs.c        |  186 ++
 ...e-simple-builder.h => ide-build-stage-mkdirs.h} |   29 +-
 libide/buildsystem/ide-build-stage-private.h       |   39 +
 libide/buildsystem/ide-build-stage-transfer.c      |  183 ++
 ...simple-builder.h => ide-build-stage-transfer.h} |   30 +-
 libide/buildsystem/ide-build-stage.c               |  899 +++++++
 libide/buildsystem/ide-build-stage.h               |  204 ++
 libide/buildsystem/ide-build-system.c              |  252 +--
 libide/buildsystem/ide-build-system.h              |   35 +-
 libide/buildsystem/ide-builder.c                   |  465 ----
 libide/buildsystem/ide-builder.h                   |  126 -
 libide/buildsystem/ide-configuration-manager.c     |   72 +-
 libide/buildsystem/ide-configuration.c             |  173 +-
 libide/buildsystem/ide-configuration.h             |   10 +-
 libide/buildsystem/ide-simple-builder.c            |  172 --
 libide/directory/ide-directory-build-system.c      |   23 -
 libide/ide-context.c                               |   22 +
 libide/ide-enums.c.in                              |    3 +-
 libide/ide-types.h                                 |    5 +-
 libide/ide.h                                       |   13 +-
 libide/local/ide-local-device.c                    |   35 +-
 libide/runner/ide-run-manager.c                    |   16 +-
 libide/runtimes/ide-runtime.c                      |  159 --
 libide/runtimes/ide-runtime.h                      |   48 -
 libide/subprocess/ide-subprocess-launcher.c        |   24 +-
 libide/subprocess/ide-subprocess.c                 |   12 +
 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                 |  415 ++++
 libide/util/ide-directory-reaper.h                 |   54 +
 libide/util/ide-glib.c                             |   44 +
 libide/util/ide-glib.h                             |    5 +
 libide/util/ide-posix.c                            |   36 +
 libide/util/ide-posix.h                            |    9 +-
 libide/workbench/ide-omni-bar.c                    |   99 +-
 libide/workbench/ide-omni-bar.ui                   |    2 +-
 org.gnome.Builder.json                             |    2 +
 plugins/Makefile.am                                |    1 -
 plugins/autotools/Makefile.am                      |   10 +-
 plugins/autotools/autotools-plugin.c               |    2 +
 .../autotools/ide-autotools-application-addin.c    |    4 +
 plugins/autotools/ide-autotools-autogen-stage.c    |  179 ++
 ...ols-builder.h => ide-autotools-autogen-stage.h} |   17 +-
 plugins/autotools/ide-autotools-build-system.c     |  359 +++-
 plugins/autotools/ide-autotools-build-task.c       | 1266 ----------
 plugins/autotools/ide-autotools-build-task.h       |   55 -
 plugins/autotools/ide-autotools-builder.c          |  802 -------
 plugins/autotools/ide-autotools-makecache-stage.c  |  253 ++
 plugins/autotools/ide-autotools-makecache-stage.h  |   38 +
 plugins/autotools/ide-autotools-pipeline-addin.c   |  427 ++++
 .../ide-autotools-pipeline-addin.h}                |   14 +-
 plugins/autotools/ide-makecache.c                  |  472 +---
 plugins/autotools/ide-makecache.h                  |   63 +-
 plugins/build-tools/gbp-build-log-panel.c          |   85 +-
 plugins/build-tools/gbp-build-log-panel.h          |    5 +-
 plugins/build-tools/gbp-build-panel.c              |  258 ++-
 plugins/build-tools/gbp-build-panel.h              |    3 -
 plugins/build-tools/gbp-build-tool.c               |   98 +-
 plugins/build-tools/gbp-build-workbench-addin.c    |   36 +-
 plugins/cargo/cargo_plugin.py                      |  201 +--
 plugins/contributing/IDEA                          |   62 -
 plugins/contributing/Makefile.am                   |   15 -
 plugins/contributing/configure.ac                  |   12 -
 plugins/contributing/contributing.plugin           |   11 -
 .../contributing/contributing_plugin/__init__.py   |  230 --
 plugins/contributing/contributing_plugin/helper.py |  141 --
 plugins/flatpak/Makefile.am                        |   24 +-
 plugins/flatpak/configure.ac                       |    2 +-
 plugins/flatpak/gbp-flatpak-pipeline-addin.c       |  529 +++++
 .../gbp-flatpak-pipeline-addin.h}                  |   14 +-
 plugins/flatpak/gbp-flatpak-plugin.c               |    4 +
 plugins/flatpak/gbp-flatpak-runtime.c              |  782 +------
 plugins/flatpak/gbp-flatpak-transfer.c             |  555 +++++
 plugins/flatpak/gbp-flatpak-transfer.h             |   40 +
 plugins/flatpak/gbp-flatpak-util.c                 |   69 +
 .../gbp-flatpak-util.h}                            |   15 +-
 plugins/gcc/Makefile.am                            |    4 +-
 plugins/gcc/gbp-gcc-build-result-addin.c           |  267 ---
 plugins/gcc/gbp-gcc-pipeline-addin.c               |   74 +
 ...ild-result-addin.h => gbp-gcc-pipeline-addin.h} |   14 +-
 plugins/gcc/gbp-gcc-plugin.c                       |    6 +-
 plugins/meson/meson_plugin/__init__.py             |  417 ++---
 plugins/todo/todo_plugin/__init__.py               |    5 +-
 plugins/vala-pack/Makefile.am                      |    1 +
 po/POTFILES.in                                     |    6 -
 tests/Makefile.am                                  |    6 +
 tests/data/project1/.gitignore                     |    1 +
 tests/test-ide-build-pipeline.c                    |  117 +
 114 files changed, 10380 insertions(+), 8651 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index d9292f9..72f76a8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -281,7 +281,6 @@ m4_include([plugins/clang/configure.ac])
 m4_include([plugins/color-picker/configure.ac])
 m4_include([plugins/command-bar/configure.ac])
 m4_include([plugins/comment-code/configure.ac])
-m4_include([plugins/contributing/configure.ac])
 m4_include([plugins/create-project/configure.ac])
 m4_include([plugins/ctags/configure.ac])
 m4_include([plugins/devhelp/configure.ac])
@@ -591,7 +590,6 @@ echo "  Clang ................................ : ${enable_clang_plugin}"
 echo "  Color picker.......................... : ${enable_color_picker_plugin}"
 echo "  Command Bar .......................... : ${enable_command_bar_plugin}"
 echo "  Comment Code.......................... : ${enable_comment_code_plugin}"
-echo "  Contribute ........................... : ${enable_contributing_plugin}"
 echo "  Ctags ................................ : ${enable_ctags_plugin}"
 echo "  Devhelp .............................. : ${enable_devhelp_plugin}"
 echo "  Editorconfig ......................... : ${enable_editorconfig}"
diff --git a/libide/Makefile.am b/libide/Makefile.am
index 1839c33..56d6895 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -30,19 +30,20 @@ libide_1_0_la_public_headers =                            \
        buffers/ide-buffer.h                              \
        buffers/ide-unsaved-file.h                        \
        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-result-addin.h              \
-       buildsystem/ide-build-result.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-system.h                    \
        buildsystem/ide-build-target.h                    \
-       buildsystem/ide-builder.h                         \
        buildsystem/ide-configuration-manager.h           \
        buildsystem/ide-configuration.h                   \
        buildsystem/ide-environment-variable.h            \
        buildsystem/ide-environment.h                     \
-       buildsystem/ide-simple-builder.h                  \
        devices/ide-device-manager.h                      \
        devices/ide-device-provider.h                     \
        devices/ide-device.h                              \
@@ -157,6 +158,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                                   \
@@ -198,19 +200,19 @@ libide_1_0_la_public_sources =                            \
        buffers/ide-buffer.c                              \
        buffers/ide-unsaved-file.c                        \
        buffers/ide-unsaved-files.c                       \
-       buildsystem/ide-build-command.c                   \
-       buildsystem/ide-build-command-queue.c             \
        buildsystem/ide-build-manager.c                   \
-       buildsystem/ide-build-result-addin.c              \
-       buildsystem/ide-build-result.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-system.c                    \
        buildsystem/ide-build-target.c                    \
-       buildsystem/ide-builder.c                         \
        buildsystem/ide-configuration-manager.c           \
        buildsystem/ide-configuration.c                   \
        buildsystem/ide-environment-variable.c            \
        buildsystem/ide-environment.c                     \
-       buildsystem/ide-simple-builder.c                  \
        devices/ide-device-manager.c                      \
        devices/ide-device-provider.c                     \
        devices/ide-device.c                              \
@@ -330,6 +332,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                                   \
@@ -370,6 +373,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                 \
@@ -601,7 +607,8 @@ endif
 
 glib_enum_headers =                        \
        buffers/ide-buffer.h               \
-       buildsystem/ide-build-result.h     \
+       buildsystem/ide-build-log.h        \
+       buildsystem/ide-build-pipeline.h   \
        devices/ide-device.h               \
        diagnostics/ide-diagnostic.h       \
        doap/ide-doap.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/libide/buildsystem/ide-simple-builder.h b/libide/buildsystem/ide-build-log.h
similarity index 55%
copy from libide/buildsystem/ide-simple-builder.h
copy to libide/buildsystem/ide-build-log.h
index 705ef74..a6c807d 100644
--- a/libide/buildsystem/ide-simple-builder.h
+++ b/libide/buildsystem/ide-build-log.h
@@ -1,4 +1,4 @@
-/* ide-simple-builder.h
+/* ide-build-log.h
  *
  * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
@@ -16,32 +16,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef IDE_SIMPLE_BUILDER_H
-#define IDE_SIMPLE_BUILDER_H
+#ifndef IDE_BUILD_LOG_H
+#define IDE_BUILD_LOG_H
 
-#include "buildsystem/ide-builder.h"
-#include "buildsystem/ide-configuration.h"
+#include <glib.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_SIMPLE_BUILDER (ide_simple_builder_get_type())
-
-G_DECLARE_DERIVABLE_TYPE (IdeSimpleBuilder, ide_simple_builder, IDE, SIMPLE_BUILDER, IdeBuilder)
-
-struct _IdeSimpleBuilderClass
+typedef enum
 {
-  IdeBuilderClass parent_class;
+  IDE_BUILD_LOG_STDOUT,
+  IDE_BUILD_LOG_STDERR,
+} IdeBuildLogStream;
 
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
-};
+typedef void (*IdeBuildLogObserver) (IdeBuildLogStream  log_stream,
+                                     const gchar       *message,
+                                     gssize             message_len,
+                                     gpointer           user_data);
 
 G_END_DECLS
 
-#endif /* IDE_SIMPLE_BUILDER_H */
+#endif /* IDE_BUILD_LOG_H */
diff --git a/libide/buildsystem/ide-build-manager.c b/libide/buildsystem/ide-build-manager.c
index 1ab9679..4e6d869 100644
--- a/libide/buildsystem/ide-build-manager.c
+++ b/libide/buildsystem/ide-build-manager.c
@@ -1,6 +1,6 @@
 /* ide-build-manager.c
  *
- * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ * Copyright (C) 2016-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
@@ -18,44 +18,36 @@
 
 #define G_LOG_DOMAIN "ide-build-manager"
 
-#include <egg-signal-group.h>
-#include <glib/gi18n.h>
-
 #include "ide-context.h"
 #include "ide-debug.h"
 
 #include "buffers/ide-buffer-manager.h"
-#include "buildsystem/ide-builder.h"
 #include "buildsystem/ide-build-manager.h"
-#include "buildsystem/ide-build-result.h"
-#include "buildsystem/ide-build-system.h"
-#include "buildsystem/ide-build-target.h"
-#include "buildsystem/ide-configuration.h"
+#include "buildsystem/ide-build-pipeline.h"
 #include "buildsystem/ide-configuration-manager.h"
+#include "buildsystem/ide-configuration.h"
 
 struct _IdeBuildManager
 {
-  IdeObject             parent_instance;
+  IdeObject         parent_instance;
 
-  EggSignalGroup       *signals;
-  IdeBuildResult       *build_result;
-  GCancellable         *cancellable;
-  GDateTime            *last_build_time;
-  GSimpleActionGroup   *actions;
+  IdeBuildPipeline *pipeline;
+  GDateTime        *last_build_time;
+  GCancellable     *cancellable;
+  GActionGroup     *actions;
 
-  guint                 has_diagnostics : 1;
-  guint                 saving : 1;
-};
+  GTimer           *running_time;
 
-typedef struct
-{
-  IdeBuilder *builder;
-  IdeBuilderBuildFlags build_flags;
-} BuildState;
+  guint             diagnostic_count;
 
-static void action_group_iface_init (GActionGroupInterface *iface);
+  guint             timer_source;
+};
+
+static void initable_iface_init     (GInitableIface *);
+static void action_group_iface_init (GActionGroupInterface *);
 
 G_DEFINE_TYPE_EXTENDED (IdeBuildManager, ide_build_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, action_group_iface_init))
 
 enum {
@@ -78,175 +70,287 @@ enum {
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
-static void
-build_state_free (gpointer data)
+static gboolean
+timer_callback (gpointer data)
 {
-  BuildState *state = data;
+  IdeBuildManager *self = data;
 
-  if (state != NULL)
-    {
-      g_clear_object (&state->builder);
-      g_slice_free (BuildState, state);
-    }
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  return G_SOURCE_CONTINUE;
 }
 
 static void
-ide_build_manager__build_result__notify_mode (IdeBuildManager *self,
-                                              GParamSpec      *mode_pspec,
-                                              IdeBuildResult  *build_result)
+ide_build_manager_start_timer (IdeBuildManager *self)
 {
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (self->timer_source == 0);
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+  if (self->running_time != NULL)
+    g_timer_start (self->running_time);
+  else
+    self->running_time = g_timer_new ();
+
+  /*
+   * We use the EggFrameSource for our timer callback because we only want to
+   * update at a rate somewhat close to a typical monitor refresh rate.
+   * Additionally, we want to handle drift (which that source does) so that we
+   * don't constantly fall behind.
+   */
+  self->timer_source = g_timeout_add_seconds (1, timer_callback, self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager__build_result__notify_running (IdeBuildManager *self,
-                                                 GParamSpec      *running_pspec,
-                                                 IdeBuildResult  *build_result)
+ide_build_manager_stop_timer (IdeBuildManager *self)
 {
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUILD_MANAGER (self));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_timer_stop (self->running_time);
+  ide_clear_source (&self->timer_source);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager__build_result__notify_running_time (IdeBuildManager *self,
-                                                      GParamSpec      *running_time_pspec,
-                                                      IdeBuildResult  *build_result)
+ide_build_manager_handle_diagnostic (IdeBuildManager  *self,
+                                     IdeDiagnostic    *diagnostic,
+                                     IdeBuildPipeline *pipeline)
 {
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (diagnostic != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+  self->diagnostic_count++;
+
+  if (self->diagnostic_count == 1)
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_DIAGNOSTICS]);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager__build_result__diagnostic (IdeBuildManager *self,
-                                             IdeDiagnostic   *diagnostic,
-                                             IdeBuildResult  *build_result)
+ide_build_manager_notify_busy (IdeBuildManager  *self,
+                               GParamSpec       *pspec,
+                               IdeBuildPipeline *pipeline)
 {
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
-  g_assert (diagnostic != NULL);
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (G_IS_PARAM_SPEC (pspec));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  if (self->has_diagnostics == FALSE)
-    {
-      self->has_diagnostics = TRUE;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
-    }
+  if (pipeline == self->pipeline)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_build_activate (GSimpleAction *action,
-                                  GVariant      *parameter,
-                                  gpointer       user_data)
+ide_build_manager_notify_message (IdeBuildManager  *self,
+                                  GParamSpec       *pspec,
+                                  IdeBuildPipeline *pipeline)
 {
-  IdeBuildManager *self = user_data;
+  IDE_ENTRY;
 
-  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (G_IS_PARAM_SPEC (pspec));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  ide_build_manager_build_async (self,
-                                 NULL,
-                                 IDE_BUILDER_BUILD_FLAGS_NONE,
-                                 NULL,
-                                 NULL,
-                                 NULL);
+  if (pipeline == self->pipeline)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_rebuild_activate (GSimpleAction *action,
-                                    GVariant      *parameter,
-                                    gpointer       user_data)
+ide_build_manager_pipeline_started (IdeBuildManager  *self,
+                                    IdeBuildPipeline *pipeline)
 {
-  IdeBuildManager *self = user_data;
+  IDE_ENTRY;
 
-  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  ide_build_manager_build_async (self,
-                                 NULL,
-                                 IDE_BUILDER_BUILD_FLAGS_FORCE_CLEAN,
-                                 NULL,
-                                 NULL,
-                                 NULL);
+  g_signal_emit (self, signals [BUILD_STARTED], 0, pipeline);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_cancel_activate (GSimpleAction *action,
-                                   GVariant      *parameter,
-                                   gpointer       user_data)
+ide_build_manager_pipeline_finished (IdeBuildManager  *self,
+                                     gboolean          failed,
+                                     IdeBuildPipeline *pipeline)
 {
-  IdeBuildManager *self = user_data;
+  IDE_ENTRY;
 
-  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  ide_build_manager_cancel (self);
+  if (failed)
+    g_signal_emit (self, signals [BUILD_FAILED], 0, pipeline);
+  else
+    g_signal_emit (self, signals [BUILD_FINISHED], 0, pipeline);
+
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_clean_activate (GSimpleAction *action,
-                                  GVariant      *parameter,
-                                  gpointer       user_data)
+ide_build_manager_invalidate_pipeline (IdeBuildManager *self)
 {
-  IdeBuildManager *self = user_data;
+  IdeConfigurationManager *config_manager;
+  g_autoptr(GError) error = NULL;
+  IdeConfiguration *config;
+  IdeContext *context;
+
+  IDE_ENTRY;
 
-  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
 
-  ide_build_manager_build_async (self,
-                                 NULL,
-                                 (IDE_BUILDER_BUILD_FLAGS_FORCE_CLEAN |
-                                  IDE_BUILDER_BUILD_FLAGS_NO_BUILD),
-                                 NULL,
-                                 NULL,
+  IDE_TRACE_MSG ("Reloading pipeline due to configuration change");
+
+  if (self->cancellable != NULL)
+    ide_build_manager_cancel (self);
+
+  g_clear_object (&self->pipeline);
+  g_clear_pointer (&self->running_time, g_timer_destroy);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_context_get_configuration_manager (context);
+  config = ide_configuration_manager_get_current (config_manager);
+
+  /*
+   * We want to set the pipeline before connecting things using the GInitable
+   * interface so that we can access the builddir from
+   * IdeRuntime.create_launcher() during pipeline addin initialization.
+   */
+  self->pipeline = g_object_new (IDE_TYPE_BUILD_PIPELINE,
+                                 "context", context,
+                                 "configuration", config,
                                  NULL);
+
+  g_signal_connect_object (self->pipeline,
+                           "diagnostic",
+                           G_CALLBACK (ide_build_manager_handle_diagnostic),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->pipeline,
+                           "notify::busy",
+                           G_CALLBACK (ide_build_manager_notify_busy),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->pipeline,
+                           "notify::message",
+                           G_CALLBACK (ide_build_manager_notify_message),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->pipeline,
+                           "started",
+                           G_CALLBACK (ide_build_manager_pipeline_started),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->pipeline,
+                           "finished",
+                           G_CALLBACK (ide_build_manager_pipeline_finished),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->diagnostic_count = 0;
+
+  g_clear_object (&self->cancellable);
+
+  /* This will cause plugins to load on the pipeline. */
+  if (!g_initable_init (G_INITABLE (self->pipeline), NULL, &error))
+    g_warning ("Failure to initialize pipeline: %s", error->message);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  IDE_EXIT;
 }
 
-static void
-ide_build_manager__build_result__notify_failed (IdeBuildManager *self,
-                                                GParamSpec      *pspec,
-                                                IdeBuildResult  *build_result)
+static gboolean
+initable_init (GInitable     *initable,
+               GCancellable  *cancellable,
+               GError       **error)
 {
+  IdeBuildManager *self = (IdeBuildManager *)initable;
+  IdeConfigurationManager *config_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_context_get_configuration_manager (context);
+
+  g_signal_connect_object (config_manager,
+                           "invalidate",
+                           G_CALLBACK (ide_build_manager_invalidate_pipeline),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_build_manager_invalidate_pipeline (self);
 
-  if (ide_build_result_get_failed (build_result))
-    g_signal_emit (self, signals [BUILD_FAILED], 0, build_result);
+  IDE_RETURN (TRUE);
 }
 
 static void
-ide_build_manager_real_build_started (IdeBuildManager *self,
-                                      IdeBuildResult  *build_result)
+ide_build_manager_real_build_started (IdeBuildManager  *self,
+                                      IdeBuildPipeline *pipeline)
 {
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  ide_build_manager_start_timer (self);
 }
 
 static void
-ide_build_manager_real_build_failed (IdeBuildManager *self,
-                                     IdeBuildResult  *build_result)
+ide_build_manager_real_build_failed (IdeBuildManager  *self,
+                                     IdeBuildPipeline *pipeline)
 {
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  ide_build_manager_stop_timer (self);
 }
 
 static void
-ide_build_manager_real_build_finished (IdeBuildManager *self,
-                                       IdeBuildResult  *build_result)
+ide_build_manager_real_build_finished (IdeBuildManager  *self,
+                                       IdeBuildPipeline *pipeline)
 {
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  ide_build_manager_stop_timer (self);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = initable_init;
 }
 
 static void
@@ -254,11 +358,12 @@ ide_build_manager_finalize (GObject *object)
 {
   IdeBuildManager *self = (IdeBuildManager *)object;
 
-  g_clear_object (&self->build_result);
-  g_clear_object (&self->signals);
-  g_clear_object (&self->actions);
+  g_clear_object (&self->pipeline);
   g_clear_object (&self->cancellable);
   g_clear_pointer (&self->last_build_time, g_date_time_unref);
+  g_clear_pointer (&self->running_time, g_timer_destroy);
+
+  ide_clear_source (&self->timer_source);
 
   G_OBJECT_CLASS (ide_build_manager_parent_class)->finalize (object);
 }
@@ -277,22 +382,22 @@ ide_build_manager_get_property (GObject    *object,
       g_value_set_boolean (value, ide_build_manager_get_busy (self));
       break;
 
-    case PROP_LAST_BUILD_TIME:
-      g_value_set_boxed (value, ide_build_manager_get_last_build_time (self));
-      break;
-
-    case PROP_HAS_DIAGNOSTICS:
-      g_value_set_boolean (value, self->has_diagnostics);
-      break;
-
     case PROP_MESSAGE:
       g_value_take_string (value, ide_build_manager_get_message (self));
       break;
 
+    case PROP_LAST_BUILD_TIME:
+      g_value_set_boxed (value, ide_build_manager_get_last_build_time (self));
+      break;
+
     case PROP_RUNNING_TIME:
       g_value_set_int64 (value, ide_build_manager_get_running_time (self));
       break;
 
+    case PROP_HAS_DIAGNOSTICS:
+      g_value_set_boolean (value, self->diagnostic_count > 0);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -306,511 +411,453 @@ ide_build_manager_class_init (IdeBuildManagerClass *klass)
   object_class->finalize = ide_build_manager_finalize;
   object_class->get_property = ide_build_manager_get_property;
 
+  /**
+   * IdeBuildManager:busy:
+   *
+   * The "busy" property indicates if there is currently a build
+   * executing. This can be bound to UI elements to display to the
+   * user that a build is active (and therefore other builds cannot
+   * be activated at the moment).
+   */
   properties [PROP_BUSY] =
     g_param_spec_boolean ("busy",
                           "Busy",
-                          "If the build manager is busy building",
+                          "If a build is actively executing",
                           FALSE,
-                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_LAST_BUILD_TIME] =
-    g_param_spec_boxed ("last-build-time",
-                        "Last Build Time",
-                        "The time the last build was submitted",
-                        G_TYPE_DATE_TIME,
-                        (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+  /**
+   * IdeBuildManager:has-diagnostics:
+   *
+   * The "has-diagnostics" property indicates that there have been
+   * diagnostics found during the last execution of the build pipeline.
+   */
   properties [PROP_HAS_DIAGNOSTICS] =
     g_param_spec_boolean ("has-diagnostics",
                           "Has Diagnostics",
-                          "If the build result has diagnostics",
+                          "Has Diagnostics",
                           FALSE,
-                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+  /**
+   * IdeBuildManager:last-build-time:
+   *
+   * The "last-build-time" property contains a #GDateTime of the time
+   * the last build request was submitted.
+   */
+  properties [PROP_LAST_BUILD_TIME] =
+    g_param_spec_boxed ("last-build-time",
+                        "Last Build Time",
+                        "The time of the last build request",
+                        G_TYPE_DATE_TIME,
+                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:message:
+   *
+   * The "message" property contains a string message describing
+   * the current state of the build process. This may be bound to
+   * UI elements to notify the user of the buid progress.
+   */
   properties [PROP_MESSAGE] =
     g_param_spec_string ("message",
                          "Message",
                          "The current build message",
                          NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:running-time:
+   *
+   * The "running-time" property can be bound by UI elements that
+   * want to track how long the current build has taken. g_object_notify()
+   * is called on a regular interval during the build so that the UI
+   * elements may automatically update.
+   *
+   * The value of this property is a #GTimeSpan, which are 64-bit signed
+   * integers with microsecond precision. See %G_USEC_PER_SEC for a constant
+   * to tranform this to seconds.
+   */
   properties [PROP_RUNNING_TIME] =
     g_param_spec_int64 ("running-time",
                         "Running Time",
-                        "The duration of the build as a GTimeSpan",
+                        "The amount of elapsed time performing the current build",
                         0,
                         G_MAXINT64,
                         0,
-                        (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
+  /**
+   * IdeBuildManager::build-started:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-started" signal is emitted when a new build has started.
+   * The build may be an incremental build. The @pipeline instance is
+   * the build pipeline which is being executed.
+   */
   signals [BUILD_STARTED] =
     g_signal_new_class_handler ("build-started",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST,
                                 G_CALLBACK (ide_build_manager_real_build_started),
+                                NULL, NULL,
                                 NULL,
-                                NULL,
-                                NULL,
-                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_RESULT);
-
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
+
+  /**
+   * IdeBuildManager::build-failed:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-failed" signal is emitted when a build that was previously
+   * notified via #IdeBuildManager::build-started has failed to complete
+   * successfully.
+   *
+   * Contrast this with #IdeBuildManager::build-finished for a successful
+   * build.
+   */
   signals [BUILD_FAILED] =
     g_signal_new_class_handler ("build-failed",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST,
                                 G_CALLBACK (ide_build_manager_real_build_failed),
+                                NULL, NULL,
                                 NULL,
-                                NULL,
-                                NULL,
-                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_RESULT);
-
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
+
+  /**
+   * IdeBuildManager::build-finished:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-finished" signal is emitted when a build completed
+   * successfully.
+   */
   signals [BUILD_FINISHED] =
     g_signal_new_class_handler ("build-finished",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST,
                                 G_CALLBACK (ide_build_manager_real_build_finished),
+                                NULL, NULL,
                                 NULL,
-                                NULL,
-                                NULL,
-                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_RESULT);
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
 }
 
 static void
-ide_build_manager_init (IdeBuildManager *self)
+ide_build_manager_action_cancel (GSimpleAction *action,
+                                 GVariant      *param,
+                                 gpointer       user_data)
 {
-  static const GActionEntry action_entries[] = {
-    { "build", ide_build_manager_build_activate },
-    { "cancel", ide_build_manager_cancel_activate },
-    { "clean", ide_build_manager_clean_activate },
-    { "rebuild", ide_build_manager_rebuild_activate },
-  };
-
-  self->signals = egg_signal_group_new (IDE_TYPE_BUILD_RESULT);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::failed",
-                                   G_CALLBACK (ide_build_manager__build_result__notify_failed),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::mode",
-                                   G_CALLBACK (ide_build_manager__build_result__notify_mode),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::running",
-                                   G_CALLBACK (ide_build_manager__build_result__notify_running),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::running-time",
-                                   G_CALLBACK (ide_build_manager__build_result__notify_running_time),
-                                   self,
-                                   G_CONNECT_SWAPPED);
+  IdeBuildManager *self = user_data;
 
-  egg_signal_group_connect_object (self->signals,
-                                   "diagnostic",
-                                   G_CALLBACK (ide_build_manager__build_result__diagnostic),
-                                   self,
-                                   G_CONNECT_SWAPPED);
+  IDE_ENTRY;
 
-  self->actions = g_simple_action_group_new ();
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_BUILD_MANAGER (self));
 
-  g_action_map_add_action_entries (G_ACTION_MAP (self->actions),
-                                   action_entries,
-                                   G_N_ELEMENTS (action_entries),
-                                   self);
+  ide_build_manager_cancel (self);
 
-  g_object_bind_property (self,
-                          "busy",
-                          g_action_map_lookup_action (G_ACTION_MAP (self->actions), "build"),
-                          "enabled",
-                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
-
-  g_object_bind_property (self,
-                          "busy",
-                          g_action_map_lookup_action (G_ACTION_MAP (self->actions), "rebuild"),
-                          "enabled",
-                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
-
-  g_object_bind_property (self,
-                          "busy",
-                          g_action_map_lookup_action (G_ACTION_MAP (self->actions), "clean"),
-                          "enabled",
-                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
-
-  g_object_bind_property (self,
-                          "busy",
-                          g_action_map_lookup_action (G_ACTION_MAP (self->actions), "cancel"),
-                          "enabled",
-                          G_BINDING_SYNC_CREATE);
-
-  g_signal_connect_object (self->actions,
-                           "action-enabled-changed",
-                           G_CALLBACK (g_action_group_action_enabled_changed),
-                           self,
-                           G_CONNECT_SWAPPED);
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_set_build_result (IdeBuildManager *self,
-                                    IdeBuildResult  *build_result)
+ide_build_manager_action_build (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
 {
+  IdeBuildManager *self = user_data;
+
   IDE_ENTRY;
 
+  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (!build_result || IDE_IS_BUILD_RESULT (build_result));
-
-  if (g_set_object (&self->build_result, build_result))
-    {
-      egg_signal_group_set_target (self->signals, build_result);
-
-      self->has_diagnostics = FALSE;
 
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
-
-      g_signal_emit (self, signals [BUILD_STARTED], 0, build_result);
-    }
+  ide_build_manager_execute_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL);
 
   IDE_EXIT;
 }
 
-static gboolean
-ide_build_manager_check_busy (IdeBuildManager  *self,
-                              GError          **error)
+static void
+ide_build_manager_action_rebuild (GSimpleAction *action,
+                                  GVariant      *param,
+                                  gpointer       user_data)
 {
+  IdeBuildManager *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (IDE_IS_BUILD_MANAGER (self));
 
-  if (ide_build_manager_get_busy (self))
-    {
-      g_set_error (error,
-                   G_IO_ERROR,
-                   G_IO_ERROR_BUSY,
-                   "%s",
-                   _("A build is already in progress"));
-      return TRUE;
-    }
+  ide_build_manager_rebuild_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL);
 
-  return FALSE;
+  IDE_EXIT;
 }
 
-static IdeBuilder *
-ide_build_manager_get_builder (IdeBuildManager  *self,
-                               GError          **error)
+static void
+ide_build_manager_action_clean (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
 {
-  IdeConfigurationManager *config_manager;
-  IdeConfiguration *config;
-  IdeBuildSystem *build_system;
-  IdeContext *context;
-
-  g_assert (IDE_IS_BUILD_MANAGER (self));
+  IdeBuildManager *self = user_data;
 
-  context = ide_object_get_context (IDE_OBJECT (self));
+  IDE_ENTRY;
 
-  config_manager = ide_context_get_configuration_manager (context);
-  config = ide_configuration_manager_get_current (config_manager);
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_BUILD_MANAGER (self));
 
-  build_system = ide_context_get_build_system (context);
+  ide_build_manager_clean_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL);
 
-  return ide_build_system_get_builder (build_system, config, error);
+  IDE_EXIT;
 }
 
 static void
-ide_build_manager_build_cb (GObject      *object,
-                            GAsyncResult *result,
-                            gpointer      user_data)
+ide_build_manager_init (IdeBuildManager *self)
 {
-  IdeBuilder *builder = (IdeBuilder *)object;
-  g_autoptr(IdeBuildResult) build_result = NULL;
-  g_autoptr(GTask) task = user_data;
-  IdeBuildManager *self;
-  GError *error = NULL;
+  static GActionEntry actions[] = {
+    { "build", ide_build_manager_action_build },
+    { "cancel", ide_build_manager_action_cancel },
+    { "clean", ide_build_manager_action_clean },
+    { "rebuild", ide_build_manager_action_rebuild },
+  };
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_BUILDER (builder));
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  build_result = ide_builder_build_finish (builder, result, &error);
-
-  g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (!build_result || IDE_IS_BUILD_RESULT (build_result));
+  self->actions = G_ACTION_GROUP (g_simple_action_group_new ());
 
-  if (self->build_result != NULL)
-    g_signal_emit (self, signals [BUILD_FINISHED], 0, self->build_result);
-
-  if (build_result == NULL)
-    {
-      IDE_TRACE_MSG ("%s", error->message);
-      g_task_return_error (task, error);
-      IDE_GOTO (failure);
-    }
-
-  g_task_return_boolean (task, TRUE);
+  g_action_map_add_action_entries (G_ACTION_MAP (self->actions),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
 
-failure:
   IDE_EXIT;
 }
 
-static void
-ide_build_manager_build_save_all_cb (GObject      *object,
-                                     GAsyncResult *result,
-                                     gpointer      user_data)
+/**
+ * ide_build_manager_get_busy:
+ * @self: An #IdeBuildManager
+ *
+ * Gets if the #IdeBuildManager is currently busy building the
+ * project.
+ *
+ * See #IdeBuildManager:busy for more information.
+ */
+gboolean
+ide_build_manager_get_busy (IdeBuildManager *self)
 {
-  IdeBufferManager *buffer_manager = (IdeBufferManager *)object;
-  g_autoptr(IdeBuildResult) build_result = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(GTask) task = user_data;
-  IdeBuildManager *self = NULL;
-  GCancellable *cancellable;
-  BuildState *state;
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
 
-  IDE_ENTRY;
+  if G_LIKELY (self->pipeline != NULL)
+    return ide_build_pipeline_get_busy (self->pipeline);
 
-  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
+  return FALSE;
+}
 
-  if (!ide_buffer_manager_save_all_finish (buffer_manager, result, &error))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      IDE_GOTO (failure);
-    }
+/**
+ * ide_build_manager_get_message:
+ * @self: An #IdeBuildManager
+ *
+ * This function returns the current build message as a string.
+ *
+ * See #IdeBuildManager:message for more information.
+ *
+ * Returns: (transfer full): A string containing the build message or %NULL
+ */
+gchar *
+ide_build_manager_get_message (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
 
-  self = g_task_get_source_object (task);
-  g_assert (IDE_IS_BUILD_MANAGER (self));
+  if G_LIKELY (self->pipeline != NULL)
+    return ide_build_pipeline_get_message (self->pipeline);
 
-  state = g_task_get_task_data (task);
-  g_assert (state != NULL);
-  g_assert (IDE_IS_BUILDER (state->builder));
+  return NULL;
+}
 
-  cancellable = g_task_get_cancellable (task);
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+/**
+ * ide_build_manager_get_last_build_time:
+ * @self: An #IdeBuildManager
+ *
+ * This function returns a #GDateTime of the last build request. If
+ * there has not yet been a build request, this will return %NULL.
+ *
+ * See #IdeBuildManager:last-build-time for more information.
+ *
+ * Returns: (nullable) (transfer none): A #GDateTime or %NULL.
+ */
+GDateTime *
+ide_build_manager_get_last_build_time (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
 
-  ide_builder_build_async (state->builder,
-                           state->build_flags,
-                           &build_result,
-                           cancellable,
-                           ide_build_manager_build_cb,
-                           g_steal_pointer (&task));
+  return self->last_build_time;
+}
 
-  ide_build_manager_set_build_result (self, build_result);
+/**
+ * ide_build_manager_get_running_time:
+ *
+ * Gets the amount of elapsed time of the current build as a
+ * #GTimeSpan.
+ *
+ * Returns: A #GTimeSpan containing the elapsed time of the build.
+ */
+GTimeSpan
+ide_build_manager_get_running_time (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), 0);
 
-failure:
-  if (self != NULL)
-    {
-      self->saving = FALSE;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
-    }
+  if (self->running_time != NULL)
+    return g_timer_elapsed (self->running_time, NULL) * G_TIME_SPAN_SECOND;
 
-  IDE_EXIT;
+  return 0;
 }
 
+/**
+ * ide_build_manager_cancel:
+ * @self: An #IdeBuildManager
+ *
+ * This function will cancel any in-flight builds.
+ *
+ * You may also activate this using the "cancel" #GAction provided
+ * by the #GActionGroup interface.
+ */
 void
-ide_build_manager_build_async (IdeBuildManager      *self,
-                               IdeBuildTarget       *build_target,
-                               IdeBuilderBuildFlags  build_flags,
-                               GCancellable         *cancellable,
-                               GAsyncReadyCallback   callback,
-                               gpointer              user_data)
+ide_build_manager_cancel (IdeBuildManager *self)
 {
-  g_autoptr(GTask) task = NULL;
-  g_autoptr(GCancellable) local_cancellable = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(IdeBuilder) builder = NULL;
-  IdeBufferManager *buffer_manager;
-  IdeContext *context;
-  BuildState *state;
+  g_autoptr(GCancellable) cancellable = NULL;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
-  g_return_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  if (cancellable == NULL)
-    cancellable = local_cancellable = g_cancellable_new ();
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_source_tag (task, ide_build_manager_build_async);
+  cancellable = g_steal_pointer (&self->cancellable);
 
-  if (ide_build_manager_check_busy (self, &error))
+  if (cancellable != NULL && !g_cancellable_is_cancelled (cancellable))
     {
-      g_task_return_error (task, g_steal_pointer (&error));
-      IDE_EXIT;
+      g_debug ("Cancelling build due to user request");
+      g_cancellable_cancel (cancellable);
     }
 
-  if (NULL == (builder = ide_build_manager_get_builder (self, &error)))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      IDE_EXIT;
-    }
-
-  state = g_slice_new0 (BuildState);
-  state->builder = g_steal_pointer (&builder);
-  state->build_flags = build_flags;
-  g_task_set_task_data (task, state, build_state_free);
-
-  g_set_object (&self->cancellable, cancellable);
-
-  /*
-   * Before we start any builds, we want to ensure that all of our buffers
-   * have been saved. So first request that the buffer manager take care
-   * of that for us.
-   */
-  self->saving = TRUE;
-  context = ide_object_get_context (IDE_OBJECT (self));
-  buffer_manager = ide_context_get_buffer_manager (context);
-  ide_buffer_manager_save_all_async (buffer_manager,
-                                     cancellable,
-                                     ide_build_manager_build_save_all_cb,
-                                     g_steal_pointer (&task));
-
-  /*
-   * Update our last build time.
-   */
-  g_clear_pointer (&self->last_build_time, g_date_time_unref);
-  self->last_build_time = g_date_time_new_now_local ();
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
-
   IDE_EXIT;
 }
 
-gboolean
-ide_build_manager_build_finish (IdeBuildManager  *self,
-                                GAsyncResult     *result,
-                                GError          **error)
+/**
+ * ide_build_manager_get_pipeline:
+ * @self: An #IdeBuildManager
+ *
+ * This function gets the current build pipeline. The pipeline will be
+ * reloaded as build configurations change.
+ *
+ * Returns: (transfer none) (nullable): An #IdeBuildPipeline.
+ */
+IdeBuildPipeline *
+ide_build_manager_get_pipeline (IdeBuildManager *self)
 {
-  gboolean ret;
-
-  IDE_ENTRY;
-
-  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE);
-
-  ret = g_task_propagate_boolean (G_TASK (result), error);
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
 
-  IDE_RETURN (ret);
+  return self->pipeline;
 }
 
 static void
-ide_build_manager_install_cb (GObject      *object,
+ide_build_manager_execute_cb (GObject      *object,
                               GAsyncResult *result,
                               gpointer      user_data)
 {
-  IdeBuilder *builder = (IdeBuilder *)object;
-  g_autoptr(IdeBuildResult) build_result = NULL;
-  g_autoptr(GTask) task = user_data;
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
   IdeBuildManager *self;
-  GError *error = NULL;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_BUILDER (builder));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
 
   self = g_task_get_source_object (task);
-  build_result = ide_builder_install_finish (builder, result, &error);
-
   g_assert (IDE_IS_BUILD_MANAGER (self));
-  g_assert (!build_result || IDE_IS_BUILD_RESULT (build_result));
 
-  if (self->build_result != NULL)
-    g_signal_emit (self, signals [BUILD_FINISHED], 0, self->build_result);
-
-  if (build_result == NULL)
+  if (!ide_build_pipeline_execute_finish (pipeline, result, &error))
     {
-      g_task_return_error (task, error);
+      g_task_return_error (task, g_steal_pointer (&error));
       IDE_GOTO (failure);
     }
 
   g_task_return_boolean (task, TRUE);
 
 failure:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
   IDE_EXIT;
 }
 
 static void
-ide_build_manager_install_save_all_cb (GObject      *object,
-                                       GAsyncResult *result,
-                                       gpointer      user_data)
+ide_build_manager_save_all_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
 {
   IdeBufferManager *buffer_manager = (IdeBufferManager *)object;
-  g_autoptr(IdeBuildResult) build_result = NULL;
-  g_autoptr(GError) error = NULL;
   g_autoptr(GTask) task = user_data;
-  IdeBuildManager *self = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeBuildManager *self;
   GCancellable *cancellable;
-  BuildState *state;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
-  g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (G_IS_TASK (task));
 
-  if (!ide_buffer_manager_save_all_finish (buffer_manager, result, &error))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      IDE_GOTO (failure);
-    }
-
   self = g_task_get_source_object (task);
-  g_assert (IDE_IS_BUILD_MANAGER (self));
-
-  state = g_task_get_task_data (task);
-  g_assert (state != NULL);
-  g_assert (IDE_IS_BUILDER (state->builder));
-
   cancellable = g_task_get_cancellable (task);
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  /*
-   * We might be able to save some build time if we can limit the target
-   * that needs to be installed. However, it's unclear that we want that
-   * because it could result in incomplete installation unless the build
-   * system compensates for it.
-   */
-
-  ide_builder_install_async (state->builder,
-                             &build_result,
-                             cancellable,
-                             ide_build_manager_install_cb,
-                             g_steal_pointer (&task));
 
-  ide_build_manager_set_build_result (self, build_result);
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-failure:
-  if (self != NULL)
+  if (!ide_buffer_manager_save_all_finish (buffer_manager, result, &error))
     {
-      self->saving = FALSE;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
     }
 
+  ide_build_pipeline_execute_async (self->pipeline,
+                                    cancellable,
+                                    ide_build_manager_execute_cb,
+                                    g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
   IDE_EXIT;
 }
 
+/**
+ * ide_build_manager_execute_async:
+ * @self: An #IdeBuildManager
+ * @phase: An #IdeBuildPhase or 0
+ * @cancellable: (nullable): A #GCancellable or %NULL
+ * @callback: A callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * This function will request that @phase is completed in the underlying
+ * build pipeline and execute a build. Upon completion, @callback will be
+ * executed and it can determine the success or failure of the operation
+ * using ide_build_manager_execute_finish().
+ */
 void
-ide_build_manager_install_async (IdeBuildManager     *self,
+ide_build_manager_execute_async (IdeBuildManager     *self,
+                                 IdeBuildPhase        phase,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
 {
   g_autoptr(GTask) task = NULL;
-  g_autoptr(IdeBuilder) builder = NULL;
-  g_autoptr(GError) error = NULL;
-  IdeBufferManager *buffer_manager;
   IdeContext *context;
-  BuildState *state;
+  IdeBufferManager *buffer_manager;
 
   IDE_ENTRY;
 
@@ -818,46 +865,81 @@ ide_build_manager_install_async (IdeBuildManager     *self,
   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_manager_install_async);
+  g_task_set_source_tag (task, ide_build_manager_execute_async);
 
-  if (ide_build_manager_check_busy (self, &error))
+  if (self->pipeline == NULL)
     {
-      g_task_return_error (task, g_steal_pointer (&error));
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_PENDING,
+                               "Cannot execute pipeline, it has not yet been prepared");
       IDE_EXIT;
     }
 
-  if (NULL == (builder = ide_build_manager_get_builder (self, &error)))
+  if (!ide_build_pipeline_request_phase (self->pipeline, phase))
     {
-      g_task_return_error (task, g_steal_pointer (&error));
+      g_task_return_boolean (task, TRUE);
       IDE_EXIT;
     }
 
-  state = g_slice_new0 (BuildState);
-  state->builder = g_steal_pointer (&builder);
-  g_task_set_task_data (task, state, build_state_free);
   g_set_object (&self->cancellable, cancellable);
 
-  /* Save the buffers before starting the build. */
-  self->saving = TRUE;
-  context = ide_object_get_context (IDE_OBJECT (self));
-  buffer_manager = ide_context_get_buffer_manager (context);
-  ide_buffer_manager_save_all_async (buffer_manager,
-                                     cancellable,
-                                     ide_build_manager_install_save_all_cb,
-                                     g_steal_pointer (&task));
+  if (self->cancellable == NULL)
+    self->cancellable = g_cancellable_new ();
 
   /*
-   * Update our last build time.
+   * Only update our "build time" if we are advancing to IDE_BUILD_PHASE_BUILD,
+   * we don't really care about "builds" for configure stages and less.
    */
-  g_clear_pointer (&self->last_build_time, g_date_time_unref);
-  self->last_build_time = g_date_time_new_now_local ();
+  if ((phase & IDE_BUILD_PHASE_MASK) >= IDE_BUILD_PHASE_BUILD)
+    {
+      g_clear_pointer (&self->last_build_time, g_date_time_unref);
+      self->last_build_time = g_date_time_new_now_local ();
+      self->diagnostic_count = 0;
+    }
+
+  /*
+   * If we are performing a real build (not just something like configure),
+   * then we want to ensure we save all the buffers. We don't want to do this
+   * on every keypress (and execute_async() could be called on every keypress)
+   * for ensuring build flags are up to date.
+   */
+  if ((phase & IDE_BUILD_PHASE_MASK) >= IDE_BUILD_PHASE_BUILD)
+    {
+      context = ide_object_get_context (IDE_OBJECT (self));
+      buffer_manager = ide_context_get_buffer_manager (context);
+      ide_buffer_manager_save_all_async (buffer_manager,
+                                         self->cancellable,
+                                         ide_build_manager_save_all_cb,
+                                         g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  ide_build_pipeline_execute_async (self->pipeline,
+                                    cancellable,
+                                    ide_build_manager_execute_cb,
+                                    g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
 
   IDE_EXIT;
 }
 
+/**
+ * ide_build_manager_execute_finish:
+ * @self: An #IdeBuildManager
+ * @result: A #GAsyncResult
+ * @error: A location for a #GError or %NULL
+ *
+ * Completes a request to ide_build_manager_execute_async().
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and @error is set.
+ */
 gboolean
-ide_build_manager_install_finish (IdeBuildManager  *self,
+ide_build_manager_execute_finish (IdeBuildManager  *self,
                                   GAsyncResult     *result,
                                   GError          **error)
 {
@@ -867,77 +949,103 @@ ide_build_manager_install_finish (IdeBuildManager  *self,
 
   g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
   g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE);
 
   ret = g_task_propagate_boolean (G_TASK (result), error);
 
   IDE_RETURN (ret);
 }
 
-gboolean
-ide_build_manager_get_busy (IdeBuildManager *self)
+static void
+ide_build_manager_clean_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
 {
-  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuildManager *self;
 
-  if (self->saving)
-    return TRUE;
+  IDE_ENTRY;
 
-  if (self->build_result != NULL)
-    return ide_build_result_get_running (self->build_result);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
 
-  return FALSE;
+  self = g_task_get_source_object (task);
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  if (!ide_build_pipeline_clean_finish (pipeline, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  g_task_return_boolean (task, TRUE);
+
+failure:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
 }
 
 void
-ide_build_manager_cancel (IdeBuildManager *self)
+ide_build_manager_clean_async (IdeBuildManager     *self,
+                               IdeBuildPhase        phase,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
 {
+  g_autoptr(GTask) task = NULL;
+
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  if (self->cancellable != NULL)
-    g_cancellable_cancel (self->cancellable);
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_manager_clean_async);
 
-  IDE_EXIT;
-}
+  if (self->pipeline == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_PENDING,
+                               "Cannot execute pipeline, it has not yet been prepared");
+      IDE_EXIT;
+    }
 
-gchar *
-ide_build_manager_get_message (IdeBuildManager *self)
-{
-  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+  g_set_object (&self->cancellable, cancellable);
 
-  if (self->build_result != NULL)
-    return ide_build_result_get_mode (self->build_result);
+  if (self->cancellable == NULL)
+    self->cancellable = g_cancellable_new ();
 
-  return g_strdup (_("Ready"));
-}
+  self->diagnostic_count = 0;
 
-/**
- * ide_build_manager_get_last_build_time:
- * @self: An #IdeBuildManager
- *
- * Gets the time the last build was started. This is %NULL until a build
- * has been executed in the context.
- *
- * Returns: (nullable) (transfer none): A #GDateTime or %NULL.
- */
-GDateTime *
-ide_build_manager_get_last_build_time (IdeBuildManager *self)
-{
-  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+  ide_build_pipeline_clean_async (self->pipeline,
+                                  phase,
+                                  self->cancellable,
+                                  ide_build_manager_clean_cb,
+                                  g_steal_pointer (&task));
 
-  return self->last_build_time;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+
+  IDE_EXIT;
 }
 
-GTimeSpan
-ide_build_manager_get_running_time (IdeBuildManager *self)
+gboolean
+ide_build_manager_clean_finish (IdeBuildManager  *self,
+                                GAsyncResult     *result,
+                                GError          **error)
 {
-  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), 0);
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
 
-  if (self->build_result == NULL)
-    return 0;
+  ret = g_task_propagate_boolean (G_TASK (result), error);
 
-  return ide_build_result_get_running_time (self->build_result);
+  IDE_RETURN (ret);
 }
 
 static gchar **
@@ -1007,3 +1115,83 @@ action_group_iface_init (GActionGroupInterface *iface)
   iface->change_action_state = ide_build_manager_change_action_state;
   iface->activate_action = ide_build_manager_activate_action;
 }
+
+static void
+ide_build_manager_rebuild_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_build_pipeline_rebuild_finish (pipeline, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+ide_build_manager_rebuild_async (IdeBuildManager     *self,
+                                 IdeBuildPhase        phase,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+  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_manager_rebuild_async);
+
+  if (self->pipeline == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_PENDING,
+                               "Cannot execute pipeline, it has not yet been prepared");
+      IDE_EXIT;
+    }
+
+  g_set_object (&self->cancellable, cancellable);
+
+  if (self->cancellable == NULL)
+    self->cancellable = g_cancellable_new ();
+
+  ide_build_pipeline_rebuild_async (self->pipeline,
+                                    phase,
+                                    self->cancellable,
+                                    ide_build_manager_rebuild_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_manager_rebuild_finish (IdeBuildManager  *self,
+                                  GAsyncResult     *result,
+                                  GError          **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
diff --git a/libide/buildsystem/ide-build-manager.h b/libide/buildsystem/ide-build-manager.h
index ffec27e..5cfc2ff 100644
--- a/libide/buildsystem/ide-build-manager.h
+++ b/libide/buildsystem/ide-build-manager.h
@@ -23,7 +23,7 @@
 
 #include "ide-object.h"
 
-#include "buildsystem/ide-builder.h"
+#include "buildsystem/ide-build-pipeline.h"
 
 G_BEGIN_DECLS
 
@@ -31,27 +31,36 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeBuildManager, ide_build_manager, IDE, BUILD_MANAGER, IdeObject)
 
-gboolean   ide_build_manager_get_busy            (IdeBuildManager       *self);
-gchar     *ide_build_manager_get_message         (IdeBuildManager       *self);
-GDateTime *ide_build_manager_get_last_build_time (IdeBuildManager       *self);
-GTimeSpan  ide_build_manager_get_running_time    (IdeBuildManager       *self);
-void       ide_build_manager_cancel              (IdeBuildManager       *self);
-void       ide_build_manager_build_async         (IdeBuildManager       *self,
-                                                  IdeBuildTarget        *build_target,
-                                                  IdeBuilderBuildFlags   build_flags,
-                                                  GCancellable          *cancellable,
-                                                  GAsyncReadyCallback    callback,
-                                                  gpointer               user_data);
-gboolean   ide_build_manager_build_finish        (IdeBuildManager       *self,
-                                                  GAsyncResult          *result,
-                                                  GError               **error);
-void       ide_build_manager_install_async       (IdeBuildManager       *self,
-                                                  GCancellable          *cancellable,
-                                                  GAsyncReadyCallback    callback,
-                                                  gpointer               user_data);
-gboolean   ide_build_manager_install_finish      (IdeBuildManager       *self,
-                                                  GAsyncResult          *result,
-                                                  GError               **error);
+gboolean          ide_build_manager_get_busy            (IdeBuildManager       *self);
+gchar            *ide_build_manager_get_message         (IdeBuildManager       *self);
+GDateTime        *ide_build_manager_get_last_build_time (IdeBuildManager       *self);
+GTimeSpan         ide_build_manager_get_running_time    (IdeBuildManager       *self);
+void              ide_build_manager_cancel              (IdeBuildManager       *self);
+IdeBuildPipeline *ide_build_manager_get_pipeline        (IdeBuildManager       *self);
+void              ide_build_manager_rebuild_async       (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+gboolean          ide_build_manager_rebuild_finish      (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+void              ide_build_manager_execute_async       (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+gboolean          ide_build_manager_execute_finish      (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+void              ide_build_manager_clean_async         (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+gboolean          ide_build_manager_clean_finish        (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
 
 G_END_DECLS
 
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..cdcbbe5
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline.c
@@ -0,0 +1,2494 @@
+/* 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 <glib/gi18n.h>
+#include <egg-counter.h>
+#include <libpeas/peas.h>
+#include <string.h>
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-enums.h"
+#include "ide-macros.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.h"
+#include "buildsystem/ide-build-stage-launcher.h"
+#include "buildsystem/ide-build-stage-private.h"
+#include "buildsystem/ide-build-system.h"
+#include "diagnostics/ide-diagnostic.h"
+#include "diagnostics/ide-source-location.h"
+#include "diagnostics/ide-source-range.h"
+#include "projects/ide-project.h"
+#include "runtimes/ide-runtime.h"
+#include "util/ide-directory-reaper.h"
+#include "vcs/ide-vcs.h"
+
+EGG_DEFINE_COUNTER (Instances, "Pipeline", "N Pipelines", "Number of Pipeline instances")
+
+/**
+ * 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;
+
+typedef struct
+{
+  guint   id;
+  GRegex *regex;
+} ErrorFormat;
+
+struct _IdeBuildPipeline
+{
+  IdeObject         parent_instance;
+
+  /*
+   * These are our extensions to the BuildPipeline. Plugins insert
+   * them and they might go about adding stages to the pipeline,
+   * add error formats, or just monitor logs.
+   */
+  PeasExtensionSet *addins;
+
+  /*
+   * This is the configuration for the build. It is a snapshot of
+   * the real configuration so that we do not need to synchronize
+   * with the UI process for accesses.
+   */
+  IdeConfiguration *configuration;
+
+  /*
+   * The IdeBuildLog is a private implementation that we use to
+   * log things from addins via observer callbacks.
+   */
+  IdeBuildLog *log;
+
+  /*
+   * These are our builddir/srcdir paths. Useful for building paths
+   * by addins. We try to create a new builddir that will be unique
+   * based on hashing of the configuration.
+   */
+  gchar *builddir;
+  gchar *srcdir;
+
+  /*
+   * This is an array of PipelineEntry, which contain information we
+   * need about the stage and an identifier that addins can use to
+   * remove their inserted stages.
+   */
+  GArray *pipeline;
+
+  /*
+   * This are used for ErrorFormat registration so that we have a
+   * single place to extract "GCC-style" warnings and errors. Other
+   * languages can also register these so they show up in the build
+   * errors panel.
+   */
+  GArray *errfmts;
+  gchar  *errfmt_current_dir;
+  gchar  *errfmt_top_dir;
+  guint   errfmt_seqnum;
+
+  /*
+   * No reference to the current stage. It is only available during
+   * the asynchronous execution of the stage.
+   */
+  IdeBuildStage *current_stage;
+
+  /*
+   * The index of our current PipelineEntry. This should start at -1
+   * to indicate that no stage is currently active.
+   */
+  gint position;
+
+  /*
+   * This is the requested mask to be built. It should be reset after
+   * performing a build so that a followup execute_async() would be
+   * innocuous.
+   */
+  IdeBuildPhase requested_mask;
+
+  /*
+   * We queue incoming tasks in case we need for a finish task to
+   * complete before our task can continue. The items in the queue
+   * are DelayedTask structs with a GTask and the type id so we
+   * can progress the task upon completion of the previous task.
+   */
+  GQueue task_queue;
+
+  /*
+   * We use this sequence number to give PipelineEntry instances a
+   * unique identifier. The addins can use this to remove their
+   * inserted build stages.
+   */
+  guint seqnum;
+
+  /*
+   * If we failed to build, this should be set.
+   */
+  guint failed : 1;
+
+  /*
+   * If we are within a built, this should be set.
+   */
+  guint busy : 1;
+
+  /*
+   * If we are in the middle of a clean operation.
+   */
+  guint in_clean : 1;
+};
+
+typedef enum
+{
+  TASK_BUILD   = 1,
+  TASK_CLEAN   = 2,
+  TASK_REBUILD = 3,
+} TaskType;
+
+typedef struct
+{
+  /*
+   * Our operation type. This will indicate one of the TaskType enum
+   * which corellate to the various async functions of the pipeline.
+   */
+  TaskType type;
+
+  /*
+   * This is an unowned pointer to the task. Since the Operation structure is
+   * the task data, we cannot reference as that would create a cycle. Instead,
+   * we just rely on this becoming invalid during the task cleanup.
+   */
+  GTask *task;
+
+  /*
+   * The phase that should be met for the given pipeline operation.
+   */
+  IdeBuildPhase phase;
+
+  union {
+    struct {
+      GPtrArray *stages;
+    } clean;
+  };
+} TaskData;
+
+static void ide_build_pipeline_queue_flush  (IdeBuildPipeline *self);
+static void ide_build_pipeline_tick_execute (IdeBuildPipeline *self,
+                                             GTask            *task);
+static void ide_build_pipeline_tick_clean   (IdeBuildPipeline *self,
+                                             GTask            *task);
+static void ide_build_pipeline_tick_rebuild (IdeBuildPipeline *self,
+                                             GTask            *task);
+static void initable_iface_init             (GInitableIface   *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeBuildPipeline, ide_build_pipeline, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  PROP_CONFIGURATION,
+  PROP_MESSAGE,
+  PROP_PHASE,
+  N_PROPS
+};
+
+enum {
+  DIAGNOSTIC,
+  STARTED,
+  FINISHED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+static const gchar *task_type_names[] = {
+  NULL,
+  "build",
+  "clean",
+  "rebuild",
+};
+
+static void
+task_data_free (gpointer data)
+{
+  TaskData *td = data;
+
+  if (td != NULL)
+    {
+      if (td->type == TASK_CLEAN)
+        g_clear_pointer (&td->clean.stages, g_ptr_array_unref);
+      td->type = 0;
+      td->task = NULL;
+      g_slice_free (TaskData, td);
+    }
+}
+
+static TaskData *
+task_data_new (GTask    *task,
+               TaskType  type)
+{
+  TaskData *td;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (type > 0);
+  g_assert (type <= TASK_REBUILD);
+
+  td = g_slice_new0 (TaskData);
+  td->type = type;
+  td->task = task;
+
+  return td;
+}
+
+static void
+clear_error_format (gpointer data)
+{
+  ErrorFormat *errfmt = data;
+
+  errfmt->id = 0;
+  g_clear_pointer (&errfmt->regex, g_regex_unref);
+}
+
+static inline const gchar *
+build_phase_nick (IdeBuildPhase phase)
+{
+  GFlagsClass *klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+  GFlagsValue *value;
+
+  g_assert (klass != NULL);
+
+  phase &= IDE_BUILD_PHASE_MASK;
+  value = g_flags_get_first_value (klass, phase);
+
+  if (value != NULL)
+    return value->value_nick;
+
+  return "unknown";
+}
+
+static IdeDiagnosticSeverity
+parse_severity (const gchar *str)
+{
+  g_autofree gchar *lower = NULL;
+
+  if (str == NULL)
+    return IDE_DIAGNOSTIC_WARNING;
+
+  lower = g_utf8_strdown (str, -1);
+
+  if (strstr (lower, "fatal") != NULL)
+    return IDE_DIAGNOSTIC_FATAL;
+
+  if (strstr (lower, "error") != NULL)
+    return IDE_DIAGNOSTIC_ERROR;
+
+  if (strstr (lower, "warning") != NULL)
+    return IDE_DIAGNOSTIC_WARNING;
+
+  if (strstr (lower, "ignored") != NULL)
+    return IDE_DIAGNOSTIC_IGNORED;
+
+  if (strstr (lower, "deprecated") != NULL)
+    return IDE_DIAGNOSTIC_DEPRECATED;
+
+  if (strstr (lower, "note") != NULL)
+    return IDE_DIAGNOSTIC_NOTE;
+
+  return IDE_DIAGNOSTIC_WARNING;
+}
+
+static IdeDiagnostic *
+create_diagnostic (IdeBuildPipeline *self,
+                   GMatchInfo       *match_info)
+{
+  g_autofree gchar *filename = NULL;
+  g_autofree gchar *line = NULL;
+  g_autofree gchar *column = NULL;
+  g_autofree gchar *message = NULL;
+  g_autofree gchar *level = NULL;
+  g_autoptr(IdeFile) file = NULL;
+  g_autoptr(IdeSourceLocation) location = NULL;
+  IdeContext *context;
+  struct {
+    gint64 line;
+    gint64 column;
+    IdeDiagnosticSeverity severity;
+  } parsed = { 0 };
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (match_info != NULL);
+
+  message = g_match_info_fetch_named (match_info, "message");
+
+  /* XXX: This is a hack to ignore a common but unuseful error message.
+   *      This really belongs somewhere else, but it's easier to do the
+   *      check here for now. We need proper callback for ErrorRegex in
+   *      the future so they can ignore it.
+   */
+  if (message == NULL || strncmp (message, "#warning _FORTIFY_SOURCE requires compiling with optimization", 
61) == 0)
+    return NULL;
+
+  filename = g_match_info_fetch_named (match_info, "filename");
+  line = g_match_info_fetch_named (match_info, "line");
+  column = g_match_info_fetch_named (match_info, "column");
+  level = g_match_info_fetch_named (match_info, "level");
+
+  if (line != NULL)
+    {
+      parsed.line = g_ascii_strtoll (line, NULL, 10);
+      if (parsed.line < 1 || parsed.line > G_MAXINT32)
+        return NULL;
+      parsed.line--;
+    }
+
+  if (column != NULL)
+    {
+      parsed.column = g_ascii_strtoll (column, NULL, 10);
+      if (parsed.column < 1 || parsed.column > G_MAXINT32)
+        return NULL;
+      parsed.column--;
+    }
+
+  parsed.severity = parse_severity (level);
+
+  if (!g_path_is_absolute (filename) && self->errfmt_current_dir != NULL)
+    {
+      const gchar *basedir = self->errfmt_current_dir;
+      gchar *path;
+
+      if (g_str_has_prefix (basedir, self->errfmt_top_dir))
+        {
+          basedir += strlen (self->errfmt_top_dir);
+          if (*basedir == '/')
+            basedir++;
+        }
+
+      path = g_build_filename (basedir, filename, NULL);
+      g_free (filename);
+      filename = path;
+    }
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!g_path_is_absolute (filename))
+    {
+      g_autoptr(GFile) child = NULL;
+      IdeVcs *vcs;
+      GFile *workdir;
+      gchar *path;
+
+      vcs = ide_context_get_vcs (context);
+      workdir = ide_vcs_get_working_directory (vcs);
+
+      child = g_file_get_child (workdir, filename);
+      path = g_file_get_path (child);
+
+      g_free (filename);
+      filename = path;
+    }
+
+  file = ide_file_new_for_path (context, filename);
+  location = ide_source_location_new (file, parsed.line, parsed.column, 0);
+
+  return ide_diagnostic_new (parsed.severity, message, location);
+}
+
+static void
+ide_build_pipeline_log_observer (IdeBuildLogStream  stream,
+                                 const gchar       *message,
+                                 gssize             message_len,
+                                 gpointer           user_data)
+{
+  IdeBuildPipeline *self = user_data;
+  const gchar *enterdir;
+
+  g_assert (stream == IDE_BUILD_LOG_STDOUT || stream == IDE_BUILD_LOG_STDERR);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (message != NULL);
+
+#define ENTERING_DIRECTORY_BEGIN "Entering directory '"
+#define ENTERING_DIRECTORY_END   "'\n"
+
+  if (message_len < 0)
+    message_len = strlen (message);
+
+  if (self->log != NULL)
+    ide_build_log_observer (stream, message, message_len, self->log);
+
+  /*
+   * This expects LANG=C, which is defined in the autotools Builder.
+   * Not the most ideal decoupling of logic, but we don't have a whole
+   * lot to work with here.
+   */
+  if (NULL != (enterdir = strstr (message, ENTERING_DIRECTORY_BEGIN)) &&
+      g_str_has_suffix (enterdir, ENTERING_DIRECTORY_END))
+    {
+      gssize len;
+
+      enterdir += IDE_LITERAL_LENGTH (ENTERING_DIRECTORY_BEGIN);
+      len = strlen (enterdir) - IDE_LITERAL_LENGTH (ENTERING_DIRECTORY_END);
+
+      if (len > 0)
+        {
+          g_free (self->errfmt_current_dir);
+          self->errfmt_current_dir = g_strndup (enterdir, len);
+          if (self->errfmt_top_dir == NULL)
+            self->errfmt_top_dir = g_strndup (enterdir, len);
+        }
+
+      return;
+    }
+
+  for (guint i = 0; i < self->errfmts->len; i++)
+    {
+      const ErrorFormat *errfmt = &g_array_index (self->errfmts, ErrorFormat, i);
+      g_autoptr(GMatchInfo) match_info = NULL;
+
+      if (g_regex_match (errfmt->regex, message, 0, &match_info))
+        {
+          g_autoptr(IdeDiagnostic) diagnostic = create_diagnostic (self, match_info);
+
+          if (diagnostic != NULL)
+            {
+              ide_build_pipeline_emit_diagnostic (self, diagnostic);
+              return;
+            }
+        }
+    }
+
+#undef ENTERING_DIRECTORY_BEGIN
+#undef ENTERING_DIRECTORY_END
+}
+
+static void
+ide_build_pipeline_release_transients (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  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))
+        {
+          IDE_TRACE_MSG ("Releasing transient stage %s at index %u",
+                         G_OBJECT_TYPE_NAME (entry->stage),
+                         i - 1);
+          g_array_remove_index (self->pipeline, i);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * 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->failed)
+    return IDE_BUILD_PHASE_FAILED;
+  else if (self->position < self->pipeline->len)
+    return g_array_index (self->pipeline, PipelineEntry, self->position).phase & IDE_BUILD_PHASE_MASK;
+  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;
+
+  if (entry->stage != NULL)
+    {
+      ide_build_stage_set_log_observer (entry->stage, NULL, NULL, NULL);
+      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)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_real_finished (IdeBuildPipeline *self,
+                                  gboolean          failed)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  IDE_EXIT;
+}
+
+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;
+}
+
+/**
+ * ide_build_pipeline_load:
+ *
+ * This manages the loading of addins which will register their necessary build
+ * stages.  We do this separately from ::constructed so that we can
+ * enable/disable the pipeline as the IdeConfiguration:ready property changes.
+ * This could happen when the device or runtime is added/removed while the
+ * application is running.
+ */
+static void
+ide_build_pipeline_load (IdeBuildPipeline *self)
+{
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (self->addins == NULL);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  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);
+
+  IDE_EXIT;
+}
+
+/*
+ * ide_build_pipeline_unload:
+ *
+ * This clears things up that were initialized in ide_build_pipeline_load. This
+ * function is safe to run even if load has not been called. We will not clean
+ * things up if the pipeline is currently executing (we can wait until its
+ * finished or dispose/finalize to cleanup up further.
+ */
+static void
+ide_build_pipeline_unload (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  g_clear_object (&self->addins);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_notify_ready (IdeBuildPipeline *self,
+                                 GParamSpec       *pspec,
+                                 IdeConfiguration *configuration)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  if (ide_configuration_get_ready (configuration))
+    ide_build_pipeline_load (self);
+  else
+    ide_build_pipeline_unload (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_finalize (GObject *object)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)object;
+
+  IDE_ENTRY;
+
+  g_assert (self->task_queue.length == 0);
+  g_queue_clear (&self->task_queue);
+
+  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_clear_pointer (&self->errfmts, g_array_unref);
+  g_clear_pointer (&self->errfmt_top_dir, g_free);
+  g_clear_pointer (&self->errfmt_current_dir, g_free);
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->finalize (object);
+
+  EGG_COUNTER_DEC (Instances);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_dispose (GObject *object)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  IDE_ENTRY;
+
+  ide_build_pipeline_unload (self);
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->dispose (object);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_pipeline_initable_init (GInitable     *initable,
+                                  GCancellable  *cancellable,
+                                  GError       **error)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)initable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+
+  g_signal_connect_object (self->configuration,
+                           "notify::ready",
+                           G_CALLBACK (ide_build_pipeline_notify_ready),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  if (ide_configuration_get_ready (self->configuration))
+    ide_build_pipeline_load (self);
+  else
+    g_message ("Configuration not ready, delaying pipeline setup");
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_build_pipeline_initable_init;
+}
+
+static void
+ide_build_pipeline_constructed (GObject *object)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+  IdeBuildSystem *build_system;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  IDE_ENTRY;
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->constructed (object);
+
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_system = ide_context_get_build_system (context);
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+
+  self->srcdir = g_file_get_path (workdir);
+  self->builddir = ide_build_system_get_builddir (build_system, self->configuration);
+
+  IDE_EXIT;
+}
+
+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_BUSY:
+      g_value_set_boolean (value, self->busy);
+      break;
+
+    case PROP_CONFIGURATION:
+      g_value_set_object (value, ide_build_pipeline_get_configuration (self));
+      break;
+
+    case PROP_MESSAGE:
+      g_value_set_string (value, ide_build_pipeline_get_message (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:busy:
+   *
+   * Gets the "busy" property. If %TRUE, the pipeline is busy executing.
+   */
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "If the pipeline is busy",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * 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));
+
+  properties [PROP_MESSAGE] =
+    g_param_spec_string ("message",
+                         "Message",
+                         "The message for the build phase",
+                         NULL,
+                         (G_PARAM_READABLE | 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)
+{
+  EGG_COUNTER_INC (Instances);
+
+  self->position = -1;
+
+  self->pipeline = g_array_new (FALSE, FALSE, sizeof (PipelineEntry));
+  g_array_set_clear_func (self->pipeline, clear_pipeline_entry);
+
+  self->errfmts = g_array_new (FALSE, FALSE, sizeof (ErrorFormat));
+  g_array_set_clear_func (self->errfmts, clear_error_format);
+
+  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;
+
+  IDE_ENTRY;
+
+  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_with_query_finish (stage, result, &error))
+    {
+      g_debug ("stage of type %s failed: %s",
+               G_OBJECT_TYPE_NAME (stage),
+               error->message);
+      self->failed = TRUE;
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_build_stage_set_completed (stage, TRUE);
+  ide_build_pipeline_tick_execute (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_tick_execute (IdeBuildPipeline *self,
+                                 GTask            *task)
+{
+  GCancellable *cancellable;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+  self->current_stage = NULL;
+
+  td = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
+
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_BUILD || td->type == TASK_REBUILD);
+  g_assert (td->task == task);
+  g_assert (td->phase != IDE_BUILD_PHASE_NONE);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* If we can skip walking the pipeline, go ahead and do so now. */
+  if (!ide_build_pipeline_request_phase (self, td->phase))
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /*
+   * Walk forward to the next stage requiring execution and asynchronously
+   * execute it. The stage may also need to perform an async ::query signal
+   * delaying pipeline execution. _ide_build_stage_execute_with_query_async()
+   * will handle all of that for us, in cause they call ide_build_stage_pause()
+   * during the ::query callback.
+   */
+  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 ((entry->phase & IDE_BUILD_PHASE_MASK) & self->requested_mask)
+        {
+          self->current_stage = entry->stage;
+
+          _ide_build_stage_execute_with_query_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_MESSAGE]);
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+
+          IDE_EXIT;
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_task_notify_completed (IdeBuildPipeline *self,
+                                          GParamSpec       *pspec,
+                                          GTask            *task)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+  IDE_TRACE_MSG ("Clearing busy bit for pipeline");
+
+  self->current_stage = NULL;
+  self->busy = FALSE;
+  self->requested_mask = 0;
+  self->in_clean = FALSE;
+
+  /*
+   * XXX: How do we ensure transients are executed with the part of the
+   *      pipeline we care about? We might just need to ensure that :busy is
+   *      FALSE before adding transients.
+   */
+  ide_build_pipeline_release_transients (self);
+
+  g_signal_emit (self, signals [FINISHED], 0, self->failed);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+
+  /*
+   * We might have a delayed addin unloading that needs to occur after the
+   * build operation completes. If the configuration is no longer valid,
+   * go ahead and unload the pipeline.
+   */
+  if (!ide_configuration_get_ready (self->configuration))
+    ide_build_pipeline_unload (self);
+  else
+    ide_build_pipeline_queue_flush (self);
+
+  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;
+  TaskData *task_data;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  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_pipeline_execute_async);
+
+  if (self->requested_mask == IDE_BUILD_PHASE_NONE)
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /*
+   * XXX: Maybe we should allow a phase to be provided with execute
+   *      now for symmetry with the others. Also, rename to build_async()?
+   */
+
+  task_data = task_data_new (task, TASK_BUILD);
+  task_data->phase = 1 << g_bit_nth_msf (self->requested_mask, -1);
+  g_task_set_task_data (task, task_data, task_data_free);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+}
+
+static gboolean
+ide_build_pipeline_do_flush (gpointer data)
+{
+  IdeBuildPipeline *self = data;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GFile) builddir = NULL;
+  g_autoptr(GError) error = NULL;
+  TaskData *task_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /*
+   * If the busy bit is set, there is nothing to do right now.
+   */
+  if (self->busy)
+    IDE_GOTO (busy_or_completed);
+
+  /* Ensure our builddir is created, or else we will fail all pending tasks. */
+  builddir = g_file_new_for_path (self->builddir);
+  if (!g_file_make_directory_with_parents (builddir, NULL, &error) &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+    {
+      GTask *failed_task;
+
+      while (NULL != (failed_task = g_queue_pop_head (&self->task_queue)))
+        {
+          g_task_return_error (failed_task, g_error_copy (error));
+          g_object_unref (failed_task);
+        }
+
+      IDE_RETURN (G_SOURCE_REMOVE);
+    }
+
+  /*
+   * Pop the next task off the queue from the head (we push to the
+   * tail and we want FIFO semantics).
+   */
+  task = g_queue_pop_head (&self->task_queue);
+  if (task == NULL)
+    IDE_GOTO (busy_or_completed);
+
+  g_assert (G_IS_TASK (task));
+  g_assert (self->busy == FALSE);
+
+  /*
+   * Now prepare the task so that when it completes we can make
+   * forward progress again.
+   */
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_build_pipeline_task_notify_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* We need access to the task data to determine how to process the task. */
+  task_data = g_task_get_task_data (task);
+
+  g_assert (task_data != NULL);
+  g_assert (task_data->type > 0);
+  g_assert (task_data->type <= TASK_REBUILD);
+  g_assert (G_IS_TASK (task_data->task));
+
+  /*
+   * Now mark the pipeline as busy to protect ourself from anything recursively
+   * calling into the pipeline.
+   */
+  self->busy = TRUE;
+  self->failed = FALSE;
+  self->position = -1;
+  self->in_clean = (task_data->type == TASK_CLEAN);
+
+  /*
+   * The following logs some helpful information about the build to our
+   * debug log. This is useful to allow users to debug some problems
+   * with our assistance (using gnome-builder -vvv).
+   */
+  {
+    g_autoptr(GString) str = g_string_new (NULL);
+    GFlagsClass *klass;
+    IdeBuildPhase phase = self->requested_mask;
+
+    klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+
+    for (guint i = 0; i < klass->n_values; i++)
+      {
+        const GFlagsValue *value = &klass->values[i];
+
+        if (phase & value->value)
+          {
+            if (str->len > 0)
+              g_string_append (str, ", ");
+            g_string_append (str, value->value_nick);
+          }
+      }
+
+    g_debug ("Executing pipeline %s stages %s with %u pipeline entries",
+             task_type_names[task_data->type],
+             str->str,
+             self->pipeline->len);
+
+    for (guint i = 0; i < self->pipeline->len; i++)
+      {
+        const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+        g_debug (" pipeline[%u]: %12s: %s [%s]",
+                 i,
+                 build_phase_nick (entry->phase),
+                 G_OBJECT_TYPE_NAME (entry->stage),
+                 ide_build_stage_get_completed (entry->stage) ? "completed" : "pending");
+      }
+  }
+
+  /*
+   * Notify any observers that a build (of some sort) is about to start.
+   */
+  g_signal_emit (self, signals [STARTED], 0);
+
+  switch (task_data->type)
+    {
+    case TASK_BUILD:
+      ide_build_pipeline_tick_execute (self, task);
+      break;
+
+    case TASK_CLEAN:
+      ide_build_pipeline_tick_clean (self, task);
+      break;
+
+    case TASK_REBUILD:
+      ide_build_pipeline_tick_rebuild (self, task);
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+busy_or_completed:
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_build_pipeline_queue_flush (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  g_timeout_add_full (G_PRIORITY_DEFAULT,
+                      0,
+                      ide_build_pipeline_do_flush,
+                      g_object_ref (self),
+                      g_object_unref);
+
+  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_pipeline_log_observer,
+                                            self,
+                                            NULL);
+
+          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.
+ *
+ * Returns: %TRUE if a stage is known to require execution.
+ */
+gboolean
+ide_build_pipeline_request_phase (IdeBuildPipeline *self,
+                                  IdeBuildPhase     phase)
+{
+  GFlagsClass *klass;
+  gboolean ret = FALSE;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, FALSE);
+
+  /*
+   * You can only request basic phases. That does not include modifiers
+   * like BEFORE, AFTER, FAILED, FINISHED.
+   */
+  phase &= IDE_BUILD_PHASE_MASK;
+
+  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:
+
+  /*
+   * If we have a stage in one of the requested phases, then we can let the
+   * caller know that they need to run execute_async() to be up to date. This
+   * is useful for situations where you might want to avoid calling
+   * execute_async() altogether. Additionally, we want to know if there are
+   * any connections to the "query" which could cause the completed state
+   * to be invalidated.
+   */
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (!(entry->phase & self->requested_mask))
+        continue;
+
+      if (!ide_build_stage_get_completed (entry->stage) ||
+          _ide_build_stage_has_query (entry->stage))
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  g_type_class_unref (klass);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * 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)
+{
+  g_autoptr(IdeSubprocessLauncher) ret = NULL;
+  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;
+    }
+
+  ret = ide_runtime_create_launcher (runtime, error);
+
+  if (ret != NULL)
+    {
+      ide_subprocess_launcher_set_cwd (ret, ide_build_pipeline_get_builddir (self));
+      ide_subprocess_launcher_set_flags (ret,
+                                         (G_SUBPROCESS_FLAGS_STDERR_PIPE |
+                                          G_SUBPROCESS_FLAGS_STDOUT_PIPE));
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+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);
+}
+
+/**
+ * ide_build_pipeline_add_error_format:
+ * @self: A #IdeBuildPipeline
+ * @regex: A regex to be compiled
+ *
+ * This can be used to add a regex that will extract errors from
+ * standard output. This is similar to the "errorformat" feature
+ * of vim to extract warnings from standard output.
+ *
+ * The regex should used named capture groups to pass information
+ * to the extraction process.
+ *
+ * Supported group names are:
+ *
+ *  • filename (a string path)
+ *  • line (an integer)
+ *  • column (an integer)
+ *  • level (a string)
+ *  • message (a string)
+ *
+ * For example, to extract warnings from GCC you might do something
+ * like the following:
+ *
+ *   "(?<filename>[a-zA-Z0-9\\-\\.\\/]+):"
+ *   "(?<line>\\d+):"
+ *   "(?<column>\\d+): "
+ *   "(?<level>[\\w\\s]+): "
+ *   "(?<message>.*)"
+ *
+ * To remove the regex, use the ide_build_pipeline_remove_error_format()
+ * function with the resulting format id returned from this function.
+ *
+ * The resulting format id will be > 0 if successful.
+ *
+ * Returns: an error format id that may be passed to
+ *   ide_build_pipeline_remove_error_format().
+ */
+guint
+ide_build_pipeline_add_error_format (IdeBuildPipeline   *self,
+                                     const gchar        *regex,
+                                     GRegexCompileFlags  flags)
+{
+  ErrorFormat errfmt = { 0 };
+  g_autoptr(GError) error = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+
+  errfmt.regex = g_regex_new (regex, G_REGEX_OPTIMIZE | flags, 0, &error);
+
+  if (errfmt.regex == NULL)
+    {
+      g_warning ("%s", error->message);
+      return 0;
+    }
+
+  errfmt.id = ++self->errfmt_seqnum;
+
+  g_array_append_val (self->errfmts, errfmt);
+
+  return errfmt.id;
+}
+
+/**
+ * ide_build_pipeline_remove_error_format:
+ * @self: An #IdeBuildPipeline
+ * @error_format_id: an identifier for the error format.
+ *
+ * Removes an error format that was registered with
+ * ide_build_pipeline_add_error_format().
+ *
+ * Returns: %TRUE if the error format was removed.
+ */
+gboolean
+ide_build_pipeline_remove_error_format (IdeBuildPipeline *self,
+                                        guint             error_format_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (error_format_id > 0, FALSE);
+
+  for (guint i = 0; i < self->errfmts->len; i++)
+    {
+      const ErrorFormat *errfmt = &g_array_index (self->errfmts, ErrorFormat, i);
+
+      if (errfmt->id == error_format_id)
+        {
+          g_array_remove_index (self->errfmts, i);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_build_pipeline_get_busy (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  return self->busy;
+}
+
+/**
+ * ide_build_pipeline_get_message:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the current message for the build pipeline. This can be
+ * shown to users in UI elements to signify progress in the
+ * build.
+ *
+ * Returns: (nullable) (transfer full): A string representing the
+ *   current stage of the build, or %NULL.
+ */
+gchar *
+ide_build_pipeline_get_message (IdeBuildPipeline *self)
+{
+  IdeBuildPhase phase;
+  const gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  if (self->in_clean)
+    return g_strdup (_("Cleaning…"));
+
+  if (self->current_stage != NULL)
+    {
+      const gchar *name = ide_build_stage_get_name (self->current_stage);
+
+      if (!ide_str_empty0 (name))
+        return g_strdup (name);
+    }
+
+  phase = ide_build_pipeline_get_phase (self);
+
+  switch (phase)
+    {
+    case IDE_BUILD_PHASE_DOWNLOADS:
+      ret = _("Downloading…");
+      break;
+
+    case IDE_BUILD_PHASE_DEPENDENCIES:
+      ret = _("Building dependencies…");
+      break;
+
+    case IDE_BUILD_PHASE_AUTOGEN:
+      ret = _("Bootstrapping…");
+      break;
+
+    case IDE_BUILD_PHASE_CONFIGURE:
+      ret = _("Configuring…");
+      break;
+
+    case IDE_BUILD_PHASE_BUILD:
+      ret = _("Building…");
+      break;
+
+    case IDE_BUILD_PHASE_INSTALL:
+      ret = _("Installing…");
+      break;
+
+    case IDE_BUILD_PHASE_EXPORT:
+      ret = _("Exporting…");
+      break;
+
+    case IDE_BUILD_PHASE_FINAL:
+      ret = _("Success");
+      break;
+
+    case IDE_BUILD_PHASE_FINISHED:
+      ret = _("Success");
+      break;
+
+    case IDE_BUILD_PHASE_FAILED:
+      ret = _("Failed");
+      break;
+
+    case IDE_BUILD_PHASE_PREPARE:
+      ret = _("Preparing…");
+      break;
+
+    case IDE_BUILD_PHASE_NONE:
+      ret = _("Ready");
+      break;
+
+    case IDE_BUILD_PHASE_AFTER:
+    case IDE_BUILD_PHASE_BEFORE:
+    default:
+      g_assert_not_reached ();
+    }
+
+  return g_strdup (ret);
+}
+
+/**
+ * ide_build_pipeline_foreach_stage:
+ * @self: An #IdeBuildPipeline
+ * @stage_callback: (scope call): A callback for each #IdePipelineStage
+ * @user_data: user data for @stage_callback
+ *
+ * This function will call @stage_callback for every #IdeBuildStage registered
+ * in the pipeline.
+ */
+void
+ide_build_pipeline_foreach_stage (IdeBuildPipeline *self,
+                                  GFunc             stage_callback,
+                                  gpointer          user_data)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (stage_callback != NULL);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      stage_callback (entry->stage, user_data);
+    }
+}
+
+static void
+ide_build_pipeline_clean_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  IdeBuildStage *stage = (IdeBuildStage *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuildPipeline *self;
+  GPtrArray *stages;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  td = g_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_CLEAN);
+  g_assert (td->task == task);
+  g_assert (td->clean.stages != NULL);
+
+  stages = td->clean.stages;
+
+  g_assert (stages != NULL);
+  g_assert (stages->len > 0);
+  g_assert (g_ptr_array_index (stages, stages->len - 1) == stage);
+
+  if (!ide_build_stage_clean_finish (stage, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  g_ptr_array_remove_index (stages, stages->len - 1);
+
+  ide_build_pipeline_tick_clean (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_tick_clean (IdeBuildPipeline *self,
+                               GTask            *task)
+{
+  GCancellable *cancellable;
+  GPtrArray *stages;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+  td = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_CLEAN);
+  g_assert (td->task == task);
+  g_assert (td->clean.stages != NULL);
+
+  stages = td->clean.stages;
+
+  if (stages->len != 0)
+    {
+      IdeBuildStage *stage = g_ptr_array_index (stages, stages->len - 1);
+
+      self->current_stage = stage;
+
+      ide_build_stage_clean_async (stage,
+                                   self,
+                                   cancellable,
+                                   ide_build_pipeline_clean_cb,
+                                   g_object_ref (task));
+
+      IDE_GOTO (notify);
+    }
+
+  g_task_return_boolean (task, TRUE);
+
+notify:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+
+  IDE_EXIT;
+}
+
+void
+ide_build_pipeline_clean_async (IdeBuildPipeline    *self,
+                                IdeBuildPhase        phase,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GPtrArray) stages = NULL;
+  IdeBuildPhase min_phase = IDE_BUILD_PHASE_FINAL;
+  IdeBuildPhase phase_mask;
+  GFlagsClass *phase_class;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_pipeline_clean_async);
+
+  td = task_data_new (task, TASK_CLEAN);
+  td->phase = phase;
+  g_task_set_task_data (task, td, task_data_free);
+
+  /*
+   * To clean the project, we go through each stage and call it's clean async
+   * vfunc pairs if they have been set. Afterwards, we ensure their
+   * IdeBuildStage:completed bit is cleared so they will run as part of the
+   * next build operation.
+   *
+   * Also, when performing a clean we walk backwards from the last stage to the
+   * present so that they can rely on things being semi-up-to-date from their
+   * point of view.
+   *
+   * To simplify the case of walking through the affected stages, we create a
+   * copy of the affected stages up front. We store them in the opposite order
+   * they need to be ran so that we only have to pop the last item after
+   * completing each stage. Otherwise we would additionally need a position
+   * variable.
+   *
+   * To calculate the phases that are affected, we subtract 1 from the min
+   * phase that was given to us. We then twos-compliment that and use it as our
+   * mask (so only our min and higher stages are cleaned).
+   */
+
+  phase_class = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < phase_class->n_values; i++)
+    {
+      const GFlagsValue *value = &phase_class->values [i];
+
+      if (value->value & phase)
+        {
+          if (value->value < min_phase)
+            min_phase = value->value;
+        }
+    }
+
+  phase_mask = ~(min_phase - 1);
+
+  stages = g_ptr_array_new_with_free_func (g_object_unref);
+
+  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) & phase_mask)
+        g_ptr_array_add (stages, g_object_ref (entry->stage));
+    }
+
+  /*
+   * Short-circuit if we don't have any stages to clean.
+   */
+  if (stages->len == 0)
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  td->clean.stages = g_steal_pointer (&stages);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_pipeline_clean_finish (IdeBuildPipeline  *self,
+                                 GAsyncResult      *result,
+                                 GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (result));
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+can_remove_builddir (IdeBuildPipeline *self)
+{
+  g_autoptr(GFile) builddir = NULL;
+  g_autoptr(GFile) cache = NULL;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /*
+   * Only remove builddir if it is in ~/.cache/ or our XDG data dirs
+   * equivalent. We don't want to accidentally remove data that might
+   * be important to the user.
+   */
+
+  cache = g_file_new_for_path (g_get_user_cache_dir ());
+  builddir = g_file_new_for_path (self->builddir);
+
+  return g_file_has_prefix (builddir, cache);
+}
+
+static void
+ide_build_pipeline_reaper_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeDirectoryReaper *reaper = (IdeDirectoryReaper *)object;
+  IdeBuildPipeline *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIRECTORY_REAPER (reaper));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  td = g_task_get_task_data (task);
+
+  g_assert (td != NULL);
+  g_assert (td->task == task);
+  g_assert (td->type == TASK_REBUILD);
+
+  cancellable = g_task_get_cancellable (task);
+  self = g_task_get_source_object (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /* Make sure our reaper completed or else we bail */
+  if (!ide_directory_reaper_execute_finish (reaper, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (td->phase == IDE_BUILD_PHASE_NONE)
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /* Perform a build using the same task and skipping the build queue. */
+  ide_build_pipeline_tick_execute (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_tick_rebuild (IdeBuildPipeline *self,
+                                 GTask            *task)
+{
+  g_autoptr(IdeDirectoryReaper) reaper = NULL;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (task));
+
+#ifndef G_DISABLE_ASSERT
+  {
+    TaskData *td = g_task_get_task_data (task);
+
+    g_assert (td != NULL);
+    g_assert (td->type == TASK_REBUILD);
+    g_assert (td->task == task);
+  }
+#endif
+
+  reaper = ide_directory_reaper_new ();
+
+  /*
+   * Check if we can remove the builddir. We don't want to do this if it is the
+   * same as the srcdir (in-tree builds).
+   */
+  if (can_remove_builddir (self))
+    {
+      g_autoptr(GFile) builddir = g_file_new_for_path (self->builddir);
+
+      ide_directory_reaper_add_directory (reaper, builddir, 0);
+    }
+
+  /*
+   * Now let the build stages add any files they might want to reap as part of
+   * the rebuild process.
+   */
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      ide_build_stage_emit_reap (entry->stage, reaper);
+      ide_build_stage_set_completed (entry->stage, FALSE);
+    }
+
+  cancellable = g_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Now execute the reaper to clean up the build files. */
+  ide_directory_reaper_execute_async (reaper,
+                                      cancellable,
+                                      ide_build_pipeline_reaper_cb,
+                                      g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+void
+ide_build_pipeline_rebuild_async (IdeBuildPipeline    *self,
+                                  IdeBuildPhase        phase,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail ((phase & ~IDE_BUILD_PHASE_MASK) == 0);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_build_pipeline_rebuild_async);
+
+  td = task_data_new (task, TASK_REBUILD);
+  td->phase = phase;
+  g_task_set_task_data (task, td, task_data_free);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_pipeline_rebuild_finish (IdeBuildPipeline  *self,
+                                   GAsyncResult      *result,
+                                   GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (G_IS_TASK (result));
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
diff --git a/libide/buildsystem/ide-build-pipeline.h b/libide/buildsystem/ide-build-pipeline.h
new file mode 100644
index 0000000..b0ce220
--- /dev/null
+++ b/libide/buildsystem/ide-build-pipeline.h
@@ -0,0 +1,128 @@
+/* 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)
+
+gboolean               ide_build_pipeline_get_busy            (IdeBuildPipeline       *self);
+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);
+gchar                 *ide_build_pipeline_get_message         (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);
+gboolean               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);
+guint                  ide_build_pipeline_add_error_format    (IdeBuildPipeline       *self,
+                                                               const gchar            *regex,
+                                                               GRegexCompileFlags      flags);
+gboolean               ide_build_pipeline_remove_error_format (IdeBuildPipeline       *self,
+                                                               guint                   error_format_id);
+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);
+void                   ide_build_pipeline_foreach_stage       (IdeBuildPipeline       *self,
+                                                               GFunc                   stage_callback,
+                                                               gpointer                user_data);
+void                   ide_build_pipeline_clean_async         (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phase,
+                                                               GCancellable           *cancellable,
+                                                               GAsyncReadyCallback     callback,
+                                                               gpointer                user_data);
+gboolean               ide_build_pipeline_clean_finish        (IdeBuildPipeline       *self,
+                                                               GAsyncResult           *result,
+                                                               GError                **error);
+void                   ide_build_pipeline_rebuild_async       (IdeBuildPipeline       *self,
+                                                               IdeBuildPhase           phase,
+                                                               GCancellable           *cancellable,
+                                                               GAsyncReadyCallback     callback,
+                                                               gpointer                user_data);
+gboolean               ide_build_pipeline_rebuild_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..0282e27
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-launcher.c
@@ -0,0 +1,456 @@
+/* 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"
+
+typedef struct
+{
+  IdeSubprocessLauncher *launcher;
+  IdeSubprocessLauncher *clean_launcher;
+  guint                  ignore_exit_status : 1;
+} IdeBuildStageLauncherPrivate;
+
+enum {
+  PROP_0,
+  PROP_CLEAN_LAUNCHER,
+  PROP_IGNORE_EXIT_STATUS,
+  PROP_LAUNCHER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE_TYPE_BUILD_STAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_build_stage_launcher_wait_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  IdeBuildStageLauncher *self = NULL;
+  IdeBuildStageLauncherPrivate *priv;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  gint exit_status;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  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_STAGE_LAUNCHER (self));
+
+  priv = ide_build_stage_launcher_get_instance_private (self);
+
+  IDE_TRACE_MSG ("  %s.ignore_exit_status=%u",
+                 G_OBJECT_TYPE_NAME (self),
+                 priv->ignore_exit_status);
+
+  if (!ide_subprocess_wait_finish (subprocess, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (ide_subprocess_get_if_signaled (subprocess))
+    {
+      g_task_return_new_error (task,
+                               G_SPAWN_ERROR,
+                               G_SPAWN_ERROR_FAILED,
+                               "The process was terminated by signal %d",
+                               ide_subprocess_get_term_sig (subprocess));
+      IDE_EXIT;
+    }
+
+  exit_status = ide_subprocess_get_exit_status (subprocess);
+
+  if (priv->ignore_exit_status)
+    IDE_GOTO (ignore_exit_failures);
+
+  if (!g_spawn_check_exit_status (exit_status, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+ignore_exit_failures:
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_stage_launcher_run (IdeBuildStage         *stage,
+                              IdeSubprocessLauncher *launcher,
+                              IdeBuildPipeline      *pipeline,
+                              GCancellable          *cancellable,
+                              GAsyncReadyCallback    callback,
+                              gpointer               user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  GSubprocessFlags flags;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!launcher || IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+  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_run);
+
+  if (launcher == NULL)
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  flags = ide_subprocess_launcher_get_flags (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 (launcher, flags);
+
+  /* Now launch the process */
+
+  subprocess = ide_subprocess_launcher_spawn (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_TRACE_MSG ("Waiting for process %s to complete, %s exit status",
+                 ide_subprocess_get_identifier (subprocess),
+                 priv->ignore_exit_status ? "ignoring" : "checking");
+
+  ide_subprocess_wait_async (subprocess,
+                             cancellable,
+                             ide_build_stage_launcher_wait_cb,
+                             g_steal_pointer (&task));
+
+  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;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ide_build_stage_launcher_run (stage, priv->launcher, pipeline, cancellable, callback, user_data);
+}
+
+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_clean_async (IdeBuildStage       *stage,
+                                      IdeBuildPipeline    *pipeline,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ide_build_stage_launcher_run (stage, priv->clean_launcher, pipeline, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_build_stage_launcher_clean_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;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_clear_object (&priv->launcher);
+  g_clear_object (&priv->clean_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_CLEAN_LAUNCHER:
+      g_value_set_object (value, ide_build_stage_launcher_get_clean_launcher (self));
+      break;
+
+    case PROP_IGNORE_EXIT_STATUS:
+      g_value_set_boolean (value, ide_build_stage_launcher_get_ignore_exit_status (self));
+      break;
+
+    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;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CLEAN_LAUNCHER:
+      ide_build_stage_launcher_set_clean_launcher (self, g_value_get_object (value));
+      break;
+
+    case PROP_IGNORE_EXIT_STATUS:
+      ide_build_stage_launcher_set_ignore_exit_status (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_LAUNCHER:
+      priv->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;
+  build_stage_class->clean_async = ide_build_stage_launcher_clean_async;
+  build_stage_class->clean_finish = ide_build_stage_launcher_clean_finish;
+
+  properties [PROP_CLEAN_LAUNCHER] =
+    g_param_spec_object ("clean-launcher",
+                         "Clean Launcher",
+                         "The subprocess launcher for cleaning",
+                         IDE_TYPE_SUBPROCESS_LAUNCHER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IGNORE_EXIT_STATUS] =
+    g_param_spec_boolean ("ignore-exit-status",
+                          "Ignore Exit Status",
+                          "If the exit status of the subprocess should be ignored",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LAUNCHER] =
+    g_param_spec_object ("launcher",
+                         "Launcher",
+                         "The subprocess launcher to execute",
+                         IDE_TYPE_SUBPROCESS_LAUNCHER,
+                         (G_PARAM_READWRITE | 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)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), NULL);
+
+  return priv->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);
+}
+
+/**
+ * ide_build_stage_launcher_get_ignore_exit_status:
+ *
+ * Gets the "ignore-exit-status" property.
+ *
+ * If set to %TRUE, a non-zero exit status from the subprocess will not cause
+ * the build stage to fail.
+ */
+gboolean
+ide_build_stage_launcher_get_ignore_exit_status (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), FALSE);
+
+  return priv->ignore_exit_status;
+}
+
+/**
+ * ide_build_stage_launcher_set_ignore_exit_status:
+ *
+ * Sets the "ignore-exit-status" property.
+ *
+ * If set to %TRUE, a non-zero exit status from the subprocess will not cause
+ * the build stage to fail.
+ */
+void
+ide_build_stage_launcher_set_ignore_exit_status (IdeBuildStageLauncher *self,
+                                                 gboolean               ignore_exit_status)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ignore_exit_status = !!ignore_exit_status;
+
+  if (priv->ignore_exit_status != ignore_exit_status)
+    {
+      priv->ignore_exit_status = ignore_exit_status;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IGNORE_EXIT_STATUS]);
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+void
+ide_build_stage_launcher_set_clean_launcher (IdeBuildStageLauncher *self,
+                                             IdeSubprocessLauncher *clean_launcher)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (clean_launcher));
+
+  if (g_set_object (&priv->clean_launcher, clean_launcher))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLEAN_LAUNCHER]);
+}
+
+/**
+ * ide_build_stage_launcher_get_clean_launcher:
+ *
+ * Returns: (nullable) (transfer none): An #IdeSubprocessLauncher or %NULL.
+ */
+IdeSubprocessLauncher *
+ide_build_stage_launcher_get_clean_launcher (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), NULL);
+
+  return priv->clean_launcher;
+}
diff --git a/libide/buildsystem/ide-build-stage-launcher.h b/libide/buildsystem/ide-build-stage-launcher.h
new file mode 100644
index 0000000..da9040b
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-launcher.h
@@ -0,0 +1,57 @@
+/* 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_DERIVABLE_TYPE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE, BUILD_STAGE_LAUNCHER, 
IdeBuildStage)
+
+struct _IdeBuildStageLauncherClass
+{
+  IdeBuildStageClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+IdeBuildStage         *ide_build_stage_launcher_new                    (IdeContext            *context,
+                                                                        IdeSubprocessLauncher *launcher);
+IdeSubprocessLauncher *ide_build_stage_launcher_get_launcher           (IdeBuildStageLauncher *self);
+IdeSubprocessLauncher *ide_build_stage_launcher_get_clean_launcher     (IdeBuildStageLauncher *self);
+void                   ide_build_stage_launcher_set_clean_launcher     (IdeBuildStageLauncher *self,
+                                                                        IdeSubprocessLauncher 
*clean_launcher);
+gboolean               ide_build_stage_launcher_get_ignore_exit_status (IdeBuildStageLauncher *self);
+void                   ide_build_stage_launcher_set_ignore_exit_status (IdeBuildStageLauncher *self,
+                                                                        gboolean               
ignore_exit_status);
+
+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..5282e62
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-mkdirs.c
@@ -0,0 +1,186 @@
+/* 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-debug.h"
+
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/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 void
+ide_build_stage_mkdirs_query (IdeBuildStage    *stage,
+                              IdeBuildPipeline *pipeline,
+                              GCancellable     *cancellable)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)stage;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  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);
+
+      if (!g_file_test (path->path, G_FILE_TEST_EXISTS))
+        {
+          ide_build_stage_set_completed (stage, FALSE);
+          IDE_EXIT;
+        }
+    }
+
+  ide_build_stage_set_completed (stage, TRUE);
+
+  IDE_EXIT;
+}
+
+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);
+
+  IDE_ENTRY;
+
+  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);
+      g_autofree gchar *message = NULL;
+      gboolean r;
+
+      if (g_file_test (path->path, G_FILE_TEST_IS_DIR))
+        continue;
+
+      message = g_strdup_printf ("Creating directory “%s”", path->path);
+      ide_build_stage_log (IDE_BUILD_STAGE (stage), IDE_BUILD_LOG_STDOUT, message, -1);
+
+      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));
+          IDE_RETURN (FALSE);
+        }
+    }
+
+  IDE_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;
+  stage_class->query = ide_build_stage_mkdirs_query;
+}
+
+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-simple-builder.h b/libide/buildsystem/ide-build-stage-mkdirs.h
similarity index 51%
copy from libide/buildsystem/ide-simple-builder.h
copy to libide/buildsystem/ide-build-stage-mkdirs.h
index 705ef74..3f25659 100644
--- a/libide/buildsystem/ide-simple-builder.h
+++ b/libide/buildsystem/ide-build-stage-mkdirs.h
@@ -1,4 +1,4 @@
-/* ide-simple-builder.h
+/* ide-build-stage-mkdirs.h
  *
  * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
@@ -16,32 +16,33 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef IDE_SIMPLE_BUILDER_H
-#define IDE_SIMPLE_BUILDER_H
+#ifndef IDE_BUILD_STAGE_MKDIRS_H
+#define IDE_BUILD_STAGE_MKDIRS_H
 
-#include "buildsystem/ide-builder.h"
-#include "buildsystem/ide-configuration.h"
+#include "ide-build-stage.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_SIMPLE_BUILDER (ide_simple_builder_get_type())
+#define IDE_TYPE_BUILD_STAGE_MKDIRS (ide_build_stage_mkdirs_get_type())
 
-G_DECLARE_DERIVABLE_TYPE (IdeSimpleBuilder, ide_simple_builder, IDE, SIMPLE_BUILDER, IdeBuilder)
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE, BUILD_STAGE_MKDIRS, 
IdeBuildStage)
 
-struct _IdeSimpleBuilderClass
+struct _IdeBuildStageMkdirsClass
 {
-  IdeBuilderClass parent_class;
+  IdeBuildStageClass parent_class;
 
   gpointer _reserved1;
   gpointer _reserved2;
   gpointer _reserved3;
   gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
 };
 
+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_SIMPLE_BUILDER_H */
+#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..0e9c6d4
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-private.h
@@ -0,0 +1,39 @@
+/* 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
+
+gboolean _ide_build_stage_has_query                 (IdeBuildStage        *self);
+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/libide/buildsystem/ide-simple-builder.h b/libide/buildsystem/ide-build-stage-transfer.h
similarity index 55%
rename from libide/buildsystem/ide-simple-builder.h
rename to libide/buildsystem/ide-build-stage-transfer.h
index 705ef74..8ae4566 100644
--- a/libide/buildsystem/ide-simple-builder.h
+++ b/libide/buildsystem/ide-build-stage-transfer.h
@@ -1,4 +1,4 @@
-/* ide-simple-builder.h
+/* ide-build-stage-transfer.h
  *
  * Copyright (C) 2016 Christian Hergert <chergert redhat com>
  *
@@ -16,32 +16,20 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef IDE_SIMPLE_BUILDER_H
-#define IDE_SIMPLE_BUILDER_H
+#ifndef IDE_BUILD_STAGE_TRANSFER_H
+#define IDE_BUILD_STAGE_TRANSFER_H
 
-#include "buildsystem/ide-builder.h"
-#include "buildsystem/ide-configuration.h"
+#include "buildsystem/ide-build-stage.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_SIMPLE_BUILDER (ide_simple_builder_get_type())
+#define IDE_TYPE_BUILD_STAGE_TRANSFER (ide_build_stage_transfer_get_type())
 
-G_DECLARE_DERIVABLE_TYPE (IdeSimpleBuilder, ide_simple_builder, IDE, SIMPLE_BUILDER, IdeBuilder)
+G_DECLARE_FINAL_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE, BUILD_STAGE_TRANSFER, 
IdeBuildStage)
 
-struct _IdeSimpleBuilderClass
-{
-  IdeBuilderClass parent_class;
-
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
-};
+IdeBuildStageTransfer *ide_build_stage_transfer_new (IdeContext  *context,
+                                                     IdeTransfer *transfer);
 
 G_END_DECLS
 
-#endif /* IDE_SIMPLE_BUILDER_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..cf8e133
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage.c
@@ -0,0 +1,899 @@
+/* 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 "ide-debug.h"
+
+#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,
+  REAP,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+typedef struct
+{
+  IdeBuildStage     *self;
+  GOutputStream     *stream;
+  IdeBuildLogStream  stream_type;
+} Tail;
+
+static Tail *
+tail_new (IdeBuildStage     *self,
+          GOutputStream     *stream,
+          IdeBuildLogStream  stream_type)
+{
+  Tail *tail;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (!stream || G_IS_OUTPUT_STREAM (stream));
+  g_assert (stream_type == IDE_BUILD_LOG_STDOUT || stream_type == IDE_BUILD_LOG_STDERR);
+
+  tail = g_slice_new0 (Tail);
+  tail->self = g_object_ref (self);
+  tail->stream = stream ? g_object_ref (stream) : NULL;
+  tail->stream_type = stream_type;
+
+  return tail;
+}
+
+static void
+tail_free (Tail *tail)
+{
+  IDE_ENTRY;
+
+  g_clear_object (&tail->self);
+  g_clear_object (&tail->stream);
+  g_slice_free (Tail, tail);
+
+  IDE_EXIT;
+}
+
+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_real_clean_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 (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_real_clean_async);
+
+  ide_build_stage_set_completed (self, FALSE);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_build_stage_real_clean_finish (IdeBuildStage  *self,
+                                   GAsyncResult   *result,
+                                   GError        **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+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;
+  klass->clean_async = ide_build_stage_real_clean_async;
+  klass->clean_finish = ide_build_stage_real_clean_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);
+
+  signals [REAP] =
+    g_signal_new ("reap",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeBuildStageClass, reap),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_DIRECTORY_REAPER);
+}
+
+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. We need
+   * to do this as some async reads to be proxied to the stream may occur after
+   * the execute_finish() completes.
+   *
+   * The Tail structure has it's own reference to stdout_stream.
+   */
+  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;
+}
+
+static void
+ide_build_stage_log_internal (IdeBuildStage     *self,
+                              IdeBuildLogStream  stream_type,
+                              GOutputStream     *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
+   * specially now and then exit without calling the observer.
+   */
+  if (stream != NULL)
+    {
+      gsize count;
+
+      if G_UNLIKELY (message_len < 0)
+        message_len = strlen (message);
+
+      g_output_stream_write_all (stream, message, message_len, &count, NULL, NULL);
+      g_output_stream_write_all (stream, "\n", 1, &count, NULL, NULL);
+
+      return;
+    }
+
+  if G_LIKELY (priv->observer != NULL)
+    priv->observer (stream_type, message, message_len, priv->observer_data);
+}
+
+void
+ide_build_stage_log (IdeBuildStage     *self,
+                     IdeBuildLogStream  stream_type,
+                     const gchar       *message,
+                     gssize             message_len)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  if (stream_type == IDE_BUILD_LOG_STDOUT)
+    ide_build_stage_log_internal (self, stream_type, priv->stdout_stream, message, message_len);
+  else
+    ide_build_stage_log_internal (self, stream_type, NULL, message, message_len);
+}
+
+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;
+  g_autoptr(GError) error = 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, &error);
+
+  if (error == NULL)
+    {
+      if (line == NULL)
+        goto cleanup;
+
+      ide_build_stage_log_internal (tail->self, tail->stream_type, tail->stream, line, (gssize)n_read);
+
+      if G_UNLIKELY (g_input_stream_is_closed (G_INPUT_STREAM (stream)))
+        goto cleanup;
+
+      g_data_input_stream_read_line_async (stream,
+                                           G_PRIORITY_DEFAULT,
+                                           NULL,
+                                           ide_build_stage_observe_stream_cb,
+                                           tail);
+
+      return;
+    }
+
+  g_debug ("%s", error->message);
+
+cleanup:
+  tail_free (tail);
+}
+
+
+static void
+ide_build_stage_observe_stream (IdeBuildStage     *self,
+                                IdeBuildLogStream  stream_type,
+                                GInputStream      *stream)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  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);
+
+  IDE_TRACE_MSG ("Logging subprocess stream of type %s as %s",
+                 G_OBJECT_TYPE_NAME (data_stream),
+                 stream_type == IDE_BUILD_LOG_STDOUT ? "stdout" : "stderr");
+
+  if (stream_type == IDE_BUILD_LOG_STDOUT)
+    tail = tail_new (self, priv->stdout_stream, stream_type);
+  else
+    tail = tail_new (self, NULL, 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;
+
+  IDE_ENTRY;
+
+  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);
+
+  IDE_EXIT;
+}
+
+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;
+}
+
+gboolean
+_ide_build_stage_has_query (IdeBuildStage *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return g_signal_has_handler_pending (self, signals [QUERY], 0, FALSE) ||
+         IDE_BUILD_STAGE_GET_CLASS (self)->query != NULL;
+}
+
+void
+ide_build_stage_clean_async (IdeBuildStage       *self,
+                             IdeBuildPipeline    *pipeline,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  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));
+
+  IDE_BUILD_STAGE_GET_CLASS (self)->clean_async (self, pipeline, cancellable, callback, user_data);
+}
+
+gboolean
+ide_build_stage_clean_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 IDE_BUILD_STAGE_GET_CLASS (self)->clean_finish (self, result, error);
+}
+
+void
+ide_build_stage_emit_reap (IdeBuildStage      *self,
+                           IdeDirectoryReaper *reaper)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_DIRECTORY_REAPER (reaper));
+
+  g_signal_emit (self, signals [REAP], 0, reaper);
+
+  IDE_EXIT;
+}
diff --git a/libide/buildsystem/ide-build-stage.h b/libide/buildsystem/ide-build-stage.h
new file mode 100644
index 0000000..2a799b5
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage.h
@@ -0,0 +1,204 @@
+/* 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"
+#include "util/ide-directory-reaper.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::clean_async:
+   * @self: A #IdeBuildStage
+   * @pipeline: An #IdeBuildPipeline
+   * @cancellable: (nullable): A #GCancellable or %NULL
+   * @callback: An async callback
+   * @user_data: user data for @callback
+   *
+   * This function will perform the clean operation.
+   */
+  void     (*clean_async)    (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+
+  /**
+   * IdeBuildStage::clean_finish:
+   * @self: A #IdeBuildStage
+   * @result: A #GErrorResult
+   * @error: A location for a #GError or %NULL.
+   *
+   * Completes an async operation to ide_build_stage_clean_async().
+   *
+   * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+   */
+  gboolean (*clean_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);
+
+  /**
+   * IdeBuildStage::reap:
+   * @self: An #IdeBuildStage
+   * @reaper: An #IdeDirectoryReaper
+   *
+   * This signal is emitted when a request to rebuild the project has
+   * occurred. This allows build stages to ensure that certain files are
+   * removed from the system. For example, an autotools build stage might
+   * request that "configure" is removed so that autogen.sh will be executed
+   * as part of the next build.
+   */
+  void     (*reap)           (IdeBuildStage        *self,
+                              IdeDirectoryReaper   *reaper);
+
+  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_clean_async      (IdeBuildStage        *self,
+                                                 IdeBuildPipeline     *pipeline,
+                                                 GCancellable         *cancellable,
+                                                 GAsyncReadyCallback   callback,
+                                                 gpointer              user_data);
+gboolean       ide_build_stage_clean_finish     (IdeBuildStage        *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error);
+void           ide_build_stage_pause            (IdeBuildStage        *self);
+void           ide_build_stage_unpause          (IdeBuildStage        *self);
+void           ide_build_stage_emit_reap        (IdeBuildStage        *self,
+                                                 IdeDirectoryReaper   *reaper);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_H */
+
diff --git a/libide/buildsystem/ide-build-system.c b/libide/buildsystem/ide-build-system.c
index b5a9c1a..7fc855a 100644
--- a/libide/buildsystem/ide-build-system.c
+++ b/libide/buildsystem/ide-build-system.c
@@ -19,13 +19,13 @@
 #define G_LOG_DOMAIN "ide-build-system"
 
 #include "ide-context.h"
+#include "ide-debug.h"
 #include "ide-object.h"
 
 #include "buildsystem/ide-build-system.h"
-#include "buildsystem/ide-builder.h"
 #include "buildsystem/ide-configuration.h"
-#include "buildsystem/ide-configuration-manager.h"
 #include "files/ide-file.h"
+#include "projects/ide-project.h"
 
 G_DEFINE_INTERFACE (IdeBuildSystem, ide_build_system, IDE_TYPE_OBJECT)
 
@@ -53,27 +53,61 @@ ide_build_system_get_priority (IdeBuildSystem *self)
   return 0;
 }
 
-static IdeBuilder *
-ide_build_system_real_get_builder (IdeBuildSystem    *self,
-                                   IdeConfiguration  *configuration,
-                                   GError           **error)
+static void
+ide_build_system_real_get_build_flags_async (IdeBuildSystem      *self,
+                                             IdeFile             *file,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
 {
-  g_assert (IDE_IS_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_CONFIGURATION (configuration));
+  g_task_report_new_error (self,
+                           callback,
+                           user_data,
+                           ide_build_system_real_get_build_flags_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Fetching build flags is not supported");
+}
 
-  g_set_error (error,
-               G_IO_ERROR,
-               G_IO_ERROR_NOT_SUPPORTED,
-               "%s() is not supported on %s build system.",
-               G_STRFUNC, G_OBJECT_TYPE_NAME (self));
+static gchar **
+ide_build_system_real_get_build_flags_finish (IdeBuildSystem  *self,
+                                              GAsyncResult    *result,
+                                              GError         **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
 
-  return NULL;
+static void
+ide_build_system_real_get_build_targets_async (IdeBuildSystem      *self,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+  g_task_report_new_error (self,
+                           callback,
+                           user_data,
+                           ide_build_system_real_get_build_targets_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Fetching build targets is not supported");
+}
+
+static GPtrArray *
+ide_build_system_real_get_build_targets_finish (IdeBuildSystem  *self,
+                                                GAsyncResult    *result,
+                                                GError         **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
 }
 
 static void
 ide_build_system_default_init (IdeBuildSystemInterface *iface)
 {
-  iface->get_builder = ide_build_system_real_get_builder;
+  iface->get_build_flags_async = ide_build_system_real_get_build_flags_async;
+  iface->get_build_flags_finish = ide_build_system_real_get_build_flags_finish;
+  iface->get_build_targets_finish = ide_build_system_real_get_build_targets_finish;
+  iface->get_build_targets_async = ide_build_system_real_get_build_targets_async;
+  iface->get_build_targets_finish = ide_build_system_real_get_build_targets_finish;
 
   properties [PROP_PROJECT_FILE] =
     g_param_spec_object ("project-file",
@@ -161,81 +195,6 @@ ide_build_system_new_finish (GAsyncResult  *result,
   return ret ? IDE_BUILD_SYSTEM (ret) : NULL;
 }
 
-/**
- * ide_build_system_get_builder:
- * @system: The #IdeBuildSystem to perform the build.
- * @configuration: An #IdeConfiguration.
- *
- * This function returns an #IdeBuilder that can be used to perform a
- * build of the project using the configuration specified.
- *
- * See ide_builder_build_async() for more information.
- *
- * Returns: (transfer full): An #IdeBuilder or %NULL and @error is set.
- */
-IdeBuilder *
-ide_build_system_get_builder (IdeBuildSystem    *system,
-                              IdeConfiguration  *configuration,
-                              GError           **error)
-{
-  IdeBuilder *ret;
-
-  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (system), NULL);
-  g_return_val_if_fail (IDE_IS_CONFIGURATION (configuration), NULL);
-
-  ret = IDE_BUILD_SYSTEM_GET_IFACE (system)->get_builder (system, configuration, error);
-
-  if (ret != NULL)
-    {
-      IdeContext *context;
-
-      context = ide_object_get_context (IDE_OBJECT (system));
-      ide_context_hold_for_object (context, ret);
-    }
-
-  return ret;
-}
-
-static IdeBuilder *
-get_default_builder (IdeBuildSystem  *self,
-                     GError         **error)
-{
-  IdeConfigurationManager *config_manager;
-  IdeConfiguration *config;
-  IdeContext *context;
-
-  g_assert (IDE_IS_BUILD_SYSTEM (self));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  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));
-
-  return ide_build_system_get_builder (IDE_BUILD_SYSTEM (self), config, error);
-}
-
-static void
-ide_build_system_get_build_flags_cb (GObject      *object,
-                                     GAsyncResult *result,
-                                     gpointer      user_data)
-{
-  IdeBuilder *builder = (IdeBuilder *)object;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-  g_auto(GStrv) flags = NULL;
-
-  g_assert (IDE_IS_BUILDER (builder));
-
-  if (NULL == (flags = ide_builder_get_build_flags_finish (builder, result, &error)))
-    g_task_return_error (task, g_steal_pointer (&error));
-  else
-    g_task_return_pointer (task, g_steal_pointer (&flags), (GDestroyNotify)g_strfreev);
-}
-
 void
 ide_build_system_get_build_flags_async (IdeBuildSystem      *self,
                                         IdeFile             *file,
@@ -243,28 +202,15 @@ ide_build_system_get_build_flags_async (IdeBuildSystem      *self,
                                         GAsyncReadyCallback  callback,
                                         gpointer             user_data)
 {
-  g_autoptr(GTask) task = NULL;
-  g_autoptr(IdeBuilder) builder = NULL;
-  g_autoptr(GError) error = NULL;
+  IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
   g_return_if_fail (IDE_IS_FILE (file));
   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_system_get_build_flags_async);
+  IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_async (self, file, cancellable, callback, user_data);
 
-  if (NULL == (builder = get_default_builder (self, &error)))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-
-  ide_builder_get_build_flags_async (builder,
-                                     file,
-                                     cancellable,
-                                     ide_build_system_get_build_flags_cb,
-                                     g_steal_pointer (&task));
+  IDE_EXIT;
 }
 
 /**
@@ -277,28 +223,16 @@ ide_build_system_get_build_flags_finish (IdeBuildSystem  *self,
                                          GAsyncResult    *result,
                                          GError         **error)
 {
-  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  gchar **ret;
 
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
+  IDE_ENTRY;
 
-static void
-ide_build_system_get_build_targets_cb (GObject      *object,
-                                       GAsyncResult *result,
-                                       gpointer      user_data)
-{
-  IdeBuilder *builder = (IdeBuilder *)object;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GPtrArray) targets = NULL;
-  g_autoptr(GError) error = NULL;
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
 
-  g_assert (IDE_IS_BUILDER (builder));
+  ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_finish (self, result, error);
 
-  if (NULL == (targets = ide_builder_get_build_targets_finish (builder, result, &error)))
-    g_task_return_error (task, g_steal_pointer (&error));
-  else
-    g_task_return_pointer (task, g_steal_pointer (&targets), (GDestroyNotify)g_ptr_array_unref);
+  IDE_RETURN (ret);
 }
 
 void
@@ -307,26 +241,14 @@ ide_build_system_get_build_targets_async (IdeBuildSystem      *self,
                                           GAsyncReadyCallback  callback,
                                           gpointer             user_data)
 {
-  g_autoptr(GTask) task = NULL;
-  g_autoptr(IdeBuilder) builder = NULL;
-  g_autoptr(GError) error = NULL;
+  IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
   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_system_get_build_targets_async);
-
-  if (NULL == (builder = get_default_builder (self, &error)))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
+  IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_targets_async (self, cancellable, callback, user_data);
 
-  ide_builder_get_build_targets_async (builder,
-                                       cancellable,
-                                       ide_build_system_get_build_targets_cb,
-                                       g_steal_pointer (&task));
+  IDE_EXIT;
 }
 
 /**
@@ -340,8 +262,58 @@ ide_build_system_get_build_targets_finish (IdeBuildSystem  *self,
                                            GAsyncResult    *result,
                                            GError         **error)
 {
+  GPtrArray *ret;
+
+  IDE_ENTRY;
+
   g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
   g_return_val_if_fail (G_IS_TASK (result), NULL);
 
-  return g_task_propagate_pointer (G_TASK (result), error);
+  ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_targets_finish (self, result, error);
+
+  IDE_RETURN (ret);
+}
+
+gchar *
+ide_build_system_get_builddir (IdeBuildSystem   *self,
+                               IdeConfiguration *configuration)
+{
+  gchar *ret = NULL;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (configuration), NULL);
+
+  if (IDE_BUILD_SYSTEM_GET_IFACE (self)->get_builddir)
+    ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_builddir (self, configuration);
+
+  if (ret == NULL)
+    {
+      g_autofree gchar *name = NULL;
+      const gchar *project_id;
+      const gchar *config_id;
+      const gchar *device_id;
+      const gchar *runtime_id;
+      IdeContext *context;
+      IdeProject *project;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      project = ide_context_get_project (context);
+      project_id = ide_project_get_id (project);
+      config_id = ide_configuration_get_id (configuration);
+      device_id = ide_configuration_get_device_id (configuration);
+      runtime_id = ide_configuration_get_runtime_id (configuration);
+
+      name = g_strdup_printf ("%s-%s-%s", config_id, device_id, runtime_id);
+
+      ret = g_build_filename (g_get_user_cache_dir (),
+                              "gnome-builder",
+                              "builds",
+                              project_id,
+                              name,
+                              NULL);
+    }
+
+  IDE_RETURN (ret);
 }
diff --git a/libide/buildsystem/ide-build-system.h b/libide/buildsystem/ide-build-system.h
index b1b779e..1fd05af 100644
--- a/libide/buildsystem/ide-build-system.h
+++ b/libide/buildsystem/ide-build-system.h
@@ -33,13 +33,26 @@ struct _IdeBuildSystemInterface
 {
   GTypeInterface parent_iface;
 
-  gint             (*get_priority) (IdeBuildSystem       *system);
-  IdeBuilder      *(*get_builder)  (IdeBuildSystem       *system,
-                                    IdeConfiguration     *configuration,
-                                    GError              **error);
+  gint        (*get_priority)             (IdeBuildSystem       *self);
+  void        (*get_build_flags_async)    (IdeBuildSystem       *self,
+                                           IdeFile              *file,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+  gchar     **(*get_build_flags_finish)   (IdeBuildSystem       *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+  void        (*get_build_targets_async)  (IdeBuildSystem       *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+  GPtrArray  *(*get_build_targets_finish) (IdeBuildSystem       *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+  gchar      *(*get_builddir)             (IdeBuildSystem       *self,
+                                           IdeConfiguration     *configuration);
 };
 
-gint            ide_build_system_get_priority             (IdeBuildSystem       *self);
 void            ide_build_system_new_async                (IdeContext           *context,
                                                            GFile                *project_file,
                                                            GCancellable         *cancellable,
@@ -47,15 +60,9 @@ void            ide_build_system_new_async                (IdeContext
                                                            gpointer              user_data);
 IdeBuildSystem *ide_build_system_new_finish               (GAsyncResult         *result,
                                                            GError              **error);
-IdeBuilder     *ide_build_system_get_builder              (IdeBuildSystem       *system,
-                                                           IdeConfiguration     *configuration,
-                                                           GError              **error);
-
-/*
- * The following is convenience API for the legacy design to allow
- * querying using the current IdeConfiguration.
- */
-
+gint            ide_build_system_get_priority             (IdeBuildSystem       *self);
+gchar          *ide_build_system_get_builddir             (IdeBuildSystem       *self,
+                                                           IdeConfiguration     *configuration);
 void            ide_build_system_get_build_flags_async    (IdeBuildSystem       *self,
                                                            IdeFile              *file,
                                                            GCancellable         *cancellable,
diff --git a/libide/buildsystem/ide-configuration-manager.c b/libide/buildsystem/ide-configuration-manager.c
index 0d7ddf2..e924fb1 100644
--- a/libide/buildsystem/ide-configuration-manager.c
+++ b/libide/buildsystem/ide-configuration-manager.c
@@ -25,8 +25,6 @@
 #include "ide-internal.h"
 #include "ide-macros.h"
 
-#include "buildsystem/ide-build-command.h"
-#include "buildsystem/ide-build-command-queue.h"
 #include "buildsystem/ide-configuration-manager.h"
 #include "buildsystem/ide-configuration.h"
 #include "buildsystem/ide-environment.h"
@@ -61,7 +59,13 @@ enum {
   LAST_PROP
 };
 
+enum {
+  INVALIDATE,
+  N_SIGNALS
+};
+
 static GParamSpec *properties [LAST_PROP];
+static guint signals [N_SIGNALS];
 
 static void
 load_string (IdeConfiguration *configuration,
@@ -116,36 +120,6 @@ load_environ (IdeConfiguration *configuration,
     }
 }
 
-static void
-load_command_queue (IdeBuildCommandQueue *cmdq,
-                    GKeyFile             *key_file,
-                    const gchar          *group,
-                    const gchar          *name)
-
-{
-  g_auto(GStrv) commands = NULL;
-
-  g_assert (IDE_IS_BUILD_COMMAND_QUEUE (cmdq));
-  g_assert (key_file != NULL);
-  g_assert (group != NULL);
-  g_assert (name != NULL);
-
-  commands = g_key_file_get_string_list (key_file, group, name, NULL, NULL);
-
-  if (commands != NULL)
-    {
-      for (guint i = 0; commands [i]; i++)
-        {
-          g_autoptr(IdeBuildCommand) command = NULL;
-
-          command = g_object_new (IDE_TYPE_BUILD_COMMAND,
-                                  "command-text", commands [i],
-                                  NULL);
-          ide_build_command_queue_append (cmdq, command);
-        }
-    }
-}
-
 static gboolean
 ide_configuration_manager_load (IdeConfigurationManager  *self,
                                 GKeyFile                 *key_file,
@@ -174,24 +148,6 @@ ide_configuration_manager_load (IdeConfigurationManager  *self,
   load_string (configuration, key_file, group, "prefix", "prefix");
   load_string (configuration, key_file, group, "app-id", "app-id");
 
-  if (g_key_file_has_key (key_file, group, "prebuild", NULL))
-    {
-      g_autoptr(IdeBuildCommandQueue) cmdq = NULL;
-
-      cmdq = ide_build_command_queue_new ();
-      load_command_queue (cmdq, key_file, group, "prebuild");
-      _ide_configuration_set_prebuild (configuration, cmdq);
-    }
-
-  if (g_key_file_has_key (key_file, group, "postbuild", NULL))
-    {
-      g_autoptr(IdeBuildCommandQueue) cmdq = NULL;
-
-      cmdq = ide_build_command_queue_new ();
-      load_command_queue (cmdq, key_file, group, "postbuild");
-      _ide_configuration_set_postbuild (configuration, cmdq);
-    }
-
   env_group = g_strdup_printf ("%s.environment", group);
 
   if (g_key_file_has_group (key_file, env_group))
@@ -617,6 +573,18 @@ ide_configuration_manager_class_init (IdeConfigurationManagerClass *klass)
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  /**
+   * IdeConfigurationManager::invalidate:
+   *
+   * This signal is emitted any time a new configuration is selected or the
+   * currently selected configurations state changes.
+   */
+  signals [INVALIDATE] =
+    g_signal_new ("invalidate",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
 static void
@@ -759,6 +727,8 @@ ide_configuration_manager_set_current (IdeConfigurationManager *self,
 
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT]);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_DISPLAY_NAME]);
+
+      g_signal_emit (self, signals [INVALIDATE], 0);
     }
 }
 
@@ -796,6 +766,8 @@ ide_configuration_manager_changed (IdeConfigurationManager *self,
   self->change_count++;
 
   ide_configuration_manager_queue_writeback (self);
+
+  g_signal_emit (self, signals [INVALIDATE], 0);
 }
 
 void
diff --git a/libide/buildsystem/ide-configuration.c b/libide/buildsystem/ide-configuration.c
index 1fae34c..f836337 100644
--- a/libide/buildsystem/ide-configuration.c
+++ b/libide/buildsystem/ide-configuration.c
@@ -24,7 +24,6 @@
 #include "ide-debug.h"
 #include "ide-internal.h"
 
-#include "buildsystem/ide-build-command-queue.h"
 #include "buildsystem/ide-configuration.h"
 #include "buildsystem/ide-configuration-manager.h"
 #include "buildsystem/ide-environment.h"
@@ -47,9 +46,6 @@ struct _IdeConfiguration
 
   IdeEnvironment *environment;
 
-  IdeBuildCommandQueue *prebuild;
-  IdeBuildCommandQueue *postbuild;
-
   GHashTable     *internal;
 
   gint            parallelism;
@@ -58,6 +54,14 @@ struct _IdeConfiguration
   guint           dirty : 1;
   guint           debug : 1;
   guint           is_snapshot : 1;
+
+  /*
+   * These are used to determine if we can make progress building
+   * with this configuration. When devices are added/removed, the
+   * IdeConfiguration:ready property will be notified.
+   */
+  guint           device_ready : 1;
+  guint           runtime_ready : 1;
 };
 
 G_DEFINE_TYPE (IdeConfiguration, ide_configuration, IDE_TYPE_OBJECT)
@@ -74,6 +78,7 @@ enum {
   PROP_ID,
   PROP_PARALLELISM,
   PROP_PREFIX,
+  PROP_READY,
   PROP_RUNTIME,
   PROP_RUNTIME_ID,
   PROP_APP_ID,
@@ -156,14 +161,22 @@ ide_configuration_device_manager_items_changed (IdeConfiguration *self,
                                                 IdeDeviceManager *device_manager)
 {
   IdeDevice *device;
+  gboolean device_ready;
 
   g_assert (IDE_IS_CONFIGURATION (self));
   g_assert (IDE_IS_DEVICE_MANAGER (device_manager));
 
   device = ide_device_manager_get_device (device_manager, self->device_id);
+  device_ready = !!device;
 
-  if (device != NULL)
+  if (!self->device_ready && device_ready)
     ide_device_prepare_configuration (device, self);
+
+  if (device_ready != self->device_ready)
+    {
+      self->device_ready = device_ready;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]);
+    }
 }
 
 static void
@@ -174,14 +187,22 @@ ide_configuration_runtime_manager_items_changed (IdeConfiguration  *self,
                                                  IdeRuntimeManager *runtime_manager)
 {
   IdeRuntime *runtime;
+  gboolean runtime_ready;
 
   g_assert (IDE_IS_CONFIGURATION (self));
   g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
 
   runtime = ide_runtime_manager_get_runtime (runtime_manager, self->runtime_id);
+  runtime_ready = !!runtime;
 
-  if (runtime != NULL)
+  if (!self->runtime_ready && runtime_ready)
     ide_runtime_prepare_configuration (runtime, self);
+
+  if (runtime_ready != self->runtime_ready)
+    {
+      self->runtime_ready = runtime_ready;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]);
+    }
 }
 
 static void
@@ -240,8 +261,6 @@ ide_configuration_finalize (GObject *object)
   IdeConfiguration *self = (IdeConfiguration *)object;
 
   g_clear_object (&self->environment);
-  g_clear_object (&self->prebuild);
-  g_clear_object (&self->postbuild);
 
   g_clear_pointer (&self->internal, g_hash_table_unref);
   g_clear_pointer (&self->config_opts, g_free);
@@ -297,6 +316,10 @@ ide_configuration_get_property (GObject    *object,
       g_value_set_int (value, ide_configuration_get_parallelism (self));
       break;
 
+    case PROP_READY:
+      g_value_set_boolean (value, ide_configuration_get_ready (self));
+      break;
+
     case PROP_PREFIX:
       g_value_set_string (value, ide_configuration_get_prefix (self));
       break;
@@ -463,6 +486,13 @@ ide_configuration_class_init (IdeConfigurationClass *klass)
                          NULL,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
+  properties [PROP_READY] =
+    g_param_spec_boolean ("ready",
+                          "Ready",
+                          "If the configuration can be used for building",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
   properties [PROP_RUNTIME] =
     g_param_spec_object ("runtime",
                          "Runtime",
@@ -997,12 +1027,6 @@ ide_configuration_snapshot (IdeConfiguration *self)
 
   copy->environment = ide_environment_copy (self->environment);
 
-  if (self->prebuild)
-    copy->prebuild = ide_build_command_queue_copy (self->prebuild);
-
-  if (self->postbuild)
-    copy->postbuild = ide_build_command_queue_copy (self->postbuild);
-
   g_hash_table_iter_init (&iter, self->internal);
   while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
     g_hash_table_insert (copy->internal, g_strdup (key), _value_copy (value));
@@ -1062,78 +1086,6 @@ ide_configuration_get_sequence (IdeConfiguration *self)
   return self->sequence;
 }
 
-/**
- * ide_configuration_get_prebuild:
- *
- * Gets a queue of commands to be run before the standard build process of
- * the configured build system. This can be useful for situations where the
- * user wants to setup some custom commands to prepare their environment.
- *
- * Constrast this with ide_configuration_get_postbuild() which gets commands
- * to be executed after the build system has completed.
- *
- * This function will always return a command queue. The command
- * queue may contain zero or more commands to be executed.
- *
- * Returns: (transfer full): An #IdeBuildCommandQueue.
- */
-IdeBuildCommandQueue *
-ide_configuration_get_prebuild (IdeConfiguration *self)
-{
-  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
-
-  if (self->prebuild != NULL)
-    return g_object_ref (self->prebuild);
-
-  return ide_build_command_queue_new ();
-}
-
-/**
- * ide_configuration_get_postbuild:
- *
- * Gets a queue of commands to be run after the standard build process of
- * the configured build system. This can be useful for situations where the
- * user wants to modify something after the build completes.
- *
- * Constrast this with ide_configuration_get_prebuild() which gets commands
- * to be executed before the build system has started.
- *
- * This function will always return a command queue. The command
- * queue may contain zero or more commands to be executed.
- *
- * Returns: (transfer full): An #IdeBuildCommandQueue.
- */
-IdeBuildCommandQueue *
-ide_configuration_get_postbuild (IdeConfiguration *self)
-{
-  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
-
-  if (self->postbuild != NULL)
-    return g_object_ref (self->postbuild);
-
-  return ide_build_command_queue_new ();
-}
-
-void
-_ide_configuration_set_prebuild (IdeConfiguration     *self,
-                                 IdeBuildCommandQueue *prebuild)
-{
-  g_assert (IDE_IS_CONFIGURATION (self));
-  g_assert (!prebuild || IDE_IS_BUILD_COMMAND_QUEUE (prebuild));
-
-  g_set_object (&self->prebuild, prebuild);
-}
-
-void
-_ide_configuration_set_postbuild (IdeConfiguration     *self,
-                                  IdeBuildCommandQueue *postbuild)
-{
-  g_assert (IDE_IS_CONFIGURATION (self));
-  g_assert (!postbuild || IDE_IS_BUILD_COMMAND_QUEUE (postbuild));
-
-  g_set_object (&self->postbuild, postbuild);
-}
-
 static GValue *
 ide_configuration_reset_internal_value (IdeConfiguration *self,
                                         const gchar      *key,
@@ -1192,6 +1144,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)
@@ -1338,3 +1321,21 @@ ide_configuration_set_internal_object (IdeConfiguration *self,
   v = ide_configuration_reset_internal_value (self, key, type);
   g_value_set_object (v, instance);
 }
+
+/**
+ * ide_configuration_get_ready:
+ * @self: An #IdeConfiguration
+ *
+ * Determines if the configuration is ready for use. That means that the
+ * build device can be accessed and the runtime is loaded. This may change
+ * at runtime as devices and runtimes are added or removed.
+ *
+ * Returns: %TRUE if the configuration is ready for use.
+ */
+gboolean
+ide_configuration_get_ready (IdeConfiguration *self)
+{
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+
+  return self->device_ready && self->runtime_ready;
+}
diff --git a/libide/buildsystem/ide-configuration.h b/libide/buildsystem/ide-configuration.h
index 96be371..38552b8 100644
--- a/libide/buildsystem/ide-configuration.h
+++ b/libide/buildsystem/ide-configuration.h
@@ -50,6 +50,7 @@ void                  ide_configuration_set_dirty            (IdeConfiguration
 const gchar          *ide_configuration_get_display_name     (IdeConfiguration  *self);
 void                  ide_configuration_set_display_name     (IdeConfiguration  *self,
                                                               const gchar       *display_name);
+gboolean              ide_configuration_get_ready            (IdeConfiguration  *self);
 IdeRuntime           *ide_configuration_get_runtime          (IdeConfiguration  *self);
 void                  ide_configuration_set_runtime          (IdeConfiguration  *self,
                                                               IdeRuntime        *runtime);
@@ -75,16 +76,19 @@ IdeEnvironment       *ide_configuration_get_environment      (IdeConfiguration
 IdeConfiguration     *ide_configuration_duplicate            (IdeConfiguration  *self);
 IdeConfiguration     *ide_configuration_snapshot             (IdeConfiguration  *self);
 guint                 ide_configuration_get_sequence         (IdeConfiguration  *self);
-IdeBuildCommandQueue *ide_configuration_get_prebuild         (IdeConfiguration  *self);
-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/directory/ide-directory-build-system.c b/libide/directory/ide-directory-build-system.c
index 7a25d9a..54bedaa 100644
--- a/libide/directory/ide-directory-build-system.c
+++ b/libide/directory/ide-directory-build-system.c
@@ -22,9 +22,6 @@
 
 #include "ide-context.h"
 
-#include "buildsystem/ide-configuration-manager.h"
-#include "buildsystem/ide-configuration.h"
-#include "buildsystem/ide-simple-builder.h"
 #include "directory/ide-directory-build-system.h"
 #include "projects/ide-project-file.h"
 #include "projects/ide-project-item.h"
@@ -176,28 +173,8 @@ ide_directory_build_system_get_priority (IdeBuildSystem *build_system)
   return 1000000;
 }
 
-static IdeBuilder *
-ide_directory_build_system_get_builder (IdeBuildSystem    *build_system,
-                                        IdeConfiguration  *configuration,
-                                        GError           **error)
-{
-  IdeDirectoryBuildSystem *self = (IdeDirectoryBuildSystem *)build_system;
-  IdeContext *context;
-
-  g_assert (IDE_IS_DIRECTORY_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_CONFIGURATION (configuration));
-
-  context = ide_object_get_context (IDE_OBJECT (build_system));
-
-  return g_object_new (IDE_TYPE_SIMPLE_BUILDER,
-                       "configuration", configuration,
-                       "context", context,
-                       NULL);
-}
-
 static void
 build_system_init (IdeBuildSystemInterface *iface)
 {
   iface->get_priority = ide_directory_build_system_get_priority;
-  iface->get_builder = ide_directory_build_system_get_builder;
 }
diff --git a/libide/ide-context.c b/libide/ide-context.c
index 693dea8..3b89c90 100644
--- a/libide/ide-context.c
+++ b/libide/ide-context.c
@@ -1463,6 +1463,27 @@ ide_context_init_diagnostics_manager (gpointer             source_object,
 }
 
 static void
+ide_context_init_build_manager (gpointer             source_object,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = NULL;
+  IdeContext *self = source_object;
+
+  g_assert (IDE_IS_CONTEXT (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (!g_initable_init (G_INITABLE (self->build_manager), cancellable, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
 ide_context_init_loaded (gpointer             source_object,
                          GCancellable        *cancellable,
                          GAsyncReadyCallback  callback,
@@ -1507,6 +1528,7 @@ ide_context_init_async (GAsyncInitable      *initable,
                         ide_context_init_search_engine,
                         ide_context_init_runtimes,
                         ide_context_init_configuration_manager,
+                        ide_context_init_build_manager,
                         ide_context_init_diagnostics_manager,
                         ide_context_init_loaded,
                         NULL);
diff --git a/libide/ide-enums.c.in b/libide/ide-enums.c.in
index 0f71244..5013750 100644
--- a/libide/ide-enums.c.in
+++ b/libide/ide-enums.c.in
@@ -5,7 +5,8 @@
 #include "ide-enums.h"
 
 #include "buffers/ide-buffer.h"
-#include "buildsystem/ide-build-result.h"
+#include "buildsystem/ide-build-log.h"
+#include "buildsystem/ide-build-pipeline.h"
 #include "devices/ide-device.h"
 #include "diagnostics/ide-diagnostic.h"
 #include "doap/ide-doap.h"
diff --git a/libide/ide-types.h b/libide/ide-types.h
index 80e9a62..93c889f 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;
 
@@ -134,6 +136,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 b8cc8f2..3908c54 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -35,19 +35,19 @@ G_BEGIN_DECLS
 #include "buffers/ide-buffer.h"
 #include "buffers/ide-unsaved-file.h"
 #include "buffers/ide-unsaved-files.h"
-#include "buildsystem/ide-build-command.h"
-#include "buildsystem/ide-build-command-queue.h"
 #include "buildsystem/ide-build-manager.h"
-#include "buildsystem/ide-build-result-addin.h"
-#include "buildsystem/ide-build-result.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-pipeline-addin.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"
 #include "buildsystem/ide-configuration-manager.h"
 #include "buildsystem/ide-configuration.h"
 #include "buildsystem/ide-environment-variable.h"
 #include "buildsystem/ide-environment.h"
-#include "buildsystem/ide-simple-builder.h"
 #include "devices/ide-device-manager.h"
 #include "devices/ide-device-provider.h"
 #include "devices/ide-device.h"
@@ -136,6 +136,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/local/ide-local-device.c b/libide/local/ide-local-device.c
index 99f468e..653a4ad 100644
--- a/libide/local/ide-local-device.c
+++ b/libide/local/ide-local-device.c
@@ -21,7 +21,8 @@
 #include <string.h>
 #include <sys/utsname.h>
 
-#include "ide-local-device.h"
+#include "local/ide-local-device.h"
+#include "util/ide-posix.h"
 
 typedef struct
 {
@@ -30,36 +31,6 @@ typedef struct
 
 G_DEFINE_TYPE_WITH_PRIVATE (IdeLocalDevice, ide_local_device, IDE_TYPE_DEVICE)
 
-static gchar *
-get_system_type (void)
-{
-  g_autofree gchar *os_lower = NULL;
-  const gchar *machine = NULL;
-  struct utsname u;
-
-  if (uname (&u) < 0)
-    return g_strdup ("unknown");
-
-  os_lower = g_utf8_strdown (u.sysname, -1);
-
-  /* config.sub doesn't accept amd64-OS */
-  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
-
-  /*
-   * TODO: Clearly we want to discover "gnu", but that should be just fine
-   *       for a default until we try to actually run on something non-gnu.
-   *       Which seems unlikely at the moment. If you run FreeBSD, you can
-   *       probably fix this for me :-) And while you're at it, make the
-   *       uname() call more portable.
-   */
-
-#ifdef __GLIBC__
-  return g_strdup_printf ("%s-%s-%s", machine, os_lower, "gnu");
-#else
-  return g_strdup_printf ("%s-%s", machine, os_lower);
-#endif
-}
-
 static const gchar *
 ide_local_device_get_system_type (IdeDevice *device)
 {
@@ -99,7 +70,7 @@ ide_local_device_init (IdeLocalDevice *self)
 {
   IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
 
-  priv->system_type = get_system_type ();
+  priv->system_type = g_strdup (ide_get_system_type ());
 
   ide_device_set_display_name (IDE_DEVICE (self), g_get_host_name ());
   ide_device_set_id (IDE_DEVICE (self), "local");
diff --git a/libide/runner/ide-run-manager.c b/libide/runner/ide-run-manager.c
index 1792f9b..c5d7ad4 100644
--- a/libide/runner/ide-run-manager.c
+++ b/libide/runner/ide-run-manager.c
@@ -382,26 +382,27 @@ ide_run_manager_install_cb (GObject      *object,
 {
   IdeBuildManager *build_manager = (IdeBuildManager *)object;
   g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
   IdeRunManager *self;
   IdeBuildTarget *build_target;
   GCancellable *cancellable;
-  GError *error = NULL;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
   g_assert (G_IS_TASK (task));
 
-  if (!ide_build_manager_build_finish (build_manager, result, &error))
+  self = g_task_get_source_object (task);
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  if (!ide_build_manager_execute_finish (build_manager, result, &error))
     {
-      g_task_return_error (task, error);
+      g_task_return_error (task, g_steal_pointer (&error));
       IDE_EXIT;
     }
 
-  self = g_task_get_source_object (task);
-  g_assert (IDE_IS_RUN_MANAGER (self));
-
   build_target = ide_run_manager_get_build_target (self);
+
   if (build_target == NULL)
     {
       cancellable = g_task_get_cancellable (task);
@@ -474,7 +475,8 @@ ide_run_manager_do_install_before_run (IdeRunManager *self,
                            self,
                            G_CONNECT_SWAPPED);
 
-  ide_build_manager_install_async (build_manager,
+  ide_build_manager_execute_async (build_manager,
+                                   IDE_BUILD_PHASE_INSTALL,
                                    g_task_get_cancellable (task),
                                    ide_run_manager_install_cb,
                                    g_object_ref (task));
diff --git a/libide/runtimes/ide-runtime.c b/libide/runtimes/ide-runtime.c
index 1476e0e..104040b 100644
--- a/libide/runtimes/ide-runtime.c
+++ b/libide/runtimes/ide-runtime.c
@@ -21,7 +21,6 @@
 #include "ide-context.h"
 #include "ide-debug.h"
 
-#include "buildsystem/ide-builder.h"
 #include "buildsystem/ide-configuration.h"
 #include "projects/ide-project.h"
 #include "runtimes/ide-runtime.h"
@@ -43,87 +42,6 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
-static void
-ide_runtime_real_prebuild_async (IdeRuntime          *self,
-                                 IdeBuildResult      *build_result,
-                                 GCancellable        *cancellable,
-                                 GAsyncReadyCallback  callback,
-                                 gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_return_boolean (task, TRUE);
-}
-
-static gboolean
-ide_runtime_real_prebuild_finish (IdeRuntime    *self,
-                                  GAsyncResult  *result,
-                                  GError       **error)
-{
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (G_IS_TASK (result));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-ide_runtime_real_postbuild_async (IdeRuntime          *self,
-                                  IdeBuildResult      *build_result,
-                                  GCancellable        *cancellable,
-                                  GAsyncReadyCallback  callback,
-                                  gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_return_boolean (task, TRUE);
-}
-
-static gboolean
-ide_runtime_real_postbuild_finish (IdeRuntime    *self,
-                                   GAsyncResult  *result,
-                                   GError       **error)
-{
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (G_IS_TASK (result));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-ide_runtime_real_postinstall_async (IdeRuntime          *self,
-                                    IdeBuildResult      *build_result,
-                                    GCancellable        *cancellable,
-                                    GAsyncReadyCallback  callback,
-                                    gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_return_boolean (task, TRUE);
-}
-
-static gboolean
-ide_runtime_real_postinstall_finish (IdeRuntime    *self,
-                                     GAsyncResult  *result,
-                                     GError       **error)
-{
-  g_assert (IDE_IS_RUNTIME (self));
-  g_assert (G_IS_TASK (result));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
 static IdeSubprocessLauncher *
 ide_runtime_real_create_launcher (IdeRuntime  *self,
                                   GError     **error)
@@ -337,12 +255,6 @@ ide_runtime_class_init (IdeRuntimeClass *klass)
   object_class->get_property = ide_runtime_get_property;
   object_class->set_property = ide_runtime_set_property;
 
-  klass->prebuild_async = ide_runtime_real_prebuild_async;
-  klass->prebuild_finish = ide_runtime_real_prebuild_finish;
-  klass->postbuild_async = ide_runtime_real_postbuild_async;
-  klass->postbuild_finish = ide_runtime_real_postbuild_finish;
-  klass->postinstall_async = ide_runtime_real_postinstall_async;
-  klass->postinstall_finish = ide_runtime_real_postinstall_finish;
   klass->create_launcher = ide_runtime_real_create_launcher;
   klass->create_runner = ide_runtime_real_create_runner;
   klass->contains_program_in_path = ide_runtime_real_contains_program_in_path;
@@ -440,83 +352,12 @@ ide_runtime_new (IdeContext  *context,
                        NULL);
 }
 
-void
-ide_runtime_prebuild_async (IdeRuntime          *self,
-                            IdeBuildResult      *build_result,
-                            GCancellable        *cancellable,
-                            GAsyncReadyCallback  callback,
-                            gpointer             user_data)
-{
-  g_return_if_fail (IDE_IS_RUNTIME (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  IDE_RUNTIME_GET_CLASS (self)->prebuild_async (self, build_result, cancellable, callback, user_data);
-}
-
-gboolean
-ide_runtime_prebuild_finish (IdeRuntime    *self,
-                             GAsyncResult  *result,
-                             GError       **error)
-{
-  g_return_val_if_fail (IDE_IS_RUNTIME (self), FALSE);
-
-  return IDE_RUNTIME_GET_CLASS (self)->prebuild_finish (self, result, error);
-}
-
-void
-ide_runtime_postbuild_async (IdeRuntime          *self,
-                             IdeBuildResult      *build_result,
-                             GCancellable        *cancellable,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  g_return_if_fail (IDE_IS_RUNTIME (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  IDE_RUNTIME_GET_CLASS (self)->postbuild_async (self, build_result, cancellable, callback, user_data);
-}
-
-gboolean
-ide_runtime_postbuild_finish (IdeRuntime    *self,
-                              GAsyncResult  *result,
-                              GError       **error)
-{
-  g_return_val_if_fail (IDE_IS_RUNTIME (self), FALSE);
-
-  return IDE_RUNTIME_GET_CLASS (self)->postbuild_finish (self, result, error);
-}
-
-void
-ide_runtime_postinstall_async (IdeRuntime          *self,
-                               IdeBuildResult      *build_result,
-                               GCancellable        *cancellable,
-                               GAsyncReadyCallback  callback,
-                               gpointer             user_data)
-{
-  g_return_if_fail (IDE_IS_RUNTIME (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  IDE_RUNTIME_GET_CLASS (self)->postinstall_async (self, build_result, cancellable, callback, user_data);
-}
-
-gboolean
-ide_runtime_postinstall_finish (IdeRuntime    *self,
-                                GAsyncResult  *result,
-                                GError       **error)
-{
-  g_return_val_if_fail (IDE_IS_RUNTIME (self), FALSE);
-
-  return IDE_RUNTIME_GET_CLASS (self)->postinstall_finish (self, result, error);
-}
-
 /**
  * ide_runtime_create_launcher:
  *
  * Creates a launcher for the runtime.
  *
  * This can be used to execute a command within a runtime.
- * If you are doing a build, you probably want to ensure you call
- * ide_runtime_prebuild_async() before using the launcher.
  *
  * It is important that this function can be run from a thread without
  * side effects.
diff --git a/libide/runtimes/ide-runtime.h b/libide/runtimes/ide-runtime.h
index 533a7ee..9bcadae 100644
--- a/libide/runtimes/ide-runtime.h
+++ b/libide/runtimes/ide-runtime.h
@@ -43,22 +43,6 @@ struct _IdeRuntimeClass
 {
   IdeObjectClass parent;
 
-  void                   (*prebuild_async)           (IdeRuntime           *self,
-                                                      IdeBuildResult       *build_result,
-                                                      GCancellable         *cancellable,
-                                                      GAsyncReadyCallback   callback,
-                                                      gpointer              user_data);
-  gboolean               (*prebuild_finish)          (IdeRuntime           *self,
-                                                      GAsyncResult         *result,
-                                                      GError              **error);
-  void                   (*postbuild_async)          (IdeRuntime           *self,
-                                                      IdeBuildResult       *build_result,
-                                                      GCancellable         *cancellable,
-                                                      GAsyncReadyCallback   callback,
-                                                      gpointer              user_data);
-  gboolean               (*postbuild_finish)         (IdeRuntime           *self,
-                                                      GAsyncResult         *result,
-                                                      GError              **error);
   gboolean               (*contains_program_in_path) (IdeRuntime           *self,
                                                       const gchar          *program,
                                                       GCancellable         *cancellable);
@@ -68,14 +52,6 @@ struct _IdeRuntimeClass
                                                       IdeConfiguration     *configuration);
   IdeRunner             *(*create_runner)            (IdeRuntime           *self,
                                                       IdeBuildTarget       *build_target);
-  void                   (*postinstall_async)        (IdeRuntime           *self,
-                                                      IdeBuildResult       *build_result,
-                                                      GCancellable         *cancellable,
-                                                      GAsyncReadyCallback   callback,
-                                                      gpointer              user_data);
-  gboolean               (*postinstall_finish)       (IdeRuntime           *self,
-                                                      GAsyncResult         *result,
-                                                      GError              **error);
   GFile                 *(*translate_file)           (IdeRuntime           *self,
                                                       GFile                *file);
 
@@ -95,30 +71,6 @@ struct _IdeRuntimeClass
 };
 
 GQuark                 ide_runtime_error_quark              (void) G_GNUC_CONST;
-void                   ide_runtime_prebuild_async           (IdeRuntime           *self,
-                                                             IdeBuildResult       *build_result,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-gboolean               ide_runtime_prebuild_finish          (IdeRuntime           *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
-void                   ide_runtime_postbuild_async          (IdeRuntime           *self,
-                                                             IdeBuildResult       *build_result,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-gboolean               ide_runtime_postbuild_finish         (IdeRuntime           *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
-void                   ide_runtime_postinstall_async        (IdeRuntime           *self,
-                                                             IdeBuildResult       *build_result,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-gboolean               ide_runtime_postinstall_finish       (IdeRuntime           *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
 gboolean               ide_runtime_contains_program_in_path (IdeRuntime           *self,
                                                              const gchar          *program,
                                                              GCancellable         *cancellable);
diff --git a/libide/subprocess/ide-subprocess-launcher.c b/libide/subprocess/ide-subprocess-launcher.c
index 6e4a0e9..1e830ff 100644
--- a/libide/subprocess/ide-subprocess-launcher.c
+++ b/libide/subprocess/ide-subprocess-launcher.c
@@ -231,16 +231,16 @@ ide_subprocess_launcher_spawn_worker (GTask        *task,
 
   g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
 
-#ifdef IDE_ENABLE_TRACE
   {
     g_autofree gchar *str = NULL;
     g_autofree gchar *env = NULL;
+
     str = g_strjoinv (" ", (gchar **)priv->argv->pdata);
     env = priv->environ ? g_strjoinv (" ", priv->environ) : g_strdup ("");
-    IDE_TRACE_MSG ("Launching '%s' from directory %s with environment %s %s parent environment",
-                   str, priv->cwd, env, priv->clear_env ? "clearing" : "inheriting");
+
+    g_debug ("Launching '%s' from directory '%s' with environment %s %s parent environment",
+             str, priv->cwd, env, priv->clear_env ? "clearing" : "inheriting");
   }
-#endif
 
   launcher = g_subprocess_launcher_new (priv->flags);
   g_subprocess_launcher_set_child_setup (launcher, child_setup_func, NULL, NULL);
@@ -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/subprocess/ide-subprocess.c b/libide/subprocess/ide-subprocess.c
index a981f08..5a90dc4 100644
--- a/libide/subprocess/ide-subprocess.c
+++ b/libide/subprocess/ide-subprocess.c
@@ -146,6 +146,18 @@ ide_subprocess_wait_check_cb (GObject      *object,
       IDE_EXIT;
     }
 
+  if (ide_subprocess_get_if_signaled (self))
+    {
+      gint term_sig = ide_subprocess_get_term_sig (self);
+
+      g_task_return_new_error (task,
+                               G_SPAWN_ERROR,
+                               G_SPAWN_ERROR_FAILED,
+                               "Child process killed by signal %d",
+                               term_sig);
+      IDE_EXIT;
+    }
+
   if (!ide_subprocess_check_exit_status (self, &error))
     {
       g_task_return_error (task, g_steal_pointer (&error));
diff --git a/libide/transfers/ide-transfer-manager.c b/libide/transfers/ide-transfer-manager.c
index 26d5b46..e818321 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,
+                         0);
+
+  g_signal_connect_data (self,
+                         "transfer-failed",
+                         G_CALLBACK (ide_transfer_manager_execute_transfer_failed),
+                         g_object_ref (task),
+                         (GClosureNotify)g_object_unref,
+                         0);
+
+  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..ec2c7da
--- /dev/null
+++ b/libide/util/ide-directory-reaper.c
@@ -0,0 +1,415 @@
+/* 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-debug.h"
+
+#include "util/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));
+
+  if (glob == NULL)
+    glob = "*";
+
+  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 gboolean
+remove_directory_with_children (GFile         *file,
+                                GCancellable  *cancellable,
+                                GError       **error)
+{
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  gpointer infoptr;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (file);
+    IDE_TRACE_MSG ("Removing directory recursively \"%s\"", uri);
+  }
+#endif
+
+  enumerator = g_file_enumerate_children (file,
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE","
+                                          G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                          cancellable,
+                                          error);
+
+  if (enumerator == NULL)
+    return FALSE;
+
+  while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
+    {
+      g_autoptr(GFileInfo) info = infoptr;
+      const gchar *name = g_file_info_get_name (info);
+      GFile *child = g_file_get_child (file, name);
+
+      if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+        {
+          if (!remove_directory_with_children (child, cancellable, error))
+            return FALSE;
+          continue;
+        }
+
+      if (!g_file_delete (child, cancellable, error))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+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_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable) == 
G_FILE_TYPE_DIRECTORY)
+                    {
+                      if (!remove_directory_with_children (file, cancellable, &error))
+                        {
+                          g_warning ("%s", error->message);
+                          g_clear_error (&error);
+                        }
+                    }
+                  else 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/libide/util/ide-glib.c b/libide/util/ide-glib.c
index 0bfe8a3..9e3b32f 100644
--- a/libide/util/ide-glib.c
+++ b/libide/util/ide-glib.c
@@ -181,3 +181,47 @@ ide_gettext (const gchar *message)
     return g_dgettext (GETTEXT_PACKAGE, message);
   return NULL;
 }
+
+gchar *
+ide_g_time_span_to_label (GTimeSpan span)
+{
+  gint64 hours;
+  gint64 minutes;
+  gint64 seconds;
+
+  span = ABS (span);
+
+  hours = span / G_TIME_SPAN_HOUR;
+  minutes = (span % G_TIME_SPAN_HOUR) / G_TIME_SPAN_MINUTE;
+  seconds = (span % G_TIME_SPAN_MINUTE) / G_TIME_SPAN_SECOND;
+
+  g_assert (minutes < 60);
+  g_assert (seconds < 60);
+
+  if (hours == 0)
+    return g_strdup_printf ("%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT,
+                            minutes, seconds);
+  else
+    return g_strdup_printf ("%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT":%02"G_GINT64_FORMAT,
+                            hours, minutes, seconds);
+}
+
+gboolean
+ide_g_time_span_to_label_mapping (GBinding     *binding,
+                                  const GValue *from_value,
+                                  GValue       *to_value,
+                                  gpointer      user_data)
+{
+  GTimeSpan span;
+
+  g_assert (G_IS_BINDING (binding));
+  g_assert (from_value != NULL);
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_INT64));
+  g_assert (to_value != NULL);
+  g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
+
+  span = g_value_get_int64 (from_value);
+  g_value_take_string (to_value, ide_g_time_span_to_label (span));
+
+  return TRUE;
+}
diff --git a/libide/util/ide-glib.h b/libide/util/ide-glib.h
index 1895dd6..437f805 100644
--- a/libide/util/ide-glib.h
+++ b/libide/util/ide-glib.h
@@ -33,6 +33,11 @@ void         ide_g_task_return_pointer_from_main (GTask          *task,
                                                   GDestroyNotify  notify);
 void         ide_g_task_return_error_from_main   (GTask          *task,
                                                   GError         *error);
+gchar       *ide_g_time_span_to_label            (GTimeSpan       span);
+gboolean     ide_g_time_span_to_label_mapping    (GBinding       *binding,
+                                                  const GValue   *from_value,
+                                                  GValue         *to_value,
+                                                  gpointer        user_data);
 
 G_END_DECLS
 
diff --git a/libide/util/ide-posix.c b/libide/util/ide-posix.c
index 5605001..3bbb495 100644
--- a/libide/util/ide-posix.c
+++ b/libide/util/ide-posix.c
@@ -25,6 +25,42 @@
 
 #include "ide-posix.h"
 
+const gchar *
+ide_get_system_type (void)
+{
+  static gchar *system_type;
+  g_autofree gchar *os_lower = NULL;
+  const gchar *machine = NULL;
+  struct utsname u;
+
+  if (system_type != NULL)
+    return system_type;
+
+  if (uname (&u) < 0)
+    return g_strdup ("unknown");
+
+  os_lower = g_utf8_strdown (u.sysname, -1);
+
+  /* config.sub doesn't accept amd64-OS */
+  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+  /*
+   * TODO: Clearly we want to discover "gnu", but that should be just fine
+   *       for a default until we try to actually run on something non-gnu.
+   *       Which seems unlikely at the moment. If you run FreeBSD, you can
+   *       probably fix this for me :-) And while you're at it, make the
+   *       uname() call more portable.
+   */
+
+#ifdef __GLIBC__
+  system_type = g_strdup_printf ("%s-%s-%s", machine, os_lower, "gnu");
+#else
+  system_type = g_strdup_printf ("%s-%s", machine, os_lower);
+#endif
+
+  return system_type;
+}
+
 gchar *
 ide_get_system_arch (void)
 {
diff --git a/libide/util/ide-posix.h b/libide/util/ide-posix.h
index b0bb05f..328f544 100644
--- a/libide/util/ide-posix.h
+++ b/libide/util/ide-posix.h
@@ -23,10 +23,11 @@
 
 G_BEGIN_DECLS
 
-gchar *ide_get_system_arch      (void);
-gsize  ide_get_system_page_size (void) G_GNUC_CONST;
-gchar *ide_path_collapse        (const gchar *path);
-gchar *ide_path_expand          (const gchar *path);
+gchar       *ide_get_system_arch      (void);
+const gchar *ide_get_system_type      (void);
+gsize        ide_get_system_page_size (void) G_GNUC_CONST;
+gchar       *ide_path_collapse        (const gchar *path);
+gchar       *ide_path_expand          (const gchar *path);
 
 G_END_DECLS
 
diff --git a/libide/workbench/ide-omni-bar.c b/libide/workbench/ide-omni-bar.c
index ef03eaa..2b1d3dc 100644
--- a/libide/workbench/ide-omni-bar.c
+++ b/libide/workbench/ide-omni-bar.c
@@ -26,10 +26,11 @@
 #include "ide-debug.h"
 
 #include "buildsystem/ide-build-manager.h"
-#include "buildsystem/ide-build-result.h"
+#include "buildsystem/ide-build-pipeline.h"
 #include "buildsystem/ide-configuration.h"
 #include "buildsystem/ide-configuration-manager.h"
 #include "projects/ide-project.h"
+#include "util/ide-glib.h"
 #include "util/ide-gtk.h"
 #include "vcs/ide-vcs.h"
 #include "workbench/ide-omni-bar.h"
@@ -88,11 +89,6 @@ struct _IdeOmniBar
   EggBindingGroup *vcs_bindings;
 
   /*
-   * This is the IdeBuildResult for the last requested built.
-   */
-  IdeBuildResult *build_result;
-
-  /*
    * This tracks the number of times we have shown the current build
    * message while looping between the various messages. After our
    * SETTLE_MESSAGE_COUNT has been reached, we stop flapping between
@@ -101,6 +97,12 @@ struct _IdeOmniBar
   guint seen_count;
 
   /*
+   * Just tracks if we have already done a build so we can change
+   * how we display user messages.
+   */
+  guint did_build : 1;
+
+  /*
    * The following are template children from the GtkBuilder template.
    */
   GtkLabel       *branch_label;
@@ -151,32 +153,20 @@ date_time_to_label (GBinding     *binding,
 }
 
 static gboolean
-time_span_to_label (GBinding     *binding,
-                    const GValue *from_value,
-                    GValue       *to_value,
-                    gpointer      user_data)
+message_to_label (GBinding     *binding,
+                  const GValue *from_value,
+                  GValue       *to_value,
+                  gpointer      user_data)
 {
-  GTimeSpan span;
-
   g_assert (G_IS_BINDING (binding));
   g_assert (from_value != NULL);
-  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_INT64));
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_STRING));
   g_assert (to_value != NULL);
   g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
 
-  if (0 != (span = g_value_get_int64 (from_value)))
-    {
-      guint hours;
-      guint minutes;
-      guint seconds;
-
-      hours = span / G_TIME_SPAN_HOUR;
-      minutes = (span % G_TIME_SPAN_HOUR) / G_TIME_SPAN_MINUTE;
-      seconds = (span % G_TIME_SPAN_MINUTE) / G_TIME_SPAN_SECOND;
-
-      g_value_take_string (to_value,
-                           g_strdup_printf ("%02u:%02u:%02u", hours, minutes, seconds));
-    }
+  g_value_take_string (to_value,
+                       /* translators: this message is shown in the header bar to indicate build status */
+                       g_strdup_printf (_("Build: %s"), g_value_get_string (from_value)));
 
   return TRUE;
 }
@@ -455,10 +445,17 @@ event_box_leave_notify (IdeOmniBar  *self,
 static void
 ide_omni_bar_next_message (IdeOmniBar *self)
 {
+  IdeBuildManager *build_manager;
   const gchar *name;
+  IdeContext *context;
 
   g_assert (IDE_IS_OMNI_BAR (self));
 
+  if (NULL == (context = ide_widget_get_context (GTK_WIDGET (self))))
+    return;
+
+  build_manager = ide_context_get_build_manager (context);
+
   name = gtk_stack_get_visible_child_name (self->message_stack);
 
   /*
@@ -473,10 +470,10 @@ ide_omni_bar_next_message (IdeOmniBar *self)
       /* Only rotate to build result if we have one and we haven't
        * flapped too many times.
        */
-      if (self->build_result != NULL && self->seen_count < 2)
+      if (self->did_build && self->seen_count < 2)
         gtk_stack_set_visible_child_name (self->message_stack, "build");
     }
-  else if (!ide_build_result_get_running (self->build_result))
+  else if (!ide_build_manager_get_busy (build_manager))
     {
       self->seen_count++;
       gtk_stack_set_visible_child_name (self->message_stack, "config");
@@ -551,33 +548,32 @@ ide_omni_bar_popover_closed (IdeOmniBar *self,
 }
 
 static void
-ide_omni_bar__build_manager__build_started (IdeOmniBar      *self,
-                                            IdeBuildResult  *build_result,
-                                            IdeBuildManager *build_manager)
+ide_omni_bar__build_manager__build_started (IdeOmniBar       *self,
+                                            IdeBuildPipeline *build_pipeline,
+                                            IdeBuildManager  *build_manager)
 {
   g_assert (IDE_IS_OMNI_BAR (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
+  self->did_build = TRUE;
+  self->seen_count = 0;
+
   gtk_widget_hide (GTK_WIDGET (self->popover_failed_label));
   gtk_widget_show (GTK_WIDGET (self->popover_build_cancel_button));
 
-  g_set_object (&self->build_result, build_result);
-
-  self->seen_count = 0;
-
   gtk_stack_set_visible_child_name (self->popover_time_stack, "current-build");
 
   gtk_revealer_set_reveal_child (self->popover_details_revealer, TRUE);
 }
 
 static void
-ide_omni_bar__build_manager__build_failed (IdeOmniBar      *self,
-                                           IdeBuildResult  *build_result,
-                                           IdeBuildManager *build_manager)
+ide_omni_bar__build_manager__build_failed (IdeOmniBar       *self,
+                                           IdeBuildPipeline *build_pipeline,
+                                           IdeBuildManager  *build_manager)
 {
   g_assert (IDE_IS_OMNI_BAR (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   gtk_widget_set_visible (GTK_WIDGET (self->popover_failed_label), TRUE);
@@ -588,12 +584,12 @@ ide_omni_bar__build_manager__build_failed (IdeOmniBar      *self,
 }
 
 static void
-ide_omni_bar__build_manager__build_finished (IdeOmniBar      *self,
-                                             IdeBuildResult  *build_result,
-                                             IdeBuildManager *build_manager)
+ide_omni_bar__build_manager__build_finished (IdeOmniBar       *self,
+                                             IdeBuildPipeline *build_pipeline,
+                                             IdeBuildManager  *build_manager)
 {
   g_assert (IDE_IS_OMNI_BAR (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
+  g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   gtk_widget_hide (GTK_WIDGET (self->popover_build_cancel_button));
@@ -606,7 +602,6 @@ ide_omni_bar_finalize (GObject *object)
 {
   IdeOmniBar *self = (IdeOmniBar *)object;
 
-  g_clear_object (&self->build_result);
   g_clear_object (&self->build_manager_bindings);
   g_clear_object (&self->build_manager_signals);
   g_clear_object (&self->config_manager_bindings);
@@ -707,11 +702,13 @@ ide_omni_bar_init (IdeOmniBar *self)
                                NULL,
                                NULL);
 
-  egg_binding_group_bind (self->build_manager_bindings,
-                          "message",
-                          self->build_result_mode_label,
-                          "label",
-                          G_BINDING_SYNC_CREATE);
+  egg_binding_group_bind_full (self->build_manager_bindings,
+                               "message",
+                               self->build_result_mode_label,
+                               "label",
+                               G_BINDING_SYNC_CREATE,
+                               message_to_label,
+                               NULL, NULL, NULL);
 
   egg_binding_group_bind (self->build_manager_bindings,
                           "message",
@@ -724,7 +721,7 @@ ide_omni_bar_init (IdeOmniBar *self)
                                self->popover_build_running_time_label,
                                "label",
                                G_BINDING_SYNC_CREATE,
-                               time_span_to_label,
+                               ide_g_time_span_to_label_mapping,
                                NULL,
                                NULL,
                                NULL);
diff --git a/libide/workbench/ide-omni-bar.ui b/libide/workbench/ide-omni-bar.ui
index 8f36e33..351087e 100644
--- a/libide/workbench/ide-omni-bar.ui
+++ b/libide/workbench/ide-omni-bar.ui
@@ -144,7 +144,7 @@
   <object class="GtkPopover" id="popover">
     <property name="relative-to">event_box</property>
     <property name="position">bottom</property>
-    <property name="width-request">475</property>
+    <property name="width-request">525</property>
     <style>
       <class name="messagepopover"/>
     </style>
diff --git a/org.gnome.Builder.json b/org.gnome.Builder.json
index fd348f0..0f17f9e 100644
--- a/org.gnome.Builder.json
+++ b/org.gnome.Builder.json
@@ -7,6 +7,8 @@
     "tags": ["devel"],
     "desktop-file-name-prefix": "(Development) ",
     "finish-args": [
+        /* Require recent flatpak at runtime */
+        "--require-version=0.8.0",
         /* Allow access to developer tools */
         "--allow=devel",
         "--talk-name=org.freedesktop.Flatpak",
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 00f8e35..af84933 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -9,7 +9,6 @@ SUBDIRS = \
        color-picker \
        command-bar \
        comment-code \
-       contributing \
        create-project \
        ctags \
        devhelp \
diff --git a/plugins/autotools/Makefile.am b/plugins/autotools/Makefile.am
index 09e4ebb..b9af5a5 100644
--- a/plugins/autotools/Makefile.am
+++ b/plugins/autotools/Makefile.am
@@ -8,14 +8,16 @@ libautotools_plugin_la_SOURCES = \
        autotools-plugin.c \
        ide-autotools-application-addin.c \
        ide-autotools-application-addin.h \
-       ide-autotools-builder.c \
-       ide-autotools-builder.h \
+       ide-autotools-autogen-stage.c \
+       ide-autotools-autogen-stage.h \
        ide-autotools-build-system.c \
        ide-autotools-build-system.h \
        ide-autotools-build-target.c \
        ide-autotools-build-target.h \
-       ide-autotools-build-task.c \
-       ide-autotools-build-task.h \
+       ide-autotools-makecache-stage.c \
+       ide-autotools-makecache-stage.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/autotools/ide-autotools-builder.h b/plugins/autotools/ide-autotools-autogen-stage.h
similarity index 57%
rename from plugins/autotools/ide-autotools-builder.h
rename to plugins/autotools/ide-autotools-autogen-stage.h
index 28be301..5dd2c6c 100644
--- a/plugins/autotools/ide-autotools-builder.h
+++ b/plugins/autotools/ide-autotools-autogen-stage.h
@@ -1,6 +1,6 @@
-/* ide-autotools-builder.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,20 +16,17 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef IDE_AUTOTOOLS_BUILDER_H
-#define IDE_AUTOTOOLS_BUILDER_H
+#ifndef IDE_AUTOTOOLS_AUTOGEN_STAGE_H
+#define IDE_AUTOTOOLS_AUTOGEN_STAGE_H
 
 #include <ide.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_AUTOTOOLS_BUILDER (ide_autotools_builder_get_type())
+#define IDE_TYPE_AUTOTOOLS_AUTOGEN_STAGE (ide_autotools_autogen_stage_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeAutotoolsBuilder, ide_autotools_builder, IDE, AUTOTOOLS_BUILDER, IdeBuilder)
-
-GFile    *ide_autotools_builder_get_build_directory (IdeAutotoolsBuilder  *self);
-gboolean  ide_autotools_builder_get_needs_bootstrap (IdeAutotoolsBuilder  *self);
+G_DECLARE_FINAL_TYPE (IdeAutotoolsAutogenStage, ide_autotools_autogen_stage, IDE, AUTOTOOLS_AUTOGEN_STAGE, 
IdeBuildStage)
 
 G_END_DECLS
 
-#endif /* IDE_AUTOTOOLS_BUILDER_H */
+#endif /* IDE_AUTOTOOLS_AUTOGEN_STAGE_H */
diff --git a/plugins/autotools/ide-autotools-build-system.c b/plugins/autotools/ide-autotools-build-system.c
index 2b6e40e..159f225 100644
--- a/plugins/autotools/ide-autotools-build-system.c
+++ b/plugins/autotools/ide-autotools-build-system.c
@@ -25,7 +25,7 @@
 #include <ide.h>
 
 #include "ide-autotools-build-system.h"
-#include "ide-autotools-builder.h"
+#include "ide-autotools-makecache-stage.h"
 #include "ide-makecache.h"
 
 struct _IdeAutotoolsBuildSystem
@@ -64,30 +64,6 @@ ide_autotools_build_system_get_tarball_name (IdeAutotoolsBuildSystem *self)
   return self->tarball_name;
 }
 
-static IdeBuilder *
-ide_autotools_build_system_get_builder (IdeBuildSystem    *build_system,
-                                        IdeConfiguration  *configuration,
-                                        GError           **error)
-{
-  IdeBuilder *ret;
-  IdeContext *context;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system));
-  g_assert (IDE_IS_CONFIGURATION (configuration));
-
-  context = ide_object_get_context (IDE_OBJECT (build_system));
-  g_assert (IDE_IS_CONTEXT (context));
-
-  ret = g_object_new (IDE_TYPE_AUTOTOOLS_BUILDER,
-                      "context", context,
-                      "configuration", configuration,
-                      NULL);
-
-  IDE_RETURN (ret);
-}
-
 static gboolean
 is_configure (GFile *file)
 {
@@ -323,6 +299,333 @@ ide_autotools_build_system_get_priority (IdeBuildSystem *system)
 }
 
 static void
+find_makecache_stage (gpointer data,
+                      gpointer user_data)
+{
+  IdeMakecache **makecache = user_data;
+  IdeBuildStage *stage = data;
+
+  if (*makecache != NULL)
+    return;
+
+  if (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (stage))
+    *makecache = ide_autotools_makecache_stage_get_makecache (IDE_AUTOTOOLS_MAKECACHE_STAGE (stage));
+}
+
+static void
+ide_autotools_build_system_get_file_flags_cb (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeMakecache *makecache = (IdeMakecache *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) flags = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAKECACHE (makecache));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  flags = ide_makecache_get_file_flags_finish (makecache, result, &error);
+
+  if (flags == NULL)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_steal_pointer (&flags), (GDestroyNotify)g_strfreev);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_build_system_get_build_flags_execute_cb (GObject      *object,
+                                                       GAsyncResult *result,
+                                                       gpointer      user_data)
+{
+  IdeBuildManager *build_manager = (IdeBuildManager *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeMakecache *makecache = NULL;
+  IdeBuildPipeline *pipeline;
+  GCancellable *cancellable;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  file = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
+
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_build_manager_execute_finish (build_manager, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  /*
+   * Locate our makecache by finding the makecache stage (which should have
+   * successfully executed by now) and get makecache object. Then we can
+   * locate the build flags for the file (which makecache will translate
+   * into the appropriate build target).
+   */
+
+  ide_build_pipeline_foreach_stage (pipeline, find_makecache_stage, &makecache);
+
+  if (makecache != NULL)
+    {
+      ide_makecache_get_file_flags_async (makecache,
+                                          file,
+                                          cancellable,
+                                          ide_autotools_build_system_get_file_flags_cb,
+                                          g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  /*
+   * We failed to locate anything, so just return an empty array of
+   * of flags.
+   */
+
+  g_task_return_pointer (task, g_new0 (gchar *, 1), g_free);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_build_system_get_build_flags_async (IdeBuildSystem      *build_system,
+                                                  IdeFile             *file,
+                                                  GCancellable        *cancellable,
+                                                  GAsyncReadyCallback  callback,
+                                                  gpointer             user_data)
+{
+  IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)build_system;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
+  g_assert (IDE_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_autotools_build_system_get_build_flags_async);
+  g_task_set_task_data (task, g_object_ref (ide_file_get_file (file)), g_object_unref);
+
+  /*
+   * To get the build flags for the file, we first need to get the makecache
+   * for the current build pipeline. That requires advancing the pipeline to
+   * at least the CONFIGURE stage so that our CONFIGURE|AFTER step has executed
+   * to generate the Makecache file in $builddir. With that, we can load a new
+   * IdeMakecache (if necessary) and scan the file for build flags.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_context_get_build_manager (context);
+
+  ide_build_manager_execute_async (build_manager,
+                                   IDE_BUILD_PHASE_CONFIGURE,
+                                   cancellable,
+                                   ide_autotools_build_system_get_build_flags_execute_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gchar **
+ide_autotools_build_system_get_build_flags_finish (IdeBuildSystem  *build_system,
+                                                   GAsyncResult    *result,
+                                                   GError         **error)
+{
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_autotools_build_system_get_build_targets_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeMakecache *makecache = (IdeMakecache *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GPtrArray) ret = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAKECACHE (makecache));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  ret = ide_makecache_get_build_targets_finish (makecache, result, &error);
+
+  if (ret == NULL)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify)g_ptr_array_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_build_system_get_build_targets_execute_cb (GObject      *object,
+                                                         GAsyncResult *result,
+                                                         gpointer      user_data)
+{
+  IdeBuildManager *build_manager = (IdeBuildManager *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) builddir_file = NULL;
+  IdeMakecache *makecache = NULL;
+  IdeBuildPipeline *pipeline;
+  GCancellable *cancellable;
+  const gchar *builddir;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  cancellable = g_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_build_manager_execute_finish (build_manager, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  builddir = ide_build_pipeline_get_builddir (pipeline);
+  builddir_file = g_file_new_for_path (builddir);
+
+  /*
+   * Locate our makecache by finding the makecache stage (which should have
+   * successfully executed by now) and get makecache object. Then we can
+   * locate the build flags for the file (which makecache will translate
+   * into the appropriate build target).
+   */
+
+  ide_build_pipeline_foreach_stage (pipeline, find_makecache_stage, &makecache);
+
+  if (makecache != NULL)
+    {
+      ide_makecache_get_build_targets_async (makecache,
+                                             builddir_file,
+                                             cancellable,
+                                             ide_autotools_build_system_get_build_targets_cb,
+                                             g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  /*
+   * We failed to locate anything, so just return an empty array of
+   * of flags.
+   */
+
+  g_task_return_pointer (task, g_ptr_array_new (), (GDestroyNotify)g_ptr_array_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_build_system_get_build_targets_async (IdeBuildSystem      *build_system,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data)
+{
+  IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)build_system;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (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_build_system_get_build_targets_async);
+
+  /*
+   * To get the build targets we first need to get the makecache for the
+   * current build pipeline. That requires advancing the pipeline to at least
+   * the CONFIGURE stage so that our CONFIGURE|AFTER step has executed to
+   * generate the Makecache file in $builddir.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_context_get_build_manager (context);
+
+  ide_build_manager_execute_async (build_manager,
+                                   IDE_BUILD_PHASE_CONFIGURE,
+                                   cancellable,
+                                   ide_autotools_build_system_get_build_targets_execute_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static GPtrArray *
+ide_autotools_build_system_get_build_targets_finish (IdeBuildSystem  *build_system,
+                                                     GAsyncResult    *result,
+                                                     GError         **error)
+{
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static gchar *
+ide_autotools_build_system_get_builddir (IdeBuildSystem   *build_system,
+                                         IdeConfiguration *configuration)
+{
+  IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)build_system;
+  g_autoptr(GFile) makefile = NULL;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  /*
+   * If there is a Makefile in the build directory, then the project has been
+   * configured in-tree, and we must override the builddir to perform in-tree
+   * builds.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  vcs = ide_context_get_vcs (context);
+  workdir = ide_vcs_get_working_directory (vcs);
+
+  if (!g_file_is_native (workdir))
+    return NULL;
+
+  makefile = g_file_get_child (workdir, "Makefile");
+
+  if (g_file_query_exists (makefile, NULL))
+    return g_file_get_path (workdir);
+
+  return NULL;
+}
+
+static void
 ide_autotools_build_system_finalize (GObject *object)
 {
   IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)object;
@@ -381,7 +684,11 @@ static void
 build_system_iface_init (IdeBuildSystemInterface *iface)
 {
   iface->get_priority = ide_autotools_build_system_get_priority;
-  iface->get_builder = ide_autotools_build_system_get_builder;
+  iface->get_build_flags_async = ide_autotools_build_system_get_build_flags_async;
+  iface->get_build_flags_finish = ide_autotools_build_system_get_build_flags_finish;
+  iface->get_builddir = ide_autotools_build_system_get_builddir;
+  iface->get_build_targets_async = ide_autotools_build_system_get_build_targets_async;
+  iface->get_build_targets_finish = ide_autotools_build_system_get_build_targets_finish;
 }
 
 static void
diff --git a/plugins/autotools/ide-autotools-makecache-stage.c 
b/plugins/autotools/ide-autotools-makecache-stage.c
new file mode 100644
index 0000000..7e7fe99
--- /dev/null
+++ b/plugins/autotools/ide-autotools-makecache-stage.c
@@ -0,0 +1,253 @@
+/* ide-autotools-makecache-stage.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-autotools-makecache-stage"
+
+#include <glib/gi18n.h>
+
+#include "ide-autotools-makecache-stage.h"
+
+struct _IdeAutotoolsMakecacheStage
+{
+  IdeBuildStageLauncher  parent_instance;
+
+  IdeMakecache          *makecache;
+  IdeRuntime            *runtime;
+  GFile                 *cache_file;
+};
+
+G_DEFINE_TYPE (IdeAutotoolsMakecacheStage, ide_autotools_makecache_stage, IDE_TYPE_BUILD_STAGE_LAUNCHER)
+
+static void
+ide_autotools_makecache_stage_makecache_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(IdeMakecache) makecache = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeAutotoolsMakecacheStage *self;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  makecache = ide_makecache_new_for_cache_file_finish (result, &error);
+
+  if (makecache == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  self = g_task_get_source_object (task);
+  g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (self));
+
+  g_clear_object (&self->makecache);
+  self->makecache = g_steal_pointer (&makecache);
+
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_makecache_stage_execute_cb (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  IdeAutotoolsMakecacheStage *self = (IdeAutotoolsMakecacheStage *)object;
+  IdeBuildStage *stage = (IdeBuildStage *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (stage));
+  g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!IDE_BUILD_STAGE_CLASS (ide_autotools_makecache_stage_parent_class)->execute_finish (stage, result, 
&error))
+    {
+      g_warning ("%s", error->message);
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  cancellable = g_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * Now that we have our makecache file created, we can mmap() it into our
+   * application address space using IdeMakecache.
+   */
+
+  ide_makecache_new_for_cache_file_async (self->runtime,
+                                          self->cache_file,
+                                          cancellable,
+                                          ide_autotools_makecache_stage_makecache_cb,
+                                          g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_makecache_stage_execute_async (IdeBuildStage       *stage,
+                                             IdeBuildPipeline    *pipeline,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  IdeAutotoolsMakecacheStage *self = (IdeAutotoolsMakecacheStage *)stage;
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (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_autotools_makecache_stage_execute_async);
+
+  /*
+   * First we need to execute our launcher (performed by our parent class).
+   * Only after that has succeeded to we move on to loading the makecache file
+   * by mmap()'ing the generated make output.
+   */
+
+  IDE_BUILD_STAGE_CLASS (ide_autotools_makecache_stage_parent_class)->execute_async (stage,
+                                                                                     pipeline,
+                                                                                     cancellable,
+                                                                                     
ide_autotools_makecache_stage_execute_cb,
+                                                                                     g_steal_pointer 
(&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_autotools_makecache_stage_execute_finish (IdeBuildStage  *stage,
+                                              GAsyncResult   *result,
+                                              GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (stage));
+  g_assert (G_IS_TASK (result));
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_autotools_makecache_stage_finalize (GObject *object)
+{
+  IdeAutotoolsMakecacheStage *self = (IdeAutotoolsMakecacheStage *)object;
+
+  IDE_ENTRY;
+
+  g_clear_object (&self->makecache);
+  g_clear_object (&self->cache_file);
+  g_clear_object (&self->runtime);
+
+  G_OBJECT_CLASS (ide_autotools_makecache_stage_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_autotools_makecache_stage_class_init (IdeAutotoolsMakecacheStageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *build_stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_autotools_makecache_stage_finalize;
+
+  build_stage_class->execute_async = ide_autotools_makecache_stage_execute_async;
+  build_stage_class->execute_finish = ide_autotools_makecache_stage_execute_finish;
+}
+
+static void
+ide_autotools_makecache_stage_init (IdeAutotoolsMakecacheStage *self)
+{
+  ide_build_stage_set_name (IDE_BUILD_STAGE (self), _("Building cache…"));
+}
+
+IdeBuildStage *
+ide_autotools_makecache_stage_new_for_pipeline (IdeBuildPipeline  *pipeline,
+                                                GError           **error)
+{
+  g_autoptr(IdeAutotoolsMakecacheStage) stage = NULL;
+  g_autoptr(GFile) cache_file = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autofree gchar *cache_path = NULL;
+  const gchar *make = "make";
+  IdeConfiguration *config;
+  IdeRuntime *runtime;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (pipeline), NULL);
+
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+  config = ide_build_pipeline_get_configuration (pipeline);
+  runtime = ide_configuration_get_runtime (config);
+
+  cache_path = ide_build_pipeline_build_builddir_path (pipeline, "Makecache", NULL);
+
+  if (ide_runtime_contains_program_in_path (runtime, "gmake", NULL))
+    make = "gmake";
+
+  if (NULL == (launcher = ide_build_pipeline_create_launcher (pipeline, error)))
+    IDE_RETURN (NULL);
+
+  ide_subprocess_launcher_push_argv (launcher, make);
+  ide_subprocess_launcher_push_argv (launcher, "-p");
+  ide_subprocess_launcher_push_argv (launcher, "-n");
+  ide_subprocess_launcher_push_argv (launcher, "-s");
+
+  stage = g_object_new (IDE_TYPE_AUTOTOOLS_MAKECACHE_STAGE,
+                        "context", context,
+                        "launcher", launcher,
+                        "ignore-exit-status", TRUE,
+                        NULL);
+
+  ide_build_stage_set_stdout_path (IDE_BUILD_STAGE (stage), cache_path);
+
+  g_assert_cmpint (ide_build_stage_launcher_get_ignore_exit_status (IDE_BUILD_STAGE_LAUNCHER (stage)), ==, 
TRUE);
+
+  stage->runtime = g_object_ref (runtime);
+  stage->cache_file = g_file_new_for_path (cache_path);
+
+  IDE_RETURN (g_steal_pointer (&stage));
+}
+
+IdeMakecache *
+ide_autotools_makecache_stage_get_makecache (IdeAutotoolsMakecacheStage *self)
+{
+  g_return_val_if_fail (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (self), NULL);
+
+  return self->makecache;
+}
diff --git a/plugins/autotools/ide-autotools-makecache-stage.h 
b/plugins/autotools/ide-autotools-makecache-stage.h
new file mode 100644
index 0000000..97bd147
--- /dev/null
+++ b/plugins/autotools/ide-autotools-makecache-stage.h
@@ -0,0 +1,38 @@
+/* ide-autotools-makecache-stage.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_AUTOTOOLS_MAKECACHE_STAGE_H
+#define IDE_AUTOTOOLS_MAKECACHE_STAGE_H
+
+#include <ide.h>
+
+#include "ide-makecache.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_AUTOTOOLS_MAKECACHE_STAGE (ide_autotools_makecache_stage_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeAutotoolsMakecacheStage, ide_autotools_makecache_stage, IDE, 
AUTOTOOLS_MAKECACHE_STAGE, IdeBuildStageLauncher)
+
+IdeBuildStage *ide_autotools_makecache_stage_new_for_pipeline (IdeBuildPipeline            *pipeline,
+                                                               GError                     **error);
+IdeMakecache  *ide_autotools_makecache_stage_get_makecache    (IdeAutotoolsMakecacheStage  *self);
+
+G_END_DECLS
+
+#endif /* IDE_AUTOTOOLS_MAKECACHE_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..3f2ac38
--- /dev/null
+++ b/plugins/autotools/ide-autotools-pipeline-addin.c
@@ -0,0 +1,427 @@
+/* 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-makecache-stage.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 gint
+compare_mtime (const gchar *path_a,
+               const gchar *path_b)
+{
+  g_autoptr(GFile) file_a = g_file_new_for_path (path_a);
+  g_autoptr(GFile) file_b = g_file_new_for_path (path_b);
+  g_autoptr(GFileInfo) info_a = NULL;
+  g_autoptr(GFileInfo) info_b = NULL;
+  gint64 ret = 0;
+
+  info_a = g_file_query_info (file_a,
+                              G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                              G_FILE_QUERY_INFO_NONE,
+                              NULL,
+                              NULL);
+
+  info_b = g_file_query_info (file_b,
+                              G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                              G_FILE_QUERY_INFO_NONE,
+                              NULL,
+                              NULL);
+
+  ret = (gint64)g_file_info_get_attribute_uint64 (info_a, G_FILE_ATTRIBUTE_TIME_MODIFIED) -
+        (gint64)g_file_info_get_attribute_uint64 (info_b, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+
+  if (ret < 0)
+    return -1;
+  else if (ret > 0)
+    return 1;
+  return 0;
+}
+
+static void
+check_configure_status (IdeAutotoolsPipelineAddin *self,
+                        IdeBuildPipeline          *pipeline,
+                        GCancellable              *cancellable,
+                        IdeBuildStage             *stage)
+{
+  g_autofree gchar *configure_ac = NULL;
+  g_autofree gchar *configure = NULL;
+  g_autofree gchar *config_status = NULL;
+  g_autofree gchar *makefile = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  configure = ide_build_pipeline_build_srcdir_path (pipeline, "configure", NULL);
+  configure_ac = ide_build_pipeline_build_srcdir_path (pipeline, "configure.ac", NULL);
+  config_status = ide_build_pipeline_build_builddir_path (pipeline, "config.status", NULL);
+  makefile = ide_build_pipeline_build_builddir_path (pipeline, "Makefile", NULL);
+
+  IDE_TRACE_MSG (" configure.ac is at %s", configure_ac);
+  IDE_TRACE_MSG (" configure is at %s", configure);
+  IDE_TRACE_MSG (" config.status is at %s", config_status);
+  IDE_TRACE_MSG (" makefile is at %s", makefile);
+
+  /*
+   * First make sure some essential files exist. If not, we need to run the
+   * configure process.
+   *
+   * TODO: This may take some tweaking if we ever try to reuse existing builds
+   *       that were performed in-tree.
+   */
+  if (!g_file_test (configure_ac, G_FILE_TEST_IS_REGULAR) ||
+      !g_file_test (configure, G_FILE_TEST_IS_REGULAR) ||
+      !g_file_test (config_status, G_FILE_TEST_IS_REGULAR) ||
+      !g_file_test (makefile, G_FILE_TEST_IS_REGULAR))
+    {
+      ide_build_stage_set_completed (stage, FALSE);
+      IDE_EXIT;
+    }
+
+  /*
+   * Now make sure that config.status and Makefile are indeed newer than
+   * our configure script.
+   */
+  if (compare_mtime (configure_ac, configure) < 0 &&
+      compare_mtime (configure, config_status) < 0 &&
+      compare_mtime (configure, makefile) < 0)
+    {
+      /*
+       * TODO: It would be fancy if we could look at '^ac_cs_config=' to determine
+       * if the configure args match what we expect. But this is a bit more
+       * complicated than simply a string comparison.
+       */
+      ide_build_stage_set_completed (stage, TRUE);
+      IDE_EXIT;
+    }
+
+  ide_build_stage_set_completed (stage, FALSE);
+
+  IDE_EXIT;
+}
+
+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;
+  const gchar *config_opts;
+  const gchar *prefix;
+  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);
+  ide_subprocess_launcher_push_argv (launcher, configure_path);
+
+  /*
+   * 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);
+  prefix = ide_configuration_get_prefix (configuration);
+
+  if (prefix != NULL)
+    {
+      g_autofree gchar *prefix_arg = g_strdup_printf ("--prefix=%s", prefix);
+      ide_subprocess_launcher_push_argv (launcher, prefix_arg);
+    }
+
+  if (!ide_str_empty0 (config_opts))
+    {
+      g_auto(GStrv) argv = NULL;
+      gint argc = 0;
+
+      if (!g_shell_parse_argv (config_opts, &argc, &argv, error))
+        return FALSE;
+
+      for (gint i = 0; i < argc; i++)
+        ide_subprocess_launcher_push_argv (launcher, argv[i]);
+    }
+
+  stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                        "context", ide_object_get_context (IDE_OBJECT (self)),
+                        "launcher", launcher,
+                        NULL);
+
+  /*
+   * 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.
+   */
+  g_signal_connect_object (stage,
+                           "query",
+                           G_CALLBACK (check_configure_status),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, stage);
+
+  ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
+
+  return TRUE;
+}
+
+static void
+make_stage_query (IdeAutotoolsPipelineAddin *self,
+                  IdeBuildPipeline          *pipeline,
+                  GCancellable              *cancellable,
+                  IdeBuildStageLauncher     *stage)
+{
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (stage));
+
+  /* Always rely on make to determine up-to-date status */
+  ide_build_stage_set_completed (IDE_BUILD_STAGE (stage), FALSE);
+}
+
+G_GNUC_NULL_TERMINATED static gboolean
+register_make_stage (IdeAutotoolsPipelineAddin  *self,
+                     IdeBuildPipeline           *pipeline,
+                     IdeRuntime                 *runtime,
+                     const gchar                *log_file,
+                     gboolean                    ignore_exit_code,
+                     gboolean                    include_clean,
+                     IdeBuildPhase               phase,
+                     GError                    **error,
+                     const gchar                *make,
+                     const gchar                *first_target,
+                     ...)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeConfiguration *configuration;
+  g_autofree gchar *j = NULL;
+  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));
+  configuration = ide_build_pipeline_get_configuration (pipeline);
+
+  launcher = ide_build_pipeline_create_launcher (pipeline, error);
+
+  if (launcher == NULL)
+    return FALSE;
+
+  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_push_argv (launcher, make);
+  ide_subprocess_launcher_push_argv (launcher, j);
+
+  /* We want silent rules when possible */
+  ide_subprocess_launcher_push_argv (launcher, "V=0");
+
+  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);
+
+  if (ignore_exit_code)
+    ide_build_stage_launcher_set_ignore_exit_status (IDE_BUILD_STAGE_LAUNCHER (stage), TRUE);
+
+  g_signal_connect_object (stage,
+                           "query",
+                           G_CALLBACK (make_stage_query),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  if (include_clean)
+    {
+      g_autoptr(IdeSubprocessLauncher) clean_launcher = NULL;
+
+      clean_launcher = ide_build_pipeline_create_launcher (pipeline, error);
+
+      if (clean_launcher == NULL)
+        return FALSE;
+
+      ide_subprocess_launcher_push_argv (clean_launcher, make);
+      ide_subprocess_launcher_push_argv (clean_launcher, "clean");
+
+      ide_build_stage_launcher_set_clean_launcher (IDE_BUILD_STAGE_LAUNCHER (stage), clean_launcher);
+    }
+
+  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 gboolean
+register_makecache_stage (IdeAutotoolsPipelineAddin  *self,
+                          IdeBuildPipeline           *pipeline,
+                          GError                    **error)
+{
+  g_autoptr(IdeBuildStage) stage = NULL;
+  guint stage_id;
+
+  g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (NULL == (stage = ide_autotools_makecache_stage_new_for_pipeline (pipeline, error)))
+    return FALSE;
+
+  stage_id = ide_build_pipeline_connect (pipeline,
+                                         IDE_BUILD_PHASE_CONFIGURE | IDE_BUILD_PHASE_AFTER,
+                                         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_autoptr(GError) error = NULL;
+  IdeConfiguration *config;
+  IdeBuildSystem *build_system;
+  IdeContext *context;
+  IdeRuntime *runtime;
+  const gchar *make = "make";
+
+  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;
+
+  if (ide_runtime_contains_program_in_path (runtime, "gmake", NULL))
+    make = "gmake";
+
+  if (!register_autoreconf_stage (self, pipeline, &error) ||
+      !register_configure_stage (self, pipeline, &error) ||
+      !register_makecache_stage (self, pipeline, &error) ||
+      !register_make_stage (self, pipeline, runtime, NULL, FALSE, TRUE, IDE_BUILD_PHASE_BUILD, &error, make, 
"all", NULL) ||
+      !register_make_stage (self, pipeline, runtime, NULL, FALSE, FALSE, IDE_BUILD_PHASE_INSTALL, &error, 
make, "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/autotools/ide-makecache.c b/plugins/autotools/ide-makecache.c
index b836714..44774c5 100644
--- a/plugins/autotools/ide-makecache.c
+++ b/plugins/autotools/ide-makecache.c
@@ -48,7 +48,6 @@ struct _IdeMakecache
 {
   IdeObject     parent_instance;
 
-  GFile        *makefile;
   GFile        *parent;
   GMappedFile  *mapped;
   EggTaskCache *file_targets_cache;
@@ -76,14 +75,6 @@ G_DEFINE_TYPE (IdeMakecache, ide_makecache, IDE_TYPE_OBJECT)
 
 EGG_DEFINE_COUNTER (instances, "IdeMakecache", "Instances", "The number of IdeMakecache")
 
-enum {
-  PROP_0,
-  PROP_MAKEFILE,
-  LAST_PROP
-};
-
-static GParamSpec *properties [LAST_PROP];
-
 static void
 file_flags_lookup_free (gpointer data)
 {
@@ -344,292 +335,6 @@ ide_makecache_validate_mapped_file (GMappedFile  *mapped,
   IDE_RETURN (TRUE);
 }
 
-static int
-ide_makecache_open_temp (IdeMakecache  *self,
-                         gchar        **name_used,
-                         GError       **error)
-{
-  IdeContext *context;
-  IdeProject *project;
-  const gchar *project_id;
-  g_autofree gchar *name = NULL;
-  g_autofree gchar *path = NULL;
-  g_autofree gchar *directory = NULL;
-  time_t now;
-  int fd;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_MAKECACHE (self));
-  g_assert (name_used);
-  g_assert (error);
-  g_assert (!*error);
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  project = ide_context_get_project (context);
-  project_id = ide_project_get_id (project);
-
-  directory = g_build_filename (g_get_user_cache_dir (),
-                                ide_get_program_name (),
-                                "makecache",
-                                NULL);
-
-  g_debug ("Using \"%s\" for makecache directory", directory);
-
-  if (g_mkdir_with_parents (directory, 0700) != 0)
-    {
-      g_set_error (error,
-                   G_IO_ERROR,
-                   g_io_error_from_errno (errno),
-                   "Failed to create makecache directory");
-      IDE_RETURN (-1);
-    }
-
-  now = time (NULL);
-  name = g_strdup_printf ("%s.makecache.tmp-%u", project_id, (guint)now);
-  path = g_build_filename (directory, name, NULL);
-
-  g_debug ("Creating temporary makecache at \"%s\"", path);
-
-  fd = g_open (path, O_CREAT|O_RDWR, 0600);
-
-  if (fd == -1)
-    {
-      *name_used = NULL;
-      g_set_error (error,
-                   G_IO_ERROR,
-                   g_io_error_from_errno (errno),
-                   "Failed to open temporary file: %s",
-                   g_strerror (errno));
-      IDE_RETURN (-1);
-    }
-
-  *name_used = g_strdup (path);
-
-  IDE_RETURN (fd);
-}
-
-static void
-ide_makecache_new_worker (GTask        *task,
-                          gpointer      source_object,
-                          gpointer      task_data,
-                          GCancellable *cancellable)
-{
-  IdeMakecache *self = source_object;
-  IdeRuntime *runtime = task_data;
-  IdeContext *context;
-  IdeProject *project;
-  const gchar *project_id;
-  g_autofree gchar *name_used = NULL;
-  g_autofree gchar *name = NULL;
-  g_autofree gchar *cache_path = NULL;
-  g_autoptr(GFile) parent = NULL;
-  g_autofree gchar *workdir = NULL;
-  g_autoptr(GMappedFile) mapped = NULL;
-  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeSubprocess) subprocess = NULL;
-  GError *error = NULL;
-  int fdcopy;
-  int fd;
-
-  IDE_ENTRY;
-
-  g_assert (G_IS_TASK (task));
-  g_assert (IDE_IS_MAKECACHE (self));
-  g_assert (IDE_IS_RUNTIME (runtime));
-
-  if (!self->makefile || !(parent = g_file_get_parent (self->makefile)))
-    {
-      g_task_return_new_error (task,
-                               G_IO_ERROR,
-                               G_IO_ERROR_INVALID_FILENAME,
-                               "No makefile was specified.");
-      IDE_EXIT;
-    }
-
-  workdir = g_file_get_path (parent);
-
-  if (!workdir)
-    {
-      g_task_return_new_error (task,
-                               G_IO_ERROR,
-                               G_IO_ERROR_INVALID_FILENAME,
-                               "Makefile must be accessable on local filesystem.");
-      IDE_EXIT;
-    }
-
-  /*
-   * If the runtime has a "gmake" instead of "make", we want to prefer that
-   * since we know it is GNU make.
-   */
-  if (ide_runtime_contains_program_in_path (runtime, "gmake", cancellable))
-    self->make_name = "gmake";
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  project = ide_context_get_project (context);
-  project_id = ide_project_get_id (project);
-  name = g_strdup_printf ("%s.makecache", project_id);
-  cache_path = g_build_filename (g_get_user_cache_dir (),
-                                 ide_get_program_name (),
-                                 "makecache",
-                                 name,
-                                 NULL);
-
- /*
-  * NOTE:
-  *
-  * The makecache file is a file that contains all of the output from `make -p -n -s` on an
-  * automake project. This contains everything we need to determine what make targets a file
-  * "belongs to".
-  *
-  * The process is as follows.
-  *
-  * 1) Open a new temporary file to contain the output. This needs to be in the same directory
-  *    as the target so that we can rename() it into place.
-  * 2) dup() the fd to pass to the child process.
-  * 3) Spawn `make -p -n -s` using the temporary file as stdout.
-  * 4) Wait for the subprocess to complete. It would be nice if we could do this asynchronously,
-  *    but we'd need to break this whole thing into more tasks.
-  * 5) Move the temporary file into position at ~/.cache/<prgname>/<project>.makecache
-  * 6) mmap() the cache file using g_mapped_file_new_from_fd().
-  * 7) Close the fd. This does NOT cause the mmap() region to be unmapped.
-  * 8) Validate the mmap() contents with g_utf8_validate().
-  */
-
-  /*
-   * Step 1, open our temporary file.
-   */
-  fd = ide_makecache_open_temp (self, &name_used, &error);
-
-  if (fd == -1)
-    {
-      g_assert (error != NULL);
-      g_task_return_error (task, error);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 2, make an extra fd to be passed to the child process.
-   */
-  if (-1 == (fdcopy = dup (fd)))
-    {
-      g_task_return_new_error (task,
-                               G_IO_ERROR,
-                               g_io_error_from_errno (errno),
-                               "Failed to open temporary file: %s",
-                               g_strerror (errno));
-      close (fd);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 3,
-   *
-   * Spawn `make -p -n -s` in the directory containing our makefile.
-   */
-  launcher = ide_runtime_create_launcher (runtime, &error);
-
-  if (launcher == NULL)
-    {
-      g_task_return_error (task, error);
-      close (fdcopy);
-      close (fd);
-      IDE_EXIT;
-    }
-
-  ide_subprocess_launcher_push_argv (launcher, self->make_name);
-  ide_subprocess_launcher_push_argv (launcher, "-p");
-  ide_subprocess_launcher_push_argv (launcher, "-n");
-  ide_subprocess_launcher_push_argv (launcher, "-s");
-  ide_subprocess_launcher_set_cwd (launcher, workdir);
-
-  ide_subprocess_launcher_set_flags (launcher, G_SUBPROCESS_FLAGS_STDERR_SILENCE);
-  ide_subprocess_launcher_take_stdout_fd (launcher, fdcopy);
-  fdcopy = -1;
-
-  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
-
-  if (subprocess == NULL)
-    {
-      g_assert (error != NULL);
-      g_task_return_error (task, error);
-      close (fd);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 4, wait for the subprocess to complete.
-   */
-  IDE_TRACE_MSG ("waiting for process to exit");
-
-  if (!ide_subprocess_wait (subprocess, cancellable, &error))
-    {
-      g_assert (error != NULL);
-      g_task_return_error (task, error);
-      close (fd);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 5, move the file into location at the cache path.
-   *
-   * TODO:
-   *
-   * If we can switch to O_TMPFILE and use renameat2(), that would be neat. I'm not sure that is
-   * possible though since the O_TMPFILE will not have a filename associated with it.
-   */
-  if (0 != g_rename (name_used, cache_path))
-    {
-      g_task_return_new_error (task,
-                               G_IO_ERROR,
-                               g_io_error_from_errno (errno),
-                               "Failed to move makecache into target directory: %s",
-                               g_strerror (errno));
-      close (fd);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 6, map the makecache file into memory.
-   */
-  lseek (fd, 0, SEEK_SET);
-  mapped = g_mapped_file_new_from_fd (fd, FALSE, &error);
-
-  if (mapped == NULL)
-    {
-      g_assert (error != NULL);
-      g_task_return_error (task, error);
-      close (fd);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 7, we are done with fd, so close it now. Note that this does not have an effect on the
-   * mmap() region.
-   */
-  close (fd);
-
-  /*
-   * Step 8, validate the contents of the mmap region.
-   */
-  if (!ide_makecache_validate_mapped_file (mapped, &error))
-    {
-      g_assert (error != NULL);
-      g_task_return_error (task, error);
-      IDE_EXIT;
-    }
-
-  /*
-   * Step 9, save the mmap and runtime for future use.
-   */
-  self->mapped = g_mapped_file_ref (mapped);
-  self->runtime = g_object_ref (runtime);
-
-  g_task_return_pointer (task, g_object_ref (self), g_object_unref);
-
-  IDE_EXIT;
-}
-
 static void
 ide_makecache_parse_c_cxx_include (IdeMakecache *self,
                                    GPtrArray    *ret,
@@ -1064,27 +769,6 @@ ide_makecache_get_file_flags_worker (GTask        *task,
   IDE_EXIT;
 }
 
-static void
-ide_makecache_set_makefile (IdeMakecache *self,
-                            GFile        *makefile)
-{
-  g_autoptr(GFile) parent = NULL;
-
-  g_return_if_fail (IDE_IS_MAKECACHE (self));
-  g_return_if_fail (G_IS_FILE (makefile));
-
-  parent = g_file_get_parent (makefile);
-
-  if (!parent)
-    {
-      g_warning (_("Invalid makefile provided, ignoring."));
-      return;
-    }
-
-  g_set_object (&self->makefile, makefile);
-  g_set_object (&self->parent, parent);
-}
-
 static gchar *
 replace_suffix (const gchar *str,
                 const gchar *replace)
@@ -1336,11 +1020,12 @@ ide_makecache_finalize (GObject *object)
 {
   IdeMakecache *self = (IdeMakecache *)object;
 
-  g_clear_object (&self->makefile);
-  g_clear_pointer (&self->mapped, g_mapped_file_unref);
   g_clear_object (&self->file_targets_cache);
   g_clear_object (&self->file_flags_cache);
   g_clear_object (&self->runtime);
+  g_clear_object (&self->parent);
+
+  g_clear_pointer (&self->mapped, g_mapped_file_unref);
   g_clear_pointer (&self->build_targets, g_ptr_array_unref);
 
   G_OBJECT_CLASS (ide_makecache_parent_class)->finalize (object);
@@ -1349,60 +1034,11 @@ ide_makecache_finalize (GObject *object)
 }
 
 static void
-ide_makecache_get_property (GObject    *object,
-                            guint       prop_id,
-                            GValue     *value,
-                            GParamSpec *pspec)
-{
-  IdeMakecache *self = IDE_MAKECACHE (object);
-
-  switch (prop_id)
-    {
-    case PROP_MAKEFILE:
-      g_value_set_object (value, ide_makecache_get_makefile (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-ide_makecache_set_property (GObject      *object,
-                            guint         prop_id,
-                            const GValue *value,
-                            GParamSpec   *pspec)
-{
-  IdeMakecache *self = IDE_MAKECACHE (object);
-
-  switch (prop_id)
-    {
-    case PROP_MAKEFILE:
-      ide_makecache_set_makefile (self, g_value_get_object (value));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
 ide_makecache_class_init (IdeMakecacheClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->finalize = ide_makecache_finalize;
-  object_class->get_property = ide_makecache_get_property;
-  object_class->set_property = ide_makecache_set_property;
-
-  properties [PROP_MAKEFILE] =
-    g_param_spec_object ("makefile",
-                         "Makefile",
-                         "The root makefile to be cached.",
-                         G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
 }
 
 static void
@@ -1439,57 +1075,113 @@ ide_makecache_init (IdeMakecache *self)
   egg_task_cache_set_name (self->file_flags_cache, "makecache: file-flags-cache");
 }
 
-GFile *
-ide_makecache_get_makefile (IdeMakecache *self)
+static void
+ide_makecache_validate_worker (GTask        *task,
+                               gpointer      source_object,
+                               gpointer      task_data,
+                               GCancellable *cancellable)
 {
-  g_return_val_if_fail (IDE_IS_MAKECACHE (self), NULL);
+  IdeMakecache *self = task_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAKECACHE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_makecache_validate_mapped_file (self->mapped, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_pointer (task, g_object_ref (self), g_object_unref);
 
-  return self->makefile;
+  IDE_EXIT;
 }
 
 void
-ide_makecache_new_for_makefile_async (IdeRuntime          *runtime,
-                                      GFile               *makefile,
-                                      GCancellable        *cancellable,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
+ide_makecache_new_for_cache_file_async (IdeRuntime          *runtime,
+                                        GFile               *cache_file,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
 {
   g_autoptr(GTask) task = NULL;
   g_autoptr(IdeMakecache) self = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(GMappedFile) mapped = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *cache_path = NULL;
   IdeContext *context;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_RUNTIME (runtime));
-  g_return_if_fail (G_IS_FILE (makefile));
+  g_return_if_fail (G_IS_FILE (cache_file));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-#ifdef IDE_ENABLE_TRACE
-  {
-    g_autofree gchar *path = g_file_get_path (makefile);
-    IDE_TRACE_MSG ("Generating makecache for %s", path);
-  }
-#endif
+  task = g_task_new (NULL, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_makecache_new_for_cache_file_async);
+
+  if (!g_file_is_native (cache_file))
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_INVALID_FILENAME,
+                               "Makecache files must be on a native filesystem");
+      IDE_EXIT;
+    }
+
+  cache_path = g_file_get_path (cache_file);
+
+  if (cache_path == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_INVALID_FILENAME,
+                               "Makecache files must be on a native filesystem");
+      IDE_EXIT;
+    }
+
+  parent = g_file_get_parent (cache_file);
+
+  if (parent == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_INVALID_FILENAME,
+                               "Makecache cannot be /");
+      IDE_EXIT;
+    }
 
   context = ide_object_get_context (IDE_OBJECT (runtime));
 
   self = g_object_new (IDE_TYPE_MAKECACHE,
                        "context", context,
-                       "makefile", makefile,
                        NULL);
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_source_tag (task, ide_makecache_new_for_makefile_async);
-  g_task_set_task_data (task, g_object_ref (runtime), g_object_unref);
+  mapped = g_mapped_file_new (cache_path, FALSE, &error);
 
-  ide_thread_pool_push_task (IDE_THREAD_POOL_COMPILER, task, ide_makecache_new_worker);
+  if (mapped == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  self->parent = g_steal_pointer (&parent);
+  self->mapped = g_steal_pointer (&mapped);
+  self->runtime = g_object_ref (runtime);
+
+  if (ide_runtime_contains_program_in_path (runtime, "gmake", NULL))
+    self->make_name = "gmake";
+
+  g_task_set_task_data (task, g_steal_pointer (&self), g_object_unref);
+  g_task_run_in_thread (task, ide_makecache_validate_worker);
 
   IDE_EXIT;
 }
 
 IdeMakecache *
-ide_makecache_new_for_makefile_finish (GAsyncResult  *result,
-                                       GError       **error)
+ide_makecache_new_for_cache_file_finish (GAsyncResult  *result,
+                                         GError       **error)
 {
   GTask *task = (GTask *)result;
   IdeMakecache *ret;
diff --git a/plugins/autotools/ide-makecache.h b/plugins/autotools/ide-makecache.h
index d069b48..151d138 100644
--- a/plugins/autotools/ide-makecache.h
+++ b/plugins/autotools/ide-makecache.h
@@ -29,38 +29,37 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeMakecache, ide_makecache, IDE, MAKECACHE, IdeObject)
 
-void                 ide_makecache_new_for_makefile_async   (IdeRuntime           *runtime,
-                                                             GFile                *makefile,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-IdeMakecache        *ide_makecache_new_for_makefile_finish  (GAsyncResult         *result,
-                                                             GError              **error);
-GFile               *ide_makecache_get_makefile             (IdeMakecache         *self);
-void                 ide_makecache_get_file_flags_async     (IdeMakecache         *self,
-                                                             GFile                *file,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-gchar              **ide_makecache_get_file_flags_finish    (IdeMakecache         *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
-void                 ide_makecache_get_file_targets_async   (IdeMakecache         *self,
-                                                             GFile                *file,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-GPtrArray           *ide_makecache_get_file_targets_finish  (IdeMakecache         *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
-void                 ide_makecache_get_build_targets_async  (IdeMakecache         *self,
-                                                             GFile                *build_dir,
-                                                             GCancellable         *cancellable,
-                                                             GAsyncReadyCallback   callback,
-                                                             gpointer              user_data);
-GPtrArray           *ide_makecache_get_build_targets_finish (IdeMakecache         *self,
-                                                             GAsyncResult         *result,
-                                                             GError              **error);
+void                 ide_makecache_new_for_cache_file_async  (IdeRuntime           *runtime,
+                                                              GFile                *cache_file,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+IdeMakecache        *ide_makecache_new_for_cache_file_finish (GAsyncResult         *result,
+                                                              GError              **error);
+void                 ide_makecache_get_file_flags_async      (IdeMakecache         *self,
+                                                              GFile                *file,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+gchar              **ide_makecache_get_file_flags_finish     (IdeMakecache         *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+void                 ide_makecache_get_file_targets_async    (IdeMakecache         *self,
+                                                              GFile                *file,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+GPtrArray           *ide_makecache_get_file_targets_finish   (IdeMakecache         *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+void                 ide_makecache_get_build_targets_async   (IdeMakecache         *self,
+                                                              GFile                *build_dir,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+GPtrArray           *ide_makecache_get_build_targets_finish  (IdeMakecache         *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
 
 G_END_DECLS
 
diff --git a/plugins/build-tools/gbp-build-log-panel.c b/plugins/build-tools/gbp-build-log-panel.c
index 59d02e0..7de582a 100644
--- a/plugins/build-tools/gbp-build-log-panel.c
+++ b/plugins/build-tools/gbp-build-log-panel.c
@@ -29,8 +29,7 @@ struct _GbpBuildLogPanel
 {
   PnlDockWidget      parent_instance;
 
-  IdeBuildResult    *result;
-  EggSignalGroup    *signals;
+  IdeBuildPipeline  *pipeline;
   GtkCssProvider    *css;
   GSettings         *settings;
   GtkTextBuffer     *buffer;
@@ -38,11 +37,13 @@ struct _GbpBuildLogPanel
   GtkScrolledWindow *scroller;
   GtkTextView       *text_view;
   GtkTextTag        *stderr_tag;
+
+  guint              log_observer;
 };
 
 enum {
   PROP_0,
-  PROP_RESULT,
+  PROP_PIPELINE,
   LAST_PROP
 };
 
@@ -87,23 +88,26 @@ gbp_build_log_panel_reset_view (GbpBuildLogPanel *self)
 }
 
 static void
-gbp_build_log_panel_log (GbpBuildLogPanel  *self,
-                         IdeBuildResultLog  log,
-                         const gchar       *message,
-                         IdeBuildResult    *result)
+gbp_build_log_panel_log_observer (IdeBuildLogStream  stream,
+                                  const gchar       *message,
+                                  gssize             message_len,
+                                  gpointer           user_data)
 {
+  GbpBuildLogPanel *self = user_data;
   GtkTextMark *insert;
   GtkTextIter iter;
 
   g_assert (GBP_IS_BUILD_LOG_PANEL (self));
   g_assert (message != NULL);
-  g_assert (IDE_IS_BUILD_RESULT (result));
+  g_assert (message_len >= 0);
+  g_assert (message[message_len] == '\0');
 
   gtk_text_buffer_get_end_iter (self->buffer, &iter);
 
-  if (G_LIKELY (log == IDE_BUILD_RESULT_LOG_STDOUT))
+  if G_LIKELY (stream == IDE_BUILD_LOG_STDOUT)
     {
       gtk_text_buffer_insert (self->buffer, &iter, message, -1);
+      gtk_text_buffer_insert (self->buffer, &iter, "\n", 1);
     }
   else
     {
@@ -112,6 +116,7 @@ gbp_build_log_panel_log (GbpBuildLogPanel  *self,
 
       offset = gtk_text_iter_get_offset (&iter);
       gtk_text_buffer_insert (self->buffer, &iter, message, -1);
+      gtk_text_buffer_insert (self->buffer, &iter, "\n", 1);
       gtk_text_buffer_get_iter_at_offset (self->buffer, &begin, offset);
       gtk_text_buffer_apply_tag (self->buffer, self->stderr_tag, &begin, &iter);
     }
@@ -121,16 +126,30 @@ gbp_build_log_panel_log (GbpBuildLogPanel  *self,
 }
 
 void
-gbp_build_log_panel_set_result (GbpBuildLogPanel *self,
-                                IdeBuildResult   *result)
+gbp_build_log_panel_set_pipeline (GbpBuildLogPanel *self,
+                                  IdeBuildPipeline *pipeline)
 {
   g_return_if_fail (GBP_IS_BUILD_LOG_PANEL (self));
-  g_return_if_fail (!result || IDE_IS_BUILD_RESULT (result));
+  g_return_if_fail (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
 
-  if (g_set_object (&self->result, result))
+  if (pipeline != self->pipeline)
     {
-      gbp_build_log_panel_reset_view (self);
-      egg_signal_group_set_target (self->signals, result);
+      if (self->pipeline != NULL)
+        {
+          ide_build_pipeline_remove_log_observer (self->pipeline, self->log_observer);
+          self->log_observer = 0;
+          g_clear_object (&self->pipeline);
+        }
+
+      if (pipeline != NULL)
+        {
+          self->pipeline = g_object_ref (pipeline);
+          self->log_observer =
+            ide_build_pipeline_add_log_observer (self->pipeline,
+                                                 gbp_build_log_panel_log_observer,
+                                                 self,
+                                                 NULL);
+        }
     }
 }
 
@@ -174,8 +193,7 @@ gbp_build_log_panel_finalize (GObject *object)
 
   self->stderr_tag = NULL;
 
-  g_clear_object (&self->result);
-  g_clear_object (&self->signals);
+  g_clear_object (&self->pipeline);
   g_clear_object (&self->css);
   g_clear_object (&self->settings);
 
@@ -183,6 +201,16 @@ gbp_build_log_panel_finalize (GObject *object)
 }
 
 static void
+gbp_build_log_panel_dispose (GObject *object)
+{
+  GbpBuildLogPanel *self = (GbpBuildLogPanel *)object;
+
+  gbp_build_log_panel_set_pipeline (self, NULL);
+
+  G_OBJECT_CLASS (gbp_build_log_panel_parent_class)->dispose (object);
+}
+
+static void
 gbp_build_log_panel_get_property (GObject    *object,
                                   guint       prop_id,
                                   GValue     *value,
@@ -192,8 +220,8 @@ gbp_build_log_panel_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_RESULT:
-      g_value_set_object (value, self->result);
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
       break;
 
     default:
@@ -211,8 +239,8 @@ gbp_build_log_panel_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_RESULT:
-      gbp_build_log_panel_set_result (self, g_value_get_object (value));
+    case PROP_PIPELINE:
+      gbp_build_log_panel_set_pipeline (self, g_value_get_object (value));
       break;
 
     default:
@@ -226,6 +254,7 @@ gbp_build_log_panel_class_init (GbpBuildLogPanelClass *klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
+  object_class->dispose = gbp_build_log_panel_dispose;
   object_class->finalize = gbp_build_log_panel_finalize;
   object_class->get_property = gbp_build_log_panel_get_property;
   object_class->set_property = gbp_build_log_panel_set_property;
@@ -234,11 +263,11 @@ gbp_build_log_panel_class_init (GbpBuildLogPanelClass *klass)
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/build-tools-plugin/gbp-build-log-panel.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpBuildLogPanel, scroller);
 
-  properties [PROP_RESULT] =
-    g_param_spec_object ("result",
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
                          "Result",
                          "Result",
-                         IDE_TYPE_BUILD_RESULT,
+                         IDE_TYPE_BUILD_PIPELINE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
@@ -255,14 +284,6 @@ gbp_build_log_panel_init (GbpBuildLogPanel *self)
 
   gbp_build_log_panel_reset_view (self);
 
-  self->signals = egg_signal_group_new (IDE_TYPE_BUILD_RESULT);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "log",
-                                   G_CALLBACK (gbp_build_log_panel_log),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
   self->settings = g_settings_new ("org.gnome.builder.terminal");
   g_signal_connect_object (self->settings,
                            "changed::font-name",
diff --git a/plugins/build-tools/gbp-build-log-panel.h b/plugins/build-tools/gbp-build-log-panel.h
index df407ba..f4d5079 100644
--- a/plugins/build-tools/gbp-build-log-panel.h
+++ b/plugins/build-tools/gbp-build-log-panel.h
@@ -19,7 +19,6 @@
 #ifndef GBP_BUILD_LOG_PANEL_H
 #define GBP_BUILD_LOG_PANEL_H
 
-#include <gtk/gtk.h>
 #include <ide.h>
 
 G_BEGIN_DECLS
@@ -28,8 +27,8 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpBuildLogPanel, gbp_build_log_panel, GBP, BUILD_LOG_PANEL, PnlDockWidget)
 
-void gbp_build_log_panel_set_result (GbpBuildLogPanel *self,
-                                     IdeBuildResult   *result);
+void gbp_build_log_panel_set_pipeline (GbpBuildLogPanel *self,
+                                       IdeBuildPipeline *pipeline);
 
 G_END_DECLS
 
diff --git a/plugins/build-tools/gbp-build-panel.c b/plugins/build-tools/gbp-build-panel.c
index d875ef9..0fb954f 100644
--- a/plugins/build-tools/gbp-build-panel.c
+++ b/plugins/build-tools/gbp-build-panel.c
@@ -19,19 +19,14 @@
 #include <glib/gi18n.h>
 #include <ide.h>
 
-#include "egg-binding-group.h"
-#include "egg-signal-group.h"
-
 #include "gbp-build-panel.h"
 
 struct _GbpBuildPanel
 {
   PnlDockWidget        parent_instance;
 
-  IdeBuildResult      *result;
-  EggSignalGroup      *signals;
-  EggBindingGroup     *bindings;
   GHashTable          *diags_hash;
+  IdeBuildPipeline    *pipeline;
 
   GtkListStore        *diagnostics_store;
   GtkCellRendererText *diagnostics_text;
@@ -51,23 +46,23 @@ struct _GbpBuildPanel
 G_DEFINE_TYPE (GbpBuildPanel, gbp_build_panel, PNL_TYPE_DOCK_WIDGET)
 
 enum {
-  PROP_0,
-  PROP_RESULT,
-  LAST_PROP
-};
-
-enum {
   COLUMN_DIAGNOSTIC,
   COLUMN_TEXT,
   LAST_COLUMN
 };
 
-static GParamSpec *properties [LAST_PROP];
+enum {
+  PROP_0,
+  PROP_PIPELINE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
 
 static void
-gbp_build_panel_diagnostic (GbpBuildPanel  *self,
-                            IdeDiagnostic  *diagnostic,
-                            IdeBuildResult *result)
+gbp_build_panel_diagnostic (GbpBuildPanel    *self,
+                            IdeDiagnostic    *diagnostic,
+                            IdeBuildPipeline *pipeline)
 {
   IdeDiagnosticSeverity severity;
   guint hash;
@@ -76,7 +71,7 @@ gbp_build_panel_diagnostic (GbpBuildPanel  *self,
 
   g_assert (GBP_IS_BUILD_PANEL (self));
   g_assert (diagnostic != NULL);
-  g_assert (IDE_IS_BUILD_RESULT (result));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
   severity = ide_diagnostic_get_severity (diagnostic);
 
@@ -151,49 +146,73 @@ gbp_build_panel_diagnostic (GbpBuildPanel  *self,
 static void
 gbp_build_panel_update_running_time (GbpBuildPanel *self)
 {
+  g_autofree gchar *text = NULL;
+
   g_assert (GBP_IS_BUILD_PANEL (self));
 
-  if (self->result != NULL)
+  if (self->pipeline != NULL)
     {
+      IdeBuildManager *build_manager;
+      IdeContext *context;
       GTimeSpan span;
-      guint hours;
-      guint minutes;
-      guint seconds;
-      gchar *text;
-
-      span = ide_build_result_get_running_time (self->result);
 
-      hours = span / G_TIME_SPAN_HOUR;
-      minutes = (span % G_TIME_SPAN_HOUR) / G_TIME_SPAN_MINUTE;
-      seconds = (span % G_TIME_SPAN_MINUTE) / G_TIME_SPAN_SECOND;
+      context = ide_widget_get_context (GTK_WIDGET (self));
+      build_manager = ide_context_get_build_manager (context);
 
-      text = g_strdup_printf ("%02u:%02u:%02u", hours, minutes, seconds);
-      gtk_label_set_label (self->running_time_label, text);
-      g_free (text);
-    }
-  else
-    {
-      gtk_label_set_label (self->running_time_label, NULL);
+      span = ide_build_manager_get_running_time (build_manager);
+      text = ide_g_time_span_to_label (span);
     }
+
+  gtk_label_set_label (self->running_time_label, text);
 }
 
 static void
-gbp_build_panel_connect (GbpBuildPanel  *self,
-                         IdeBuildResult *result)
+gbp_build_panel_started (GbpBuildPanel    *self,
+                         IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILD_PANEL (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  self->error_count = 0;
+  self->warning_count = 0;
+
+  gtk_label_set_label (self->warnings_label, "—");
+  gtk_label_set_label (self->errors_label, "—");
+
+  gtk_list_store_clear (self->diagnostics_store);
+  g_hash_table_remove_all (self->diags_hash);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_build_panel_connect (GbpBuildPanel    *self,
+                         IdeBuildPipeline *pipeline)
 {
   g_return_if_fail (GBP_IS_BUILD_PANEL (self));
-  g_return_if_fail (IDE_IS_BUILD_RESULT (result));
-  g_return_if_fail (self->result == NULL);
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (self->pipeline == NULL);
 
-  self->result = g_object_ref (result);
+  self->pipeline = g_object_ref (pipeline);
   self->error_count = 0;
   self->warning_count = 0;
 
   gtk_label_set_label (self->warnings_label, "—");
   gtk_label_set_label (self->errors_label, "—");
 
-  egg_signal_group_set_target (self->signals, result);
-  egg_binding_group_set_source (self->bindings, result);
+  g_signal_connect_object (pipeline,
+                           "diagnostic",
+                           G_CALLBACK (gbp_build_panel_diagnostic),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (pipeline,
+                           "started",
+                           G_CALLBACK (gbp_build_panel_started),
+                           self,
+                           G_CONNECT_SWAPPED);
 
   gtk_revealer_set_reveal_child (self->status_revealer, TRUE);
 
@@ -204,57 +223,38 @@ static void
 gbp_build_panel_disconnect (GbpBuildPanel *self)
 {
   g_return_if_fail (GBP_IS_BUILD_PANEL (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self->pipeline));
+
+  g_signal_handlers_disconnect_by_func (self->pipeline,
+                                        G_CALLBACK (gbp_build_panel_diagnostic),
+                                        self);
+  g_clear_object (&self->pipeline);
 
   gtk_revealer_set_reveal_child (self->status_revealer, FALSE);
 
-  egg_signal_group_set_target (self->signals, NULL);
-  egg_binding_group_set_source (self->bindings, NULL);
-  g_clear_object (&self->result);
   g_hash_table_remove_all (self->diags_hash);
   gtk_list_store_clear (self->diagnostics_store);
   gtk_stack_set_visible_child_name (self->stack, "empty-state");
 }
 
 void
-gbp_build_panel_set_result (GbpBuildPanel  *self,
-                            IdeBuildResult *result)
+gbp_build_panel_set_pipeline (GbpBuildPanel    *self,
+                              IdeBuildPipeline *pipeline)
 {
   g_return_if_fail (GBP_IS_BUILD_PANEL (self));
-  g_return_if_fail (!result || IDE_IS_BUILD_RESULT (result));
+  g_return_if_fail (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
 
-  if (result != self->result)
+  if (pipeline != self->pipeline)
     {
-      if (self->result)
+      if (self->pipeline)
         gbp_build_panel_disconnect (self);
 
-      if (result)
-        gbp_build_panel_connect (self, result);
+      if (pipeline)
+        gbp_build_panel_connect (self, pipeline);
     }
 }
 
 static void
-gbp_build_panel_notify_running (GbpBuildPanel  *self,
-                                GParamSpec     *pspec,
-                                IdeBuildResult *result)
-{
-  g_assert (GBP_IS_BUILD_PANEL (self));
-  g_assert (IDE_IS_BUILD_RESULT (result));
-
-  gbp_build_panel_update_running_time (self);
-}
-
-static void
-gbp_build_panel_notify_running_time (GbpBuildPanel  *self,
-                                     GParamSpec     *pspec,
-                                     IdeBuildResult *result)
-{
-  g_assert (GBP_IS_BUILD_PANEL (self));
-  g_assert (IDE_IS_BUILD_RESULT (result));
-
-  gbp_build_panel_update_running_time (self);
-}
-
-static void
 gbp_build_panel_diagnostic_activated (GbpBuildPanel     *self,
                                       GtkTreePath       *path,
                                       GtkTreeViewColumn *colun,
@@ -371,15 +371,61 @@ gbp_build_panel_text_func (GtkCellLayout   *layout,
 }
 
 static void
+gbp_build_panel_context_handler (GtkWidget  *widget,
+                                 IdeContext *context)
+{
+  GbpBuildPanel *self = (GbpBuildPanel *)widget;
+  IdeBuildManager *build_manager;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILD_PANEL (self));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context == NULL)
+    IDE_EXIT;
+
+  build_manager = ide_context_get_build_manager (context);
+
+  g_object_bind_property (build_manager, "message",
+                          self->status_label, "label",
+                          G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (build_manager,
+                           "notify::running-time",
+                           G_CALLBACK (gbp_build_panel_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-started",
+                           G_CALLBACK (gbp_build_panel_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-finished",
+                           G_CALLBACK (gbp_build_panel_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-failed",
+                           G_CALLBACK (gbp_build_panel_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_EXIT;
+}
+
+static void
 gbp_build_panel_destroy (GtkWidget *widget)
 {
   GbpBuildPanel *self = (GbpBuildPanel *)widget;
 
-  if (self->result)
+  if (self->pipeline != NULL)
     gbp_build_panel_disconnect (self);
 
-  g_clear_object (&self->bindings);
-  g_clear_object (&self->signals);
   g_clear_pointer (&self->diags_hash, g_hash_table_unref);
 
   GTK_WIDGET_CLASS (gbp_build_panel_parent_class)->destroy (widget);
@@ -391,16 +437,16 @@ gbp_build_panel_get_property (GObject    *object,
                               GValue     *value,
                               GParamSpec *pspec)
 {
-  GbpBuildPanel *self = GBP_BUILD_PANEL(object);
+  GbpBuildPanel *self = GBP_BUILD_PANEL (object);
 
   switch (prop_id)
     {
-    case PROP_RESULT:
-      g_value_set_object (value, self->result);
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
       break;
 
     default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
 }
 
@@ -410,16 +456,16 @@ gbp_build_panel_set_property (GObject      *object,
                               const GValue *value,
                               GParamSpec   *pspec)
 {
-  GbpBuildPanel *self = GBP_BUILD_PANEL(object);
+  GbpBuildPanel *self = GBP_BUILD_PANEL (object);
 
   switch (prop_id)
     {
-    case PROP_RESULT:
-      gbp_build_panel_set_result (self, g_value_get_object (value));
+    case PROP_PIPELINE:
+      gbp_build_panel_set_pipeline (self, g_value_get_object (value));
       break;
 
     default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
 }
 
@@ -429,19 +475,19 @@ gbp_build_panel_class_init (GbpBuildPanelClass *klass)
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
+  widget_class->destroy = gbp_build_panel_destroy;
+
   object_class->get_property = gbp_build_panel_get_property;
   object_class->set_property = gbp_build_panel_set_property;
 
-  widget_class->destroy = gbp_build_panel_destroy;
-
-  properties [PROP_RESULT] =
-    g_param_spec_object ("result",
-                         "Result",
-                         "Result",
-                         IDE_TYPE_BUILD_RESULT,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
+                         NULL,
+                         NULL,
+                         IDE_TYPE_BUILD_PIPELINE,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/build-tools-plugin/gbp-build-panel.ui");
   gtk_widget_class_set_css_name (widget_class, "buildpanel");
@@ -468,25 +514,7 @@ gbp_build_panel_init (GbpBuildPanel *self)
 
   g_object_set (self, "title", _("Build"), NULL);
 
-  self->signals = egg_signal_group_new (IDE_TYPE_BUILD_RESULT);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "diagnostic",
-                                   G_CALLBACK (gbp_build_panel_diagnostic),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::running",
-                                   G_CALLBACK (gbp_build_panel_notify_running),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
-  egg_signal_group_connect_object (self->signals,
-                                   "notify::running-time",
-                                   G_CALLBACK (gbp_build_panel_notify_running_time),
-                                   self,
-                                   G_CONNECT_SWAPPED);
+  ide_widget_set_context_handler (self, gbp_build_panel_context_handler);
 
   g_signal_connect_object (self->diagnostics_tree_view,
                            "row-activated",
@@ -498,10 +526,4 @@ gbp_build_panel_init (GbpBuildPanel *self)
                                       GTK_CELL_RENDERER (self->diagnostics_text),
                                       gbp_build_panel_text_func,
                                       self, NULL);
-
-
-  self->bindings = egg_binding_group_new ();
-
-  egg_binding_group_bind (self->bindings, "mode", self->status_label, "label",
-                          G_BINDING_SYNC_CREATE);
 }
diff --git a/plugins/build-tools/gbp-build-panel.h b/plugins/build-tools/gbp-build-panel.h
index 489b49c..a3e61ca 100644
--- a/plugins/build-tools/gbp-build-panel.h
+++ b/plugins/build-tools/gbp-build-panel.h
@@ -28,9 +28,6 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpBuildPanel, gbp_build_panel, GBP, BUILD_PANEL, PnlDockWidget)
 
-void gbp_build_panel_set_result (GbpBuildPanel  *self,
-                                 IdeBuildResult *result);
-
 G_END_DECLS
 
 #endif /* GBP_BUILD_PANEL_H */
diff --git a/plugins/build-tools/gbp-build-tool.c b/plugins/build-tools/gbp-build-tool.c
index d63d396..231cf36 100644
--- a/plugins/build-tools/gbp-build-tool.c
+++ b/plugins/build-tools/gbp-build-tool.c
@@ -21,6 +21,7 @@
 #endif
 
 #include <glib/gi18n.h>
+#include <ide.h>
 
 #include "gbp-build-tool.h"
 
@@ -30,17 +31,15 @@ struct _GbpBuildTool
   gint64  build_start;
 };
 
-static gint                  parallel = -1;
-static IdeBuilderBuildFlags  flags;
-static gchar                *configuration_id;
-static gchar                *device_id;
-static gchar                *runtime_id;
+static gint   parallel = -1;
+static gchar *configuration_id;
+static gchar *device_id;
+static gchar *runtime_id;
 
 static void application_tool_init (IdeApplicationToolInterface *iface);
 
 G_DEFINE_TYPE_EXTENDED (GbpBuildTool, gbp_build_tool, G_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_TOOL,
-                                               application_tool_init))
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_TOOL, application_tool_init))
 
 static void
 gbp_build_tool_class_init (GbpBuildToolClass *klass)
@@ -53,19 +52,19 @@ gbp_build_tool_init (GbpBuildTool *self)
 }
 
 static void
-gbp_build_tool_log (GbpBuildTool      *self,
-                    IdeBuildResultLog  log,
-                    const gchar       *message,
-                    IdeBuildResult    *build_result)
+gbp_build_tool_log_observer (IdeBuildLogStream  stream,
+                             const gchar       *message,
+                             gssize             message_len,
+                             gpointer           user_data)
 {
-  if (log == IDE_BUILD_RESULT_LOG_STDERR)
+  if (stream == IDE_BUILD_LOG_STDERR)
     g_printerr ("%s", message);
   else
     g_print ("%s", message);
 }
 
 static void
-print_build_info (IdeContext *context,
+print_build_info (IdeContext       *context,
                   IdeConfiguration *configuration)
 {
   IdeProject *project;
@@ -116,35 +115,35 @@ print_build_info (IdeContext *context,
 }
 
 static void
-gbp_build_tool_build_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      user_data)
+gbp_build_tool_execute_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
 {
+  IdeBuildManager *build_manager = (IdeBuildManager *)object;
   g_autoptr(GTask) task = user_data;
-  g_autoptr(IdeBuildResult) build_result = NULL;
+  g_autoptr(GError) error = NULL;
   GbpBuildTool *self;
-  IdeBuilder *builder = (IdeBuilder *)object;
-  GError *error = NULL;
   guint64 completed_at;
   guint64 total_usec;
 
   g_assert (G_IS_TASK (task));
-  g_assert (IDE_IS_BUILDER (builder));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   self = g_task_get_source_object (task);
   completed_at = g_get_monotonic_time ();
-  build_result = ide_builder_build_finish (builder, result, &error);
+
+  ide_build_manager_execute_finish (build_manager, result, &error);
 
   total_usec = completed_at - self->build_start;
 
-  if (build_result == NULL)
+  if (error != NULL)
     {
       g_printerr (_("===============\n"));
       g_printerr (_(" Build Failure: %s\n"), error->message);
       g_printerr (_(" Build ran for: %"G_GUINT64_FORMAT".%"G_GUINT64_FORMAT" seconds\n"),
                   (total_usec / 1000000), ((total_usec % 1000000) / 1000));
       g_printerr (_("===============\n"));
-      g_task_return_error (task, error);
+      g_task_return_error (task, g_steal_pointer (&error));
       return;
     }
 
@@ -170,17 +169,16 @@ gbp_build_tool_new_context_cb (GObject      *object,
 {
   g_autoptr(GTask) task = user_data;
   g_autoptr(IdeContext) context = NULL;
-  g_autoptr(IdeBuilder) builder = NULL;
-  g_autoptr(IdeBuildResult) build_result = NULL;
   g_autoptr(IdeConfiguration) configuration = NULL;
   IdeConfigurationManager *configuration_manager;
-  IdeBuildSystem *build_system;
-  GbpBuildTool *self;
+  IdeBuildManager *build_manager;
+  IdeBuildPipeline *pipeline;
+  GCancellable *cancellable;
   GError *error = NULL;
 
   g_assert (G_IS_TASK (task));
 
-  self = g_task_get_source_object (task);
+  cancellable = g_task_get_cancellable (task);
 
   context = ide_context_new_finish (result, &error);
 
@@ -233,37 +231,18 @@ gbp_build_tool_new_context_cb (GObject      *object,
 
   print_build_info (context, configuration);
 
-  build_system = ide_context_get_build_system (context);
-  builder = ide_build_system_get_builder (build_system, configuration, &error);
+  build_manager = ide_context_get_build_manager (context);
 
-  if (builder == NULL)
-    {
-      g_task_return_error (task, error);
-      return;
-    }
-
-  self->build_start = g_get_monotonic_time ();
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+  ide_build_pipeline_add_log_observer (pipeline,
+                                       gbp_build_tool_log_observer,
+                                       NULL, NULL);
 
-  ide_builder_build_async (builder,
-                           flags,
-                           &build_result,
-                           g_task_get_cancellable (task),
-                           gbp_build_tool_build_cb,
-                           g_object_ref (task));
-
-  if (build_result != NULL)
-    {
-      /*
-       * XXX: Technically we could lose some log lines unless we
-       * guarantee that the build can't start until the main loop
-       * is reached. (Which is probably reasonable).
-       */
-      g_signal_connect_object (build_result,
-                               "log",
-                               G_CALLBACK (gbp_build_tool_log),
-                               g_task_get_source_object (task),
-                               G_CONNECT_SWAPPED);
-    }
+  ide_build_manager_execute_async (build_manager,
+                                   IDE_BUILD_PHASE_BUILD,
+                                   cancellable,
+                                   gbp_build_tool_execute_cb,
+                                   g_steal_pointer (&task));
 }
 
 static void
@@ -328,14 +307,13 @@ gbp_build_tool_run_async (IdeApplicationTool  *tool,
 
   if (clean)
     {
-      flags |= IDE_BUILDER_BUILD_FLAGS_FORCE_CLEAN;
-      flags |= IDE_BUILDER_BUILD_FLAGS_NO_BUILD;
+      /* TODO */
     }
 
   ide_context_new_async (project_file,
                          cancellable,
                          gbp_build_tool_new_context_cb,
-                         g_object_ref (task));
+                         g_steal_pointer (&task));
 }
 
 static gboolean
diff --git a/plugins/build-tools/gbp-build-workbench-addin.c b/plugins/build-tools/gbp-build-workbench-addin.c
index 8dae6b4..47f47ff 100644
--- a/plugins/build-tools/gbp-build-workbench-addin.c
+++ b/plugins/build-tools/gbp-build-workbench-addin.c
@@ -36,7 +36,7 @@ struct _GbpBuildWorkbenchAddin
   GbpBuildPerspective *build_perspective;
 
   /* Owned */
-  IdeBuildResult      *result;
+  IdeBuildPipeline    *pipeline;
   GSimpleActionGroup  *actions;
 };
 
@@ -47,25 +47,25 @@ G_DEFINE_TYPE_EXTENDED (GbpBuildWorkbenchAddin, gbp_build_workbench_addin, G_TYP
 
 enum {
   PROP_0,
-  PROP_RESULT,
+  PROP_PIPELINE,
   LAST_PROP
 };
 
 static GParamSpec *properties [LAST_PROP];
 
 static void
-gbp_build_workbench_addin_set_result (GbpBuildWorkbenchAddin *self,
-                                      IdeBuildResult         *result)
+gbp_build_workbench_addin_set_pipeline (GbpBuildWorkbenchAddin *self,
+                                        IdeBuildPipeline       *pipeline)
 {
   g_return_if_fail (GBP_IS_BUILD_WORKBENCH_ADDIN (self));
-  g_return_if_fail (!result || IDE_IS_BUILD_RESULT (result));
+  g_return_if_fail (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
   g_return_if_fail (self->workbench != NULL);
 
-  if (g_set_object (&self->result, result))
+  if (g_set_object (&self->pipeline, pipeline))
     {
-      gbp_build_log_panel_set_result (self->build_log_panel, result);
+      gbp_build_log_panel_set_pipeline (self->build_log_panel, pipeline);
       gtk_widget_show (GTK_WIDGET (self->build_log_panel));
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESULT]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PIPELINE]);
     }
 }
 
@@ -137,7 +137,7 @@ gbp_build_workbench_addin_load (IdeWorkbenchAddin *addin,
 
   g_signal_connect_object (build_manager,
                            "build-started",
-                           G_CALLBACK (gbp_build_workbench_addin_set_result),
+                           G_CALLBACK (gbp_build_workbench_addin_set_pipeline),
                            self,
                            G_CONNECT_SWAPPED);
 
@@ -158,7 +158,7 @@ gbp_build_workbench_addin_load (IdeWorkbenchAddin *addin,
   gtk_widget_insert_action_group (GTK_WIDGET (workbench), "build-tools",
                                   G_ACTION_GROUP (self->actions));
 
-  g_object_bind_property (self, "result", self->panel, "result", 0);
+  g_object_bind_property (self, "pipeline", self->panel, "pipeline", 0);
 
   self->build_perspective = g_object_new (GBP_TYPE_BUILD_PERSPECTIVE,
                                           "configuration-manager", configuration_manager,
@@ -194,8 +194,8 @@ gbp_build_workbench_addin_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_RESULT:
-      g_value_set_object (value, self->result);
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
       break;
 
     default:
@@ -209,7 +209,7 @@ gbp_build_workbench_addin_finalize (GObject *object)
   GbpBuildWorkbenchAddin *self = (GbpBuildWorkbenchAddin *)object;
 
   g_clear_object (&self->actions);
-  g_clear_object (&self->result);
+  g_clear_object (&self->pipeline);
 
   G_OBJECT_CLASS (gbp_build_workbench_addin_parent_class)->finalize (object);
 }
@@ -222,11 +222,11 @@ gbp_build_workbench_addin_class_init (GbpBuildWorkbenchAddinClass *klass)
   object_class->finalize = gbp_build_workbench_addin_finalize;
   object_class->get_property = gbp_build_workbench_addin_get_property;
 
-  properties [PROP_RESULT] =
-    g_param_spec_object ("result",
-                         "Result",
-                         "The current build result",
-                         IDE_TYPE_BUILD_RESULT,
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
+                         "Pipeline",
+                         "The current build pipeline",
+                         IDE_TYPE_BUILD_PIPELINE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
diff --git a/plugins/cargo/cargo_plugin.py b/plugins/cargo/cargo_plugin.py
index d42464f..c556262 100644
--- a/plugins/cargo/cargo_plugin.py
+++ b/plugins/cargo/cargo_plugin.py
@@ -29,7 +29,7 @@ from gi.repository import GLib
 from gi.repository import GObject
 from gi.repository import Ide
 
-_ = Ide.gettext
+_CARGO = 'cargo'
 
 class CargoBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     project_file = GObject.Property(type=Gio.File)
@@ -65,149 +65,62 @@ class CargoBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
         # Priority is used to determine the order of discovery
         return 2000
 
-    def do_get_builder(self, config):
-        return CargoBuilder(config, context=self.get_context())
-
-class CargoBuilder(Ide.Builder):
-    config = GObject.Property(type=Ide.Configuration)
-
-    def __init__(self, config, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.config = config
-
-    def do_build_async(self, flags, cancellable, callback, data):
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_result = CargoBuildResult(self.config, flags, context=self.get_context())
-
-        def wrap_execute():
-            try:
-                task.build_result.build()
-                task.build_result.set_mode(_('Successful'))
-                task.build_result.set_failed(False)
-                task.build_result.set_running(False)
-                task.return_boolean(True)
-            except Exception as ex:
-                task.build_result.set_mode(_("Failed"))
-                task.build_result.set_failed(True)
-                task.build_result.set_running(False)
-                task.return_error(ex)
-
-        thread = threading.Thread(target=wrap_execute)
-        thread.start()
-
-        return task.build_result
-
-    def do_build_finish(self, task):
-        if task.propagate_boolean():
-            return task.build_result
-
-    def do_install_async(self, cancellable, callback, data):
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_result = CargoBuildResult(self.config, 0, context=self.get_context())
-
-        def wrap_execute():
-            try:
-                task.build_result.install()
-                self = task.get_source_object()
-                task.build_result.set_mode(_('Successful'))
-                task.build_result.set_failed(False)
-                task.build_result.set_running(False)
-                task.return_boolean(True)
-            except Exception as ex:
-                task.build_result.set_mode(_("Failed"))
-                task.build_result.set_failed(True)
-                task.build_result.set_running(False)
-                task.return_error(ex)
-
-        thread = threading.Thread(target=wrap_execute)
-        thread.start()
-
-        return task.build_result
-
-    def do_install_finish(self, task):
-        if task.propagate_boolean():
-            return task.build_result
-
-    def do_get_build_flags_async(self, ifile, cancellable, callback, data):
-        # TODO:
-        # GTask sort of is painful from Python.
-        # We can use it to attach some data to return from the finish
-        # function though.
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_flags = []
-        task.return_boolean(True)
-
-    def do_get_build_flags_finish(self, result):
-        if task.propagate_boolean():
-            return result.build_flags
-
-    def do_get_build_targets_async(self, cancellable, callback, data):
-        # TODO: We need a way to figure out what "cargo run" will do so that
-        #       we can synthesize that as a build result.
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_targets = []
-        task.return_boolean(True)
-
-    def do_get_build_targets_finish(self, task):
-        if task.propagate_boolean():
-            return task.build_targets
-
-class CargoBuildResult(Ide.BuildResult):
-    runtime = GObject.Property(type=Ide.Runtime)
-
-    def __init__(self, config, flags, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+class CargoPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
+    """
+    The CargoPipelineAddin is responsible for creating the necessary build
+    stages and attaching them to phases of the build pipeline.
+    """
 
+    def do_load(self, pipeline):
         context = self.get_context()
-        vcs = context.get_vcs()
-
-        self.flags = flags
-        self.runtime = config.get_runtime()
-        self.directory = vcs.get_working_directory().get_path()
-
-        self.set_running(True)
-
-    def _run(self, mode, *args):
-        self.set_mode(mode)
-
-        if self.runtime is not None:
-            launcher = self.runtime.create_launcher()
-        else:
-            launcher = Ide.SubprocessLauncher()
-            launcher.set_run_on_host(True)
-            launcher.set_clear_environment(False)
-
-        launcher.set_cwd(self.directory)
-        launcher.push_argv('cargo')
-        for arg in args:
-            launcher.push_argv(arg)
-
-        subprocess = launcher.spawn()
-
-        self.log_subprocess(subprocess)
-
-        subprocess.wait_check()
-
-        return True
-
-    def build(self):
-        # Do a clean first if requested.
-        if self.flags & Ide.BuilderBuildFlags.FORCE_CLEAN != 0:
-            self._run(_("Cleaning…"), 'clean')
-
-        if self.flags & Ide.BuilderBuildFlags.NO_BUILD == 0:
-            self._run(_("Building…"), 'build', '--verbose')
-
-        self.set_mode(_('Successful'))
-        self.set_failed(False)
-        self.set_running(False)
-
-        return True
-
-    def install(self):
-        self.build()
-        self._run(_('Installing…'), 'install')
+        build_system = context.get_build_system()
+
+        # Ignore pipeline unless this is a cargo project
+        if type(build_system) != CargoBuildSystem:
+            return
+
+        cargo_toml = build_system.props.project_file.get_path()
+        config = pipeline.get_configuration()
+        system_type = config.get_device().get_system_type()
+        builddir = pipeline.get_builddir()
+
+        # Fetch dependencies so that we no longer need network access
+        fetch_launcher = pipeline.create_launcher()
+        fetch_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
+        fetch_launcher.push_argv(_CARGO)
+        fetch_launcher.push_argv('fetch')
+        fetch_launcher.push_argv('--manifest-path')
+        fetch_launcher.push_argv(cargo_toml)
+        self.track(pipeline.connect_launcher(Ide.BuildPhase.DOWNLOADS, 0, fetch_launcher))
+
+        # Fetch dependencies so that we no longer need network access
+        build_launcher = pipeline.create_launcher()
+        build_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
+        build_launcher.push_argv(_CARGO)
+        build_launcher.push_argv('build')
+        build_launcher.push_argv('--manifest-path')
+        build_launcher.push_argv(cargo_toml)
+        build_launcher.push_argv('--message-format')
+        build_launcher.push_argv('human')
+
+        if Ide.get_system_type() != system_type:
+            build_launcher.push_argv('--target')
+            build_launcher.push_argv(system_type)
+
+        if config.props.parallelism > 0:
+            build_launcher.push_argv('-j{}'.format(config.props.parallelism))
+
+        if not config.props.debug:
+            build_launcher.push_argv('--release')
+
+        clean_launcher = pipeline.create_launcher()
+        clean_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
+        clean_launcher.push_argv(_CARGO)
+        clean_launcher.push_argv('clean')
+        clean_launcher.push_argv('--manifest-path')
+        clean_launcher.push_argv(cargo_toml)
+
+        build_stage = Ide.BuildStageLauncher.new(context, build_launcher)
+        build_stage.set_clean_launcher(clean_launcher)
+        self.track(pipeline.connect(Ide.BuildPhase.BUILD, 0, build_stage))
 
-        self.set_mode(_('Successful'))
-        self.set_failed(False)
-        self.set_running(False)
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/configure.ac b/plugins/flatpak/configure.ac
index 6f6ce47..8064a93 100644
--- a/plugins/flatpak/configure.ac
+++ b/plugins/flatpak/configure.ac
@@ -1,4 +1,4 @@
-m4_define([flatpak_required_version], [0.6.9])
+m4_define([flatpak_required_version], [0.8.0])
 m4_define([ggit_required_version], [0.24.0])
 
 # --enable-flatpak-plugin=yes/no/auto
diff --git a/plugins/flatpak/gbp-flatpak-pipeline-addin.c b/plugins/flatpak/gbp-flatpak-pipeline-addin.c
new file mode 100644
index 0000000..9e4bbb9
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-pipeline-addin.c
@@ -0,0 +1,529 @@
+/* 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_BUILD_INIT,
+  PREPARE_REMOTES,
+};
+
+static IdeSubprocessLauncher *
+create_subprocess_launcher (void)
+{
+  IdeSubprocessLauncher *launcher;
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE |
+                                          G_SUBPROCESS_FLAGS_STDERR_PIPE);
+  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 void
+check_if_installed (IdeBuildStageTransfer   *stage,
+                    IdeBuildPipeline        *pipeline,
+                    GCancellable            *cancellable,
+                    GbpFlatpakTransfer      *transfer)
+{
+  gboolean installed;
+
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (stage));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (GBP_IS_FLATPAK_TRANSFER (transfer));
+
+  installed = gbp_flatpak_transfer_is_installed (transfer, cancellable);
+  ide_build_stage_set_completed (IDE_BUILD_STAGE (stage), installed);
+}
+
+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(GbpFlatpakTransfer) transfer = NULL;
+      const gchar *id = items[i];
+
+      if (id == NULL)
+        continue;
+
+      transfer = gbp_flatpak_transfer_new (context, id, NULL, branch, FALSE);
+
+      stage = g_object_new (IDE_TYPE_BUILD_STAGE_TRANSFER,
+                            "context", context,
+                            "transfer", transfer,
+                            NULL);
+
+      g_signal_connect_object (stage,
+                               "query",
+                               G_CALLBACK (check_if_installed),
+                               transfer,
+                               0);
+
+      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_TRACE_MSG ("%s::query checking for %s: %s",
+                 G_OBJECT_TYPE_NAME (stage),
+                 file_path,
+                 exists ? "yes" : "no");
+
+  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 *metadata_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");
+
+  if (platform == NULL && sdk == NULL)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "Platform and SDK are both NULL");
+      return FALSE;
+    }
+
+  if (platform == NULL)
+    platform = sdk;
+
+  if (sdk == NULL)
+    sdk = platform;
+
+  metadata_path = g_build_filename (staging_dir, "metadata", 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_LAUNCHER,
+                        "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 (&metadata_path),
+                         (GClosureNotify)g_free,
+                         0);
+
+  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_LAUNCHER,
+                        "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_LAUNCHER,
+                        "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,
+                         0);
+
+  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))
+    {
+      g_message ("Configuration is not using flatpak, ignoring pipeline");
+      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-plugin.c b/plugins/flatpak/gbp-flatpak-plugin.c
index fb9bb72..654ec4b 100644
--- a/plugins/flatpak/gbp-flatpak-plugin.c
+++ b/plugins/flatpak/gbp-flatpak-plugin.c
@@ -22,6 +22,7 @@
 #include "gbp-flatpak-runtime-provider.h"
 #include "gbp-flatpak-application-addin.h"
 #include "gbp-flatpak-genesis-addin.h"
+#include "gbp-flatpak-pipeline-addin.h"
 
 void
 peas_register_types (PeasObjectModule *module)
@@ -35,4 +36,7 @@ peas_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_GENESIS_ADDIN,
                                               GBP_TYPE_FLATPAK_GENESIS_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              GBP_TYPE_FLATPAK_PIPELINE_ADDIN);
 }
diff --git a/plugins/flatpak/gbp-flatpak-runtime.c b/plugins/flatpak/gbp-flatpak-runtime.c
index c65e1d6..a83aa95 100644
--- a/plugins/flatpak/gbp-flatpak-runtime.c
+++ b/plugins/flatpak/gbp-flatpak-runtime.c
@@ -56,7 +56,7 @@ enum {
 static GParamSpec *properties [N_PROPS];
 
 static gchar *
-get_build_directory (GbpFlatpakRuntime *self)
+get_staging_directory (GbpFlatpakRuntime *self)
 {
   IdeContext *context;
   IdeProject *project;
@@ -68,9 +68,9 @@ get_build_directory (GbpFlatpakRuntime *self)
 
   return g_build_filename (g_get_user_cache_dir (),
                            "gnome-builder",
-                           "builds",
-                           ide_project_get_id (project),
                            "flatpak",
+                           "staging",
+                           ide_project_get_id (project),
                            ide_runtime_get_id (IDE_RUNTIME (self)),
                            NULL);
 }
@@ -80,691 +80,49 @@ gbp_flatpak_runtime_contains_program_in_path (IdeRuntime   *runtime,
                                               const gchar  *program,
                                               GCancellable *cancellable)
 {
+  GbpFlatpakRuntime *self = (GbpFlatpakRuntime *)runtime;
   g_autoptr(IdeSubprocessLauncher) launcher = NULL;
   g_autoptr(IdeSubprocess) subprocess = NULL;
 
-  g_assert (IDE_IS_RUNTIME (runtime));
-  g_assert (program != NULL);
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  launcher = ide_runtime_create_launcher (runtime, 0);
-
-  ide_subprocess_launcher_push_argv (launcher, "which");
-  ide_subprocess_launcher_push_argv (launcher, program);
-
-  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, NULL);
-
-  return (subprocess != NULL) && ide_subprocess_wait_check (subprocess, cancellable, NULL);
-}
-
-/**
- * manifest_has_multiple_modules:
- *
- * Searches a #JsonObject to see if it has more than one
- * element in a "modules" list.
- */
-static gboolean
-manifest_has_multiple_modules (JsonObject *object)
-{
-  JsonArray *modules;
-  guint num_modules;
-
-  modules = json_object_get_array_member (object, "modules");
-  if (modules == NULL)
-    return FALSE;
-
-  num_modules = json_array_get_length (modules);
-  if (num_modules > 1)
-      return TRUE;
-  else if (num_modules == 0)
-      return FALSE;
-  else
-    {
-      JsonNode *module;
-      module = json_array_get_element (modules, 0);
-      if (JSON_NODE_HOLDS_OBJECT (module))
-        {
-          object = json_node_get_object (module);
-          if (json_object_has_member (object, "modules"))
-            {
-              modules = json_object_get_array_member (object, "modules");
-              if (modules == NULL)
-                return FALSE;
-              return (json_array_get_length (modules) > 0);
-            }
-        }
-      return FALSE;
-    }
-}
-
-static void
-gbp_flatpak_runtime_prebuild_worker (GTask        *task,
-                                     gpointer      source_object,
-                                     gpointer      task_data,
-                                     GCancellable *cancellable)
-{
-  GbpFlatpakRuntime *self = source_object;
-  IdeBuildResult *build_result = (IdeBuildResult *)task_data;
-  IdeContext *context;
-  IdeConfigurationManager *config_manager;
-  IdeConfiguration *configuration;
-  IdeRuntimeManager *runtime_manager;
-  const gchar *flatpak_repo_name = NULL;
-  gboolean already_ran_build_init = FALSE;
-  g_autofree gchar *build_path = NULL;
-  g_autofree gchar *flatpak_repo_path = NULL;
-  g_autofree gchar *metadata_path = NULL;
-  g_autoptr(GFile) build_dir = NULL;
-  g_autoptr(GFile) flatpak_repo_dir = NULL;
-  g_autoptr(GFile) metadata_file = NULL;
-  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-  g_autoptr(IdeSubprocess) process = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (G_IS_TASK (task));
   g_assert (GBP_IS_FLATPAK_RUNTIME (self));
+  g_assert (program != NULL);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
-
-  build_path = get_build_directory (self);
-  build_dir = g_file_new_for_path (build_path);
-
-  if (!g_file_query_exists (build_dir, cancellable))
-    {
-      if (!g_file_make_directory_with_parents (build_dir, cancellable, &error))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-    }
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
-  configuration = ide_configuration_manager_get_current (config_manager);
-  runtime_manager = ide_context_get_runtime_manager (context);
-
-  g_assert (IDE_IS_CONFIGURATION (configuration));
-  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
-
-  /* Make sure there's a local flatpak repo we can use to export the build */
-  flatpak_repo_path = g_build_filename (g_get_user_cache_dir (),
-                                        "gnome-builder",
-                                        "flatpak-repo",
-                                        NULL);
-  flatpak_repo_dir = g_file_new_for_path (flatpak_repo_path);
-  if (!g_file_query_exists (flatpak_repo_dir, cancellable))
-    {
-      if (!g_file_make_directory_with_parents (flatpak_repo_dir, cancellable, &error))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-    }
-
-  launcher = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-  if (launcher == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  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, "--no-gpg-verify");
-  ide_subprocess_launcher_push_argv (launcher, "--if-not-exists");
-  flatpak_repo_name = ide_configuration_get_internal_string (configuration, "flatpak-repo-name");
-  g_assert (!ide_str_empty0 (flatpak_repo_name));
-  ide_subprocess_launcher_push_argv (launcher, flatpak_repo_name);
-  ide_subprocess_launcher_push_argv (launcher, flatpak_repo_path);
-  process = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
-
-  if (process == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_build_result_log_subprocess (build_result, process);
-  if (!ide_subprocess_wait_check (process, cancellable, &error))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-
-  ide_configuration_set_internal_object (configuration, "flatpak-repo-dir", flatpak_repo_dir);
-
-  /* Check if flatpak build-init has been run by checking for the metadata file */
-  metadata_path = g_build_filename (build_path, "metadata", NULL);
-  metadata_file = g_file_new_for_path (metadata_path);
-  g_assert (metadata_file != NULL);
-  if (g_file_query_exists (metadata_file, cancellable))
-    already_ran_build_init = TRUE;
 
   /*
-   * Install the runtime and sdk if they're just the standard gnome ones,
-   * and run flatpak-builder if necessary.
+   * To check if a program is available, we don't want to use the normal
+   * launcher because it will only be available if the build directory
+   * has been created and setup. Instead, we will use flatpak to run the
+   * runtime which was added in Flatpak 0.6.13.
    */
-  if (self->manifest != NULL)
-    {
-      gchar *manifest_path;
-      g_autoptr(JsonParser) parser = NULL;
-      JsonNode *root_node = NULL;
-      JsonObject *root_object = NULL;
-      gboolean has_multiple_modules;
-
-      manifest_path = g_file_get_path (self->manifest);
-      g_assert (!ide_str_empty0 (manifest_path));
-
-      parser = json_parser_new ();
-      if (!json_parser_load_from_file (parser, manifest_path, &error))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-      root_node = json_parser_get_root (parser);
-      g_assert (JSON_NODE_HOLDS_OBJECT (root_node));
-      root_object = json_node_get_object (root_node);
-      has_multiple_modules = manifest_has_multiple_modules (root_object);
-
-      if (g_strcmp0 (self->platform, "org.gnome.Platform") == 0 ||
-          g_strcmp0 (self->sdk, "org.gnome.Sdk") == 0)
-        {
-          gchar *gnome_repo_name = NULL;
-          gchar *gnome_repo_path = NULL;
-          const gchar *arch = NULL;
-          g_autofree gchar *runtime_id = NULL;
-          g_autofree gchar *sdk_id = NULL;
-          IdeRuntime *runtime;
-          IdeRuntime *sdk;
-
-          arch = flatpak_get_default_arch ();
-
-          runtime_id = g_strdup_printf ("flatpak:%s/%s/%s", self->platform, self->branch, arch);
-          sdk_id = g_strdup_printf ("flatpak:%s/%s/%s", self->sdk, self->branch, arch);
-          runtime = ide_runtime_manager_get_runtime (runtime_manager, runtime_id);
-          sdk = ide_runtime_manager_get_runtime (runtime_manager, sdk_id);
-
-          /* Add the gnome or gnome-nightly remote */
-          if (runtime == NULL || sdk == NULL)
-            {
-              g_autoptr(IdeSubprocessLauncher) launcher2 = NULL;
-              g_autoptr(IdeSubprocess) process2 = NULL;
-
-              if (g_strcmp0 (self->branch, "master") == 0)
-                {
-                  gnome_repo_name = "gnome-nightly";
-                  gnome_repo_path = "https://sdk.gnome.org/gnome-nightly.flatpakrepo";;
-                }
-              else
-                {
-                  gnome_repo_name = "gnome";
-                  gnome_repo_path = "https://sdk.gnome.org/gnome.flatpakrepo";;
-                }
-
-              launcher2 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME 
(self), &error);
-              if (launcher2 == NULL)
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-              ide_subprocess_launcher_push_argv (launcher2, "flatpak");
-              ide_subprocess_launcher_push_argv (launcher2, "remote-add");
-              ide_subprocess_launcher_push_argv (launcher2, "--user");
-              ide_subprocess_launcher_push_argv (launcher2, "--if-not-exists");
-              ide_subprocess_launcher_push_argv (launcher2, "--from");
-              ide_subprocess_launcher_push_argv (launcher2, gnome_repo_name);
-              ide_subprocess_launcher_push_argv (launcher2, gnome_repo_path);
-              ide_build_result_log_stderr (build_result,
-                                           "Adding missing flatpak repository %s from %s\n",
-                                           gnome_repo_name, gnome_repo_path);
-              process2 = ide_subprocess_launcher_spawn (launcher2, cancellable, &error);
-
-              if (process2 != NULL)
-                ide_build_result_log_subprocess (build_result, process2);
-
-              if (process2 == NULL || !ide_subprocess_wait_check (process2, cancellable, &error))
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-            }
-
-          /* Install the runtime */
-          if (runtime == NULL && g_strcmp0 (self->platform, "org.gnome.Platform") == 0)
-            {
-              g_autoptr(IdeSubprocessLauncher) launcher3 = NULL;
-              g_autoptr(IdeSubprocess) process3 = NULL;
-
-              launcher3 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME 
(self), &error);
-              if (launcher3 == NULL)
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-              ide_subprocess_launcher_push_argv (launcher3, "flatpak");
-              ide_subprocess_launcher_push_argv (launcher3, "install");
-              ide_subprocess_launcher_push_argv (launcher3, "--user");
-              ide_subprocess_launcher_push_argv (launcher3, "--runtime");
-              ide_subprocess_launcher_push_argv (launcher3, gnome_repo_name);
-              ide_subprocess_launcher_push_argv (launcher3, self->platform);
-              ide_subprocess_launcher_push_argv (launcher3, self->branch);
-              ide_build_result_log_stderr (build_result,
-                                           "Installing missing flatpak runtime %s (%s)\n",
-                                           self->platform, self->branch);
-              process3 = ide_subprocess_launcher_spawn (launcher3, cancellable, &error);
-
-              if (process3 != NULL)
-                ide_build_result_log_subprocess (build_result, process3);
-
-              if (process3 == NULL || !ide_subprocess_wait_check (process3, cancellable, &error))
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-            }
-
-          /* Install the sdk */
-          if (sdk == NULL && g_strcmp0 (self->sdk, "org.gnome.Sdk") == 0)
-            {
-              g_autoptr(IdeSubprocessLauncher) launcher4 = NULL;
-              g_autoptr(IdeSubprocess) process4 = NULL;
-
-              launcher4 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME 
(self), &error);
-              if (launcher4 == NULL)
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-              ide_subprocess_launcher_push_argv (launcher4, "flatpak");
-              ide_subprocess_launcher_push_argv (launcher4, "install");
-              ide_subprocess_launcher_push_argv (launcher4, "--user");
-              ide_subprocess_launcher_push_argv (launcher4, "--runtime");
-              ide_subprocess_launcher_push_argv (launcher4, gnome_repo_name);
-              ide_subprocess_launcher_push_argv (launcher4, self->sdk);
-              ide_subprocess_launcher_push_argv (launcher4, self->branch);
-              ide_build_result_log_stderr (build_result,
-                                           "Installing missing flatpak SDK %s (%s)\n",
-                                           self->sdk, self->branch);
-              process4 = ide_subprocess_launcher_spawn (launcher4, cancellable, &error);
-
-              if (process4 != NULL)
-                ide_build_result_log_subprocess (build_result, process4);
-
-              if (process4 == NULL || !ide_subprocess_wait_check (process4, cancellable, &error))
-                {
-                  g_task_return_error (task, g_steal_pointer (&error));
-                  return;
-                }
-            }
-        }
-
-      /* No need to run flatpak-builder if there are no dependencies */
-      if (has_multiple_modules)
-        {
-          g_autoptr(IdeSubprocessLauncher) launcher5 = NULL;
-          g_autoptr(IdeSubprocess) process5 = NULL;
-          g_autoptr(GFile) success_file = NULL;
-          g_autofree gchar *stop_at_option = NULL;
-          g_autofree gchar *success_filename = NULL;
-
-          success_filename = g_build_filename (build_path, "flatpak-builder-success", NULL);
-          success_file = g_file_new_for_path (success_filename);
-          if (g_file_query_exists (success_file, cancellable))
-            {
-              g_task_return_boolean (task, TRUE);
-              return;
-            }
-
-          /* Run flatpak-builder to build just the dependencies */
-          launcher5 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME 
(self), &error);
-          if (launcher5 == NULL)
-            {
-              g_task_return_error (task, g_steal_pointer (&error));
-              return;
-            }
-          ide_subprocess_launcher_push_argv (launcher5, "flatpak-builder");
-          ide_subprocess_launcher_push_argv (launcher5, "--ccache");
-          ide_subprocess_launcher_push_argv (launcher5, "--force-clean");
-          stop_at_option = g_strdup_printf ("--stop-at=%s", self->primary_module);
-          ide_subprocess_launcher_push_argv (launcher5, stop_at_option);
-          ide_subprocess_launcher_push_argv (launcher5, build_path);
-          ide_subprocess_launcher_push_argv (launcher5, manifest_path);
-          process5 = ide_subprocess_launcher_spawn (launcher5, cancellable, &error);
-
-          if (process5 == NULL)
-            {
-              g_task_return_error (task, g_steal_pointer (&error));
-              return;
-            }
-          ide_build_result_log_subprocess (build_result, process5);
-          if (!ide_subprocess_wait_check (process5, cancellable, &error))
-            {
-              g_task_return_error (task, g_steal_pointer (&error));
-              return;
-            }
-
-          /*
-           * Make a file indicating that flatpak-builder finished successfully,
-           * so we know whether to run it for the next build.
-           */
-          g_object_unref (g_file_create (success_file, 0, cancellable, NULL));
-
-          g_task_return_boolean (task, TRUE);
-          return;
-        }
-    }
-
-  /* Run flatpak build-init */
-  if (!already_ran_build_init)
-    {
-      const gchar *app_id = NULL;
-      g_autoptr(IdeSubprocessLauncher) launcher6 = NULL;
-      g_autoptr(IdeSubprocess) process6 = NULL;
-
-      app_id = self->app_id;
-      if (ide_str_empty0 (app_id))
-        {
-          g_warning ("Could not determine application ID");
-          app_id = "org.gnome.FlatpakApp";
-        }
-
-      launcher6 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-      if (launcher6 == NULL)
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-      ide_subprocess_launcher_push_argv (launcher6, "flatpak");
-      ide_subprocess_launcher_push_argv (launcher6, "build-init");
-      ide_subprocess_launcher_push_argv (launcher6, build_path);
-      ide_subprocess_launcher_push_argv (launcher6, app_id);
-      ide_subprocess_launcher_push_argv (launcher6, self->sdk);
-      ide_subprocess_launcher_push_argv (launcher6, self->platform);
-      ide_subprocess_launcher_push_argv (launcher6, self->branch);
-      process6 = ide_subprocess_launcher_spawn (launcher6, cancellable, &error);
-
-      if (process6 == NULL)
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-      ide_build_result_log_subprocess (build_result, process6);
-      if (!ide_subprocess_wait_check (process6, cancellable, &error))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-    }
-
-  g_task_return_boolean (task, TRUE);
-}
-
-static void
-gbp_flatpak_runtime_prebuild_async (IdeRuntime          *runtime,
-                                    IdeBuildResult      *build_result,
-                                    GCancellable        *cancellable,
-                                    GAsyncReadyCallback  callback,
-                                    gpointer             user_data)
-{
-  GbpFlatpakRuntime *self = (GbpFlatpakRuntime *)runtime;
-  g_autoptr(GTask) task = NULL;
-
-  g_assert (GBP_IS_FLATPAK_RUNTIME (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_task_data (task, g_object_ref (build_result), (GDestroyNotify)g_object_unref);
-  g_task_run_in_thread (task, gbp_flatpak_runtime_prebuild_worker);
-}
-
-static gboolean
-gbp_flatpak_runtime_prebuild_finish (IdeRuntime    *runtime,
-                                     GAsyncResult  *result,
-                                     GError       **error)
-{
-  GbpFlatpakRuntime *self = (GbpFlatpakRuntime *)runtime;
-
-  g_assert (GBP_IS_FLATPAK_RUNTIME (self));
-  g_assert (G_IS_TASK (result));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-gbp_flatpak_runtime_postinstall_worker (GTask        *task,
-                                        gpointer      source_object,
-                                        gpointer      task_data,
-                                        GCancellable *cancellable)
-{
-  GbpFlatpakRuntime *self = source_object;
-  IdeBuildResult *build_result = (IdeBuildResult *)task_data;
-  IdeContext *context;
-  IdeConfigurationManager *config_manager;
-  IdeConfiguration *configuration;
-  const gchar *repo_name = NULL;
-  const gchar *app_id = NULL;
-  g_autofree gchar *repo_path = NULL;
-  g_autofree gchar *build_path = NULL;
-  g_autofree gchar *manifest_path = NULL;
-  g_autofree gchar *export_path = NULL;
-  g_autoptr(GFile) export_dir = NULL;
-  g_autoptr(IdeSubprocessLauncher) launcher2 = NULL;
-  g_autoptr(IdeSubprocessLauncher) launcher3 = NULL;
-  g_autoptr(IdeSubprocessLauncher) launcher4 = NULL;
-  g_autoptr(IdeSubprocess) process2 = NULL;
-  g_autoptr(IdeSubprocess) process3 = NULL;
-  g_autoptr(IdeSubprocess) process4 = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (G_IS_TASK (task));
-  g_assert (GBP_IS_FLATPAK_RUNTIME (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
-  configuration = ide_configuration_manager_get_current (config_manager);
-
-  build_path = get_build_directory (self);
-  repo_name = ide_configuration_get_internal_string (configuration, "flatpak-repo-name");
-  repo_path = g_file_get_path (ide_configuration_get_internal_object (configuration, "flatpak-repo-dir"));
 
-  g_assert (!ide_str_empty0 (repo_name));
-  g_assert (!ide_str_empty0 (repo_path));
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
 
-  /* Check if flatpak build-finish has already been run by checking for the export directory */
-  export_path = g_build_filename (build_path, "export", NULL);
-  export_dir = g_file_new_for_path (export_path);
-  g_assert (export_dir != NULL);
-  if (!g_file_query_exists (export_dir, cancellable))
-    {
-      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
-      g_autoptr(IdeSubprocess) process = NULL;
-      JsonArray *finish_args = NULL;
-      const gchar *command = NULL;
-      JsonParser *parser = NULL;
-
-      /* Attempt to parse the flatpak manifest */
-      if (self->manifest != NULL && (manifest_path = g_file_get_path (self->manifest)))
-        {
-          GError *json_error = NULL;
-          JsonObject *root_object;
+  ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
 
-          parser = json_parser_new ();
-          json_parser_load_from_file (parser, manifest_path, &json_error);
-          if (json_error)
-            g_debug ("Error parsing flatpak manifest %s: %s", manifest_path, json_error->message);
-          else
-            {
-              root_object = json_node_get_object (json_parser_get_root (parser));
-              if (root_object != NULL)
-                {
-                  if (json_object_has_member (root_object, "command"))
-                    command = json_object_get_string_member (root_object, "command");
-                  if (json_object_has_member (root_object, "finish-args"))
-                    finish_args = json_object_get_array_member (root_object, "finish-args");
-                }
-            }
-        }
-
-      /* Finalize the build directory */
-      launcher = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-      if (launcher == NULL)
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-      ide_subprocess_launcher_push_argv (launcher, "flatpak");
-      ide_subprocess_launcher_push_argv (launcher, "build-finish");
-      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);
-        }
-      if (finish_args != NULL)
-        {
-          for (guint i = 0; i < json_array_get_length (finish_args); i++)
-            {
-              const gchar *arg;
-              arg = json_array_get_string_element (finish_args, i);
-              if (!ide_str_empty0 (arg))
-                ide_subprocess_launcher_push_argv (launcher, arg);
-            }
-        }
-      ide_subprocess_launcher_push_argv (launcher, build_path);
-
-      g_clear_object (&parser);
-
-      process = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
-
-      if (process == NULL)
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-      ide_build_result_log_subprocess (build_result, process);
-      if (!ide_subprocess_wait_check (process, cancellable, &error))
-        {
-          g_task_return_error (task, g_steal_pointer (&error));
-          return;
-        }
-    }
-
-  /* Export the build to the repo */
-  launcher2 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-  if (launcher2 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_subprocess_launcher_push_argv (launcher2, "flatpak");
-  ide_subprocess_launcher_push_argv (launcher2, "build-export");
-  ide_subprocess_launcher_push_argv (launcher2, "--subject=\"Development build\"");
-  ide_subprocess_launcher_push_argv (launcher2, repo_path);
-  ide_subprocess_launcher_push_argv (launcher2, build_path);
-  process2 = ide_subprocess_launcher_spawn (launcher2, cancellable, &error);
-
-  if (process2 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_build_result_log_subprocess (build_result, process2);
-  if (!ide_subprocess_wait_check (process2, cancellable, &error))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-
-  app_id = self->app_id;
-  if (ide_str_empty0 (app_id))
-    {
-      g_warning ("Could not determine application ID");
-      app_id = "org.gnome.FlatpakApp";
-    }
-  /* Try to uninstall it in case this isn't the first run */
-  launcher3 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-  if (launcher3 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_subprocess_launcher_push_argv (launcher3, "flatpak");
-  ide_subprocess_launcher_push_argv (launcher3, "uninstall");
-  ide_subprocess_launcher_push_argv (launcher3, "--user");
-  ide_subprocess_launcher_push_argv (launcher3, app_id);
-  process3 = ide_subprocess_launcher_spawn (launcher3, cancellable, &error);
-
-  if (process3 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_subprocess_wait (process3, cancellable, NULL);
-
-  /* Finally install the app */
-  launcher4 = IDE_RUNTIME_CLASS (gbp_flatpak_runtime_parent_class)->create_launcher (IDE_RUNTIME (self), 
&error);
-  if (launcher4 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_subprocess_launcher_push_argv (launcher4, "flatpak");
-  ide_subprocess_launcher_push_argv (launcher4, "install");
-  ide_subprocess_launcher_push_argv (launcher4, "--user");
-  ide_subprocess_launcher_push_argv (launcher4, "--app");
-  ide_subprocess_launcher_push_argv (launcher4, "--no-deps");
-  ide_subprocess_launcher_push_argv (launcher4, repo_name);
-  ide_subprocess_launcher_push_argv (launcher4, app_id);
-
-  process4 = ide_subprocess_launcher_spawn (launcher4, cancellable, &error);
-
-  if (process4 == NULL)
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-  ide_build_result_log_subprocess (build_result, process4);
-  if (!ide_subprocess_wait_check (process4, cancellable, &error))
-    {
-      g_task_return_error (task, g_steal_pointer (&error));
-      return;
-    }
-
-  g_task_return_boolean (task, TRUE);
-}
+  ide_subprocess_launcher_push_argv (launcher, "flatpak");
+  ide_subprocess_launcher_push_argv (launcher, "run");
+  ide_subprocess_launcher_push_argv (launcher, "--command=which");
+  ide_subprocess_launcher_push_argv (launcher, self->sdk);
+  ide_subprocess_launcher_push_argv (launcher, program);
 
-static void
-gbp_flatpak_runtime_postinstall_async (IdeRuntime          *runtime,
-                                       IdeBuildResult      *build_result,
-                                       GCancellable        *cancellable,
-                                       GAsyncReadyCallback  callback,
-                                       gpointer             user_data)
-{
-  GbpFlatpakRuntime *self = (GbpFlatpakRuntime *)runtime;
-  g_autoptr(GTask) task = NULL;
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, NULL);
 
-  g_assert (GBP_IS_FLATPAK_RUNTIME (self));
-  g_assert (IDE_IS_BUILD_RESULT (build_result));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  if (subprocess != NULL)
+    return ide_subprocess_wait_check (subprocess, cancellable, NULL);
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_task_data (task, g_object_ref (build_result), (GDestroyNotify)g_object_unref);
-  g_task_run_in_thread (task, gbp_flatpak_runtime_postinstall_worker);
+  return FALSE;
 }
 
-static gboolean
-gbp_flatpak_runtime_postinstall_finish (IdeRuntime    *runtime,
-                                        GAsyncResult  *result,
-                                        GError       **error)
+static const gchar *
+get_builddir (GbpFlatpakRuntime *self)
 {
-  GbpFlatpakRuntime *self = (GbpFlatpakRuntime *)runtime;
+  IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+  IdeBuildManager *build_manager = ide_context_get_build_manager (context);
+  IdeBuildPipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
+  const gchar *builddir = ide_build_pipeline_get_builddir (pipeline);
 
-  g_assert (GBP_IS_FLATPAK_RUNTIME (self));
-  g_assert (G_IS_TASK (result));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
+  return builddir;
 }
 
 static IdeSubprocessLauncher *
@@ -780,10 +138,11 @@ gbp_flatpak_runtime_create_launcher (IdeRuntime  *runtime,
 
   if (ret != NULL)
     {
-      g_autofree gchar *build_path = NULL;
       g_autofree gchar *manifest_path = NULL;
+      g_autofree gchar *build_path = NULL;
       gchar *project_path;
       gchar *project_name;
+      const gchar *builddir = NULL;
       const gchar *cflags = NULL;
       const gchar *cxxflags = NULL;
       JsonObject *env_vars = NULL;
@@ -792,7 +151,8 @@ gbp_flatpak_runtime_create_launcher (IdeRuntime  *runtime,
       IdeContext *context;
       GFile *project_file;
 
-      build_path = get_build_directory (self);
+      build_path = get_staging_directory (self);
+      builddir = get_builddir (self);
 
       /* Attempt to parse the flatpak manifest */
       if (self->manifest != NULL && (manifest_path = g_file_get_path (self->manifest)))
@@ -850,7 +210,7 @@ gbp_flatpak_runtime_create_launcher (IdeRuntime  *runtime,
           g_autofree gchar *bind_mount_option = NULL;
           g_autofree gchar *build_dir_option = NULL;
           filesystem_option = g_strdup_printf ("--filesystem=%s", project_path);
-          bind_mount_option = g_strdup_printf ("--bind-mount=/run/build/%s=%s", project_name, project_path);
+          bind_mount_option = g_strdup_printf ("--bind-mount=/run/build/%s=%s", project_name, builddir);
           build_dir_option = g_strdup_printf ("--build-dir=/run/build/%s", project_name);
           ide_subprocess_launcher_push_argv (ret, "--nofilesystem=host");
           ide_subprocess_launcher_push_argv (ret, filesystem_option);
@@ -958,6 +318,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 +329,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
@@ -1023,7 +453,7 @@ gbp_flatpak_runtime_translate_file (IdeRuntime *runtime,
   if (g_str_has_prefix (path, "/usr/"))
     return g_file_get_child (self->deploy_dir_files, path + IDE_LITERAL_LENGTH ("/usr/"));
 
-  build_dir = get_build_directory (self);
+  build_dir = get_staging_directory (self);
   app_files_path = g_build_filename (build_dir, "files", NULL);
 
   if (g_str_equal (path, "/app"))
@@ -1155,10 +585,6 @@ gbp_flatpak_runtime_class_init (GbpFlatpakRuntimeClass *klass)
   object_class->get_property = gbp_flatpak_runtime_get_property;
   object_class->set_property = gbp_flatpak_runtime_set_property;
 
-  runtime_class->prebuild_async = gbp_flatpak_runtime_prebuild_async;
-  runtime_class->prebuild_finish = gbp_flatpak_runtime_prebuild_finish;
-  runtime_class->postinstall_async = gbp_flatpak_runtime_postinstall_async;
-  runtime_class->postinstall_finish = gbp_flatpak_runtime_postinstall_finish;
   runtime_class->create_launcher = gbp_flatpak_runtime_create_launcher;
   runtime_class->create_runner = gbp_flatpak_runtime_create_runner;
   runtime_class->contains_program_in_path = gbp_flatpak_runtime_contains_program_in_path;
diff --git a/plugins/flatpak/gbp-flatpak-transfer.c b/plugins/flatpak/gbp-flatpak-transfer.c
new file mode 100644
index 0000000..498f827
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-transfer.c
@@ -0,0 +1,555 @@
+/* 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;
+
+  IDE_ENTRY;
+
+  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));
+
+  g_debug ("Installing %s/%s/%s from remote %s",
+           self->id, self->arch, self->branch,
+           flatpak_remote_get_name (remote));
+
+  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);
+
+  IDE_TRACE_MSG ("ref = %p", ref);
+
+  if (ref != NULL)
+    g_debug ("%s/%s/%s was installed from remote %s",
+             self->id, self->arch, self->branch,
+             flatpak_remote_get_name (remote));
+
+  IDE_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;
+
+  IDE_ENTRY;
+
+  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));
+          IDE_EXIT;
+        }
+
+      for (guint j = 0; j < refs->len; j++)
+        {
+          FlatpakInstalledRef *ref = g_ptr_array_index (refs, j);
+          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));
+
+          IDE_TRACE_MSG ("Found %s/%s/%s installed in installation[%u]",
+                         id, arch, branch, i);
+
+          if (g_strcmp0 (self->id, id) == 0 &&
+              g_strcmp0 (self->branch, branch) == 0 &&
+              g_strcmp0 (self->arch, arch) == 0)
+            {
+              if (!self->force_update)
+                {
+                  IDE_TRACE_MSG ("Force update unset, considering transfer complete");
+                  g_task_return_boolean (task, TRUE);
+                  IDE_EXIT;
+                }
+
+              if (!update_installation (self, installation, cancellable, &error))
+                g_task_return_error (task, g_steal_pointer (&error));
+              else
+                g_task_return_boolean (task, TRUE);
+
+              IDE_EXIT;
+            }
+        }
+    }
+
+  /*
+   * 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.
+   */
+  g_debug ("%s was not found, locating within remote", self->id);
+
+  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));
+          IDE_EXIT;
+        }
+
+      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));
+              IDE_EXIT;
+            }
+
+          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));
+
+                  IDE_EXIT;
+                }
+            }
+        }
+    }
+
+  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);
+
+  IDE_EXIT;
+}
+
+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);
+
+  self->arch = g_strdup (flatpak_get_default_arch ());
+  self->branch = g_strdup ("master");
+}
+
+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);
+}
+
+gboolean
+gbp_flatpak_transfer_is_installed (GbpFlatpakTransfer *self,
+                                   GCancellable       *cancellable)
+{
+  FlatpakInstallation *installations[2] = { NULL };
+  g_autoptr(FlatpakInstallation) user = NULL;
+  g_autoptr(FlatpakInstallation) system = NULL;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (GBP_IS_FLATPAK_TRANSFER (self), FALSE);
+
+  installations[0] = user = flatpak_installation_new_user (cancellable, NULL);
+  installations[1] = system = flatpak_installation_new_system (cancellable, NULL);
+
+  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 (refs == NULL)
+        continue;
+
+      for (guint j = 0; j < refs->len; j++)
+        {
+          FlatpakInstalledRef *ref = g_ptr_array_index (refs, j);
+          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)
+            return TRUE;
+        }
+    }
+
+  return FALSE;
+}
diff --git a/plugins/flatpak/gbp-flatpak-transfer.h b/plugins/flatpak/gbp-flatpak-transfer.h
new file mode 100644
index 0000000..f7d4847
--- /dev/null
+++ b/plugins/flatpak/gbp-flatpak-transfer.h
@@ -0,0 +1,40 @@
+/* gbp-flatpak-transfer.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 GBP_FLATPAK_TRANSFER_H
+#define GBP_FLATPAK_TRANSFER_H
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_FLATPAK_TRANSFER (gbp_flatpak_transfer_get_type())
+
+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);
+gboolean            gbp_flatpak_transfer_is_installed (GbpFlatpakTransfer *self,
+                                                       GCancellable       *cancellable);
+
+G_END_DECLS
+
+#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-pipeline-addin.c b/plugins/gcc/gbp-gcc-pipeline-addin.c
new file mode 100644
index 0000000..d73b68f
--- /dev/null
+++ b/plugins/gcc/gbp-gcc-pipeline-addin.c
@@ -0,0 +1,74 @@
+/* gbp-gcc-pipeline-addin.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 "gbp-gcc-pipeline-addin"
+
+#include "gbp-gcc-pipeline-addin.h"
+
+#define ERROR_FORMAT_REGEX              \
+  "(?<filename>[a-zA-Z0-9\\-\\.\\/]+):" \
+  "(?<line>\\d+):"                      \
+  "(?<column>\\d+): "                   \
+  "(?<level>[\\w\\s]+): "               \
+  "(?<message>.*)"
+
+struct _GbpGccPipelineAddin
+{
+  IdeObject parent_instance;
+  guint     error_format_id;
+};
+
+static void
+gbp_gcc_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                             IdeBuildPipeline      *pipeline)
+{
+  GbpGccPipelineAddin *self = (GbpGccPipelineAddin *)addin;
+
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  self->error_format_id = ide_build_pipeline_add_error_format (pipeline,
+                                                               ERROR_FORMAT_REGEX,
+                                                               G_REGEX_CASELESS);
+}
+
+static void
+gbp_gcc_pipeline_addin_unload (IdeBuildPipelineAddin *addin,
+                               IdeBuildPipeline      *pipeline)
+{
+  GbpGccPipelineAddin *self = (GbpGccPipelineAddin *)addin;
+
+  g_assert (GBP_IS_GCC_PIPELINE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  ide_build_pipeline_remove_error_format (pipeline, self->error_format_id);
+  self->error_format_id = 0;
+}
+
+static void
+addin_iface_init (IdeBuildPipelineAddinInterface *iface)
+{
+  iface->load = gbp_gcc_pipeline_addin_load;
+  iface->unload = gbp_gcc_pipeline_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGccPipelineAddin, gbp_gcc_pipeline_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN, addin_iface_init))
+
+static void gbp_gcc_pipeline_addin_class_init (GbpGccPipelineAddinClass *klass) { }
+static void gbp_gcc_pipeline_addin_init (GbpGccPipelineAddin *self) { }
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..ac614da 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, IdeObject)
 
 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/meson/meson_plugin/__init__.py b/plugins/meson/meson_plugin/__init__.py
index 755bd48..ec05c79 100644
--- a/plugins/meson/meson_plugin/__init__.py
+++ b/plugins/meson/meson_plugin/__init__.py
@@ -16,7 +16,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from os import path
-import subprocess
 import threading
 import shutil
 import json
@@ -33,8 +32,14 @@ from gi.repository import (
 
 _ = Ide.gettext
 
-ninja = None
+_NINJA_NAMES = [ 'ninja-build', 'ninja' ]
 
+def execInRuntime(runtime, *args):
+    launcher = runtime.create_launcher()
+    launcher.push_args(args)
+    proc = launcher.spawn(None)
+    _, stdout, stderr = proc.communicate_utf8(None, None)
+    return stdout
 
 class MesonBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     project_file = GObject.Property(type=Gio.File)
@@ -60,126 +65,30 @@ class MesonBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     def do_get_priority(self):
         return -200 # Lower priority than Autotools for now
 
-    def do_get_builder(self, config):
-        return MesonBuilder(context=self.get_context(), configuration=config)
-
-
-class MesonBuilder(Ide.Builder):
-    def _get_build_dir(self) -> Gio.File:
-        context = self.get_context()
-
-        # This matches the Autotools layout
-        project_id = context.get_project().get_id()
-        buildroot = context.get_root_build_dir()
-        device = self.props.configuration.get_device()
-        device_id = device.get_id()
-        system_type = device.get_system_type()
-
-        return Gio.File.new_for_path(path.join(buildroot, project_id, device_id, system_type))
-
-    def _get_source_dir(self) -> Gio.File:
-        context = self.get_context()
-        return context.get_vcs().get_working_directory()
-
-    @staticmethod
-    def _task_set_error(task, err):
-        task.build_result.set_mode(_('Failed'))
-        task.build_result.set_failed(True)
-        task.return_error(err)
-        task.build_result.set_running(False)
-
-    @staticmethod
-    def _task_set_success(task):
-        task.build_result.set_mode(_('Successful'))
-        task.build_result.set_failed(False)
-        task.return_boolean(True)
-        task.build_result.set_running(False)
-
-    def do_build_async(self, flags, cancellable, callback, data=None):
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_result = MesonBuildResult(self.props.configuration,
-                                             self._get_build_dir(),
-                                             self._get_source_dir(),
-                                             cancellable,
-                                             flags=flags)
-
-        def wrap_build(task):
-            task.build_result.set_running(True)
-            try:
-                task.build_result.build()
-            except GLib.Error as err:
-                self._task_set_error(task, err)
-            else:
-                def postbuild_finish(err):
-                    if err:
-                        self._task_set_error(task, err)
-                    else:
-                        self._task_set_success(task)
-                task.build_result._postbuild_async(postbuild_finish)
-
-        def _on_prebuild_finish(task, err):
-            if err:
-                self._task_set_error(task, err)
-            else:
-                thread = threading.Thread(target=wrap_build, args=(task,))
-                thread.start()
-        task.build_result._prebuild_async(task, _on_prebuild_finish)
-
-        return task.build_result
-
-    def do_build_finish(self, result) -> Ide.BuildResult:
-        if result.propagate_boolean():
-            return result.build_result
-
-    def do_install_async(self, cancellable, callback, data=None):
+    def do_get_build_flags_async(self, ifile, cancellable, callback, data=None):
         task = Gio.Task.new(self, cancellable, callback)
-        task.build_result = MesonBuildResult(self.props.configuration,
-                                             self._get_build_dir(),
-                                             self._get_source_dir(),
-                                             cancellable)
-
-        def wrap_install(task):
-            task.build_result.set_running(True)
-            try:
-                task.build_result.install()
-            except GLib.Error as err:
-                self._task_set_error(task, err)
-            else:
-                def postinstall_finish(err):
-                    if err:
-                        self._task_set_error(task, err)
-                    else:
-                        self._task_set_success(task)
-                task.build_result._postinstall_async(postinstall_finish)
+        task.ifile = ifile
+        task.build_flags = []
 
-        thread = threading.Thread(target=wrap_install, args=(task,))
-        thread.start()
+        context = self.get_context()
+        build_manager = context.get_build_manager()
 
-        return task.build_result
+        # First we need to ensure that the build pipeline has progressed
+        # to complete the CONFIGURE stage which is where our mesonintrospect
+        # command caches the results.
+        build_manager.execute_async(Ide.BuildPhase.CONFIGURE,
+                                    cancellable,
+                                    self._get_build_flags_cb,
+                                    task)
 
-    def do_install_finish(self, result) -> Ide.BuildResult:
+    def do_get_build_flags_finish(self, result):
         if result.propagate_boolean():
-            return result.build_result
-
-    def do_get_build_flags_async(self, ifile, cancellable, callback, data=None):
-        task = Gio.Task.new(self, cancellable, callback)
-        task.build_flags = []
-
-        def extract_flags(command: str):
-            flags = GLib.shell_parse_argv(command)[1] # Raises on failure
-            build_dir = self._get_build_dir().get_path()
-            wanted_flags = []
-            for flag in flags:
-                if flag.startswith('-I'):
-                    # All paths are relative to build
-                    abspath = path.normpath(path.join(build_dir, flag[2:]))
-                    wanted_flags.append('-I' + abspath)
-                elif flag.startswith(('-isystem', '-W', '-D')):
-                    wanted_flags.append(flag)
-            return wanted_flags
+            return result.build_flags
 
+    def _get_build_flags_cb(self, build_manager, result, task):
         def build_flags_thread():
-            commands_file = path.join(self._get_build_dir().get_path(), 'compile_commands.json')
+            config = build_manager.get_pipeline().get_configuration()
+            commands_file = path.join(self.get_builddir(config), 'compile_commands.json')
             try:
                 with open(commands_file) as f:
                     commands = json.loads(f.read(), encoding='utf-8')
@@ -187,7 +96,7 @@ class MesonBuilder(Ide.Builder):
                 task.return_error(GLib.Error('Failed to decode meson json: {}'.format(e)))
                 return
 
-            infile = ifile.get_path()
+            infile = task.ifile.get_path()
             for c in commands:
                 filepath = path.normpath(path.join(c['directory'], c['file']))
                 if filepath == infile:
@@ -202,36 +111,50 @@ class MesonBuilder(Ide.Builder):
 
             task.return_boolean(True)
 
-        thread = threading.Thread(target=build_flags_thread)
-        thread.start()
-
-    def do_get_build_flags_finish(self, result):
-        if result.propagate_boolean():
-            return result.build_flags
+        try:
+            build_manager.execute_finish(result)
+            thread = threading.Thread(target=build_flags_thread)
+            thread.start()
+        except Exception as err:
+            task.return_error(err)
 
     def do_get_build_targets_async(self, cancellable, callback, data=None):
         task = Gio.Task.new(self, cancellable, callback)
         task.build_targets = []
 
-        config = self.get_configuration()
+        build_manager = self.get_context().get_build_manager()
+        build_manager.execute_async(Ide.BuildPhase.CONFIGURE,
+                                    cancellable,
+                                    self._get_build_targets_cb,
+                                    task)
+
+    def do_get_build_targets_finish(self, result):
+        if result.propagate_boolean():
+            return result.build_targets
+
+    def _get_build_targets_cb(self, build_manager, result, task):
+        context = build_manager.get_context()
+        config = build_manager.get_pipeline().get_configuration()
+        runtime = config.get_runtime()
+        builddir = self.get_builddir(config)
+        prefix = config.get_prefix()
+        bindir = path.join(prefix, 'bin')
 
         def build_targets_thread():
-            # TODO: Ide.Subprocess.communicate_utf8(None, cancellable) doesn't work?
             try:
-                ret = subprocess.check_output(['mesonintrospect', '--targets',
-                                               self._get_build_dir().get_path()])
-            except (subprocess.CalledProcessError, FileNotFoundError) as e:
+                ret = execInRuntime(runtime, 'mesonintrospect', '--targets', builddir)
+            except Exception as e:
                 task.return_error(GLib.Error('Failed to run mesonintrospect: {}'.format(e)))
                 return
 
             targets = []
+
             try:
-                meson_targets = json.loads(ret.decode('utf-8'))
+                meson_targets = json.loads(ret)
             except (json.JSONDecodeError, UnicodeDecodeError) as e:
                 task.return_error(GLib.Error('Failed to decode mesonintrospect json: {}'.format(e)))
                 return
 
-            bindir = path.join(config.get_prefix(), 'bin')
             for t in meson_targets:
                 # TODO: Ideally BuildTargets understand filename != name
                 name = t['filename']
@@ -242,7 +165,7 @@ class MesonBuilder(Ide.Builder):
                 install_dir = path.dirname(t.get('install_filename', ''))
                 installed = t['installed']
 
-                ide_target = MesonBuildTarget(install_dir, name=name)
+                ide_target = MesonBuildTarget(install_dir, name=name, context=context)
                 # Try to be smart and sort these because Builder runs the
                 # first one. Ideally it allows the user to select the run targets.
                 if t['type'] == 'executable' and t['installed'] and \
@@ -255,9 +178,8 @@ class MesonBuilder(Ide.Builder):
             if not targets or targets[0].install_directory.get_path() != bindir:
                 try:
                     # This is a new feature in Meson 0.37.0
-                    ret = subprocess.check_output(['mesonintrospect', '--installed',
-                                                   self._get_build_dir().get_path()])
-                    installed = json.loads(ret.decode('utf-8'))
+                    ret = execInRuntime(runtime, 'mesonintrospect', '--installed', builddir)
+                    installed = json.loads(ret)
                     for f in installed.values():
                         install_dir = path.dirname(f)
                         if install_dir == bindir:
@@ -266,169 +188,104 @@ class MesonBuilder(Ide.Builder):
                             ide_target = MesonBuildTarget(install_dir, name=path.basename(f))
                             targets.insert(0, ide_target)
                             break # Only need one
-                except (subprocess.CalledProcessError, json.JSONDecodeError, UnicodeDecodeError):
+                except Exception as e:
                     pass
 
             task.build_targets = targets
             task.return_boolean(True)
 
-        thread = threading.Thread(target=build_targets_thread)
-        thread.start()
+        try:
+            build_manager.execute_finish(result)
+            thread = threading.Thread(target=build_targets_thread)
+            thread.start()
+        except Exception as err:
+            task.return_error(err)
 
-    def do_get_build_targets_finish(self, result):
-        if result.propagate_boolean():
-            return result.build_targets
 
+class MesonPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
+    """
+    The MesonPipelineAddin registers stages to be executed when various
+    phases of the build pipeline are requested.
 
-class MesonBuildResult(Ide.BuildResult):
+    The configuration cannot change during the lifetime of the pipeline,
+    so it is safe to setup everything up-front.
+    """
 
-    def __init__(self, config, blddir, srcdir, cancel, flags=0, **kwargs):
-        super().__init__(**kwargs)
-        self.config = config
-        self.cancel = cancel
-        self.flags = flags
-        self.runtime = config.get_runtime()
-        self.blddir = blddir
-        self.srcdir = srcdir
-
-    def _new_launcher(self, cwd=None):
-        if self.runtime:
-            launcher = self.runtime.create_launcher()
-        else:
-            launcher = Ide.SubprocessLauncher.new(Gio.SubprocessFlags.NONE)
-            launcher.set_run_on_host(True)
-            launcher.set_clear_env(False)
-        if cwd:
-            launcher.set_cwd(cwd.get_path())
-        launcher.overlay_environment(self.config.get_environment())
-        return launcher
-
-    def _get_ninja(self):
-        global ninja
-        if not ninja:
-            if GLib.find_program_in_path('ninja-build'):
-                ninja = 'ninja-build' # Fedora...
-            else:
-                ninja = 'ninja'
-        return ninja
+    def do_load(self, pipeline):
+        context = pipeline.get_context()
+        build_system = context.get_build_system()
 
-    def _run_subprocess(self, launcher):
-        self.log_stdout_literal('Running: {}…'.format(' '.join(launcher.get_argv())))
-        proc = launcher.spawn()
-        self.log_subprocess(proc)
-        proc.wait_check(self.cancel)
+        # Only register stages if we are a meson project
+        if type(build_system) != MesonBuildSystem:
+            print("Not a meson project, ignoring")
+            return
 
-    def _ensure_configured(self):
-        bootstrap = bool(self.flags & Ide.BuilderBuildFlags.FORCE_BOOTSTRAP)
+        print("Registering meson pipeline addins")
 
-        if bootstrap or self.config.get_dirty():
-            try:
-                shutil.rmtree(self.blddir.get_path())
-                self.log_stdout_literal('Deleting build directory…')
-            except FileNotFoundError:
-                pass
-            self.config.set_dirty(False)
-
-        if not self.blddir.query_exists():
-            self.log_stdout_literal('Creating build directory…')
-            self.blddir.make_directory_with_parents(self.cancel)
-
-        # TODO: For dirty config we could use `mesonconf` but it does not
-        # handle removing defines which might be unclear
-
-        if not self.blddir.get_child('build.ninja').query_exists():
-            config_opts = self.config.get_config_opts()
-            extra_opts = config_opts.split() if config_opts else []
-            extra_opts.append('--prefix=' + self.config.get_prefix())
-            launcher = self._new_launcher(self.srcdir)
-            launcher.push_args(['meson', self.blddir.get_path()] + extra_opts)
-
-            self.set_mode(_('Configuring…'))
-            self._run_subprocess(launcher)
-
-    def install(self):
-        self._ensure_configured()
-
-        launcher = self._new_launcher(cwd=self.blddir)
-        launcher.push_args([self._get_ninja(), 'install'])
-        self._run_subprocess(launcher)
-
-    def _prebuild_async(self, task, callback):
-        # Unlike the rest of this class this is ran on the main thread
-        # using async apis so try to catch the error and propogate it
-        # where others are handled
-        def prebuild_command_finish(command, result):
-            try:
-                command.execute_finish(result)
-            except GLib.Error as e:
-                callback(task, e)
-            else:
-                callback(task, None)
+        config = pipeline.get_configuration()
+        runtime = config.get_runtime()
 
-        def prebuild_finish(runtime, result):
-            try:
-                runtime.prebuild_finish(result)
-                prebuild_command = self.config.get_prebuild()
-                prebuild_command.execute_async(runtime, self.config.get_environment(),
-                                             self, self.cancel, prebuild_command_finish)
-            except GLib.Error as e:
-                callback(task, e)
-
-        if self.runtime:
-            self.set_mode(_('Running prebuild…'))
-            self.runtime.prebuild_async(self, self.cancel, prebuild_finish)
-
-    def _postaction_async(self, callback, install=False):
-        if not self.runtime:
+        srcdir = context.get_vcs().get_working_directory().get_path()
+        builddir = build_system.get_builddir(config)
+
+        # Discover ninja in the runtime/SDK
+        ninja = None
+        for name in _NINJA_NAMES:
+            if runtime.contains_program_in_path(name):
+                ninja = name
+                break
+        if ninja is None:
+            print("Failed to locate ninja. Meson Building is disabled.")
             return
 
-        def postaction_finish(runtime, result):
-            try:
-                if install:
-                    runtime.postinstall_finish(result)
-                else:
-                    runtime.postbuild_finish(result)
-            except GLib.Error as e:
-                callback(e)
-            else:
-                callback(None)
+        # Register the configuration launcher which will perform our
+        # "meson --prefix=..." configuration command.
+        config_launcher = pipeline.create_launcher()
+        config_launcher.push_argv('meson')
+        config_launcher.push_argv(srcdir)
+        config_launcher.push_argv(builddir)
+        config_launcher.push_argv('--prefix={}'.format(config.props.prefix))
+        config_opts = config.get_config_opts()
+        if config_opts:
+            _, config_opts = GLib.shell_parse_argv(config_opts)
+            config_launcher.push_args(config_opts)
+
+        config_stage = Ide.BuildStageLauncher.new(context, config_launcher)
+        config_stage.set_completed(path.exists(path.join(builddir, 'build.ninja')))
+        self.track(pipeline.connect(Ide.BuildPhase.CONFIGURE, 0, config_stage))
+
+        # Register the build launcher which will perform the incremental
+        # build of the project when the Ide.BuildPhase.BUILD phase is
+        # requested of the pipeline.
+        build_launcher = pipeline.create_launcher()
+        build_launcher.push_argv(ninja)
+        if config.props.parallelism > 0:
+            build_launcher.push_argv('-j{}'.format(config.props.parallelism))
+
+        clean_launcher = pipeline.create_launcher()
+        clean_launcher.push_argv(ninja)
+        clean_launcher.push_argv('clean')
+        if config.props.parallelism > 0:
+            clean_launcher.push_argv('-j{}'.format(config.props.parallelism))
+
+        build_stage = Ide.BuildStageLauncher.new(context, build_launcher)
+        build_stage.set_clean_launcher(clean_launcher)
+        build_stage.connect('query', self._query)
+        self.track(pipeline.connect(Ide.BuildPhase.BUILD, 0, build_stage))
+
+        # Register the install launcher which will perform our
+        # "ninja install" when the Ide.BuildPhase.INSTALL phase
+        # is requested of the pipeline.
+        install_launcher = pipeline.create_launcher()
+        install_launcher.push_argv(ninja)
+        install_launcher.push_argv('install')
+
+        install_stage = Ide.BuildStageLauncher.new(context, install_launcher)
+        self.track(pipeline.connect(Ide.BuildPhase.INSTALL, 0, install_stage))
+
+    def _query(self, stage, pipeline, cancellable):
+        stage.set_completed(False)
 
-        if install:
-            self.set_mode(_('Running post-install…'))
-            self.runtime.postinstall_async(self, self.cancel, postaction_finish)
-        else:
-            self.set_mode(_('Running post-build…'))
-            self.runtime.postbuild_async(self, self.cancel, postaction_finish)
-
-    def _postbuild_async(self, callback):
-        self._postaction_async(callback)
-
-    def _postinstall_async(self, callback):
-        self._postaction_async(callback, install=True)
-
-    def build(self):
-        # NOTE: These are ran in a thread and it raising GLib.Error is handled a layer up.
-        self._ensure_configured()
-
-        clean = bool(self.flags & Ide.BuilderBuildFlags.FORCE_CLEAN)
-        build = not self.flags & Ide.BuilderBuildFlags.NO_BUILD
-
-        launcher = self._new_launcher(self.blddir)
-        launcher.push_args([self._get_ninja()])
-        build_jobs = self.config.props.parallelism
-        if build_jobs > 0:
-            launcher.push_args(['-j{}'.format(build_jobs)])
-        if clean:
-            self.log_stdout_literal('Cleaning…')
-            self.set_mode(_('Cleaning…'))
-            launcher.push_args(['clean'])
-            self._run_subprocess(launcher)
-        if build:
-            if clean: # Build after cleaning
-                launcher.pop_argv()
-            self.log_stdout_literal('Building…')
-            self.set_mode(_('Building…'))
-            self._run_subprocess(launcher)
 
 class MesonBuildTarget(Ide.Object, Ide.BuildTarget):
     # TODO: These should be part of the BuildTarget interface
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/plugins/vala-pack/Makefile.am b/plugins/vala-pack/Makefile.am
index 187b7cc..8f1e85a 100644
--- a/plugins/vala-pack/Makefile.am
+++ b/plugins/vala-pack/Makefile.am
@@ -94,6 +94,7 @@ libvala_pack_plugin_la_CFLAGS = \
        -Wno-unused-but-set-variable \
        -Wno-unused-label \
        -Wno-unused-function \
+       -Wno-unused-variable \
        $(NULL)
 
 libvala_pack_plugin_la_LIBADD = $(VALA_LIBS)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 9f4afe3..37419dc 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -41,9 +41,7 @@ libide/application/ide-application.c
 libide/application/ide-application-command-line.c
 libide/buffers/ide-buffer.c
 libide/buffers/ide-buffer-manager.c
-libide/buildsystem/ide-builder.c
 libide/buildsystem/ide-build-manager.c
-libide/buildsystem/ide-build-result.c
 libide/buildsystem/ide-build-system.c
 libide/buildsystem/ide-configuration-manager.c
 libide/devices/ide-device-manager.c
@@ -98,9 +96,7 @@ libide/workbench/ide-omni-bar.ui
 libide/workbench/ide-workbench-actions.c
 libide/workbench/ide-workbench.c
 libide/workbench/ide-workbench-header-bar.ui
-plugins/autotools/ide-autotools-builder.c
 plugins/autotools/ide-autotools-build-system.c
-plugins/autotools/ide-autotools-build-task.c
 plugins/autotools/ide-makecache.c
 plugins/autotools-templates/autotools_templates/__init__.py
 plugins/beautifier/gb-beautifier-workbench-addin.c
@@ -131,8 +127,6 @@ plugins/color-picker/gtk/menus.ui
 plugins/command-bar/gb-command-bar.c
 plugins/command-bar/gb-vim.c
 plugins/comment-code/gtk/menus.ui
-plugins/contributing/contributing_plugin/helper.py
-plugins/contributing/contributing_plugin/__init__.py
 plugins/c-pack/ide-c-format-provider.c
 plugins/create-project/gbp-create-project-genesis-addin.c
 plugins/create-project/gbp-create-project-tool.c
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]