[gnome-builder/wip/chergert/pipeline-merge: 30/76] pipeline: add IdeBuildPipeline



commit 1aadbf67407436bf3439a68b06073c993562c4dd
Author: Christian Hergert <chergert redhat com>
Date:   Fri Feb 3 13:41:50 2017 -0800

    pipeline: add IdeBuildPipeline
    
    The new IdeBuildPipeline provides a pluggable pipeline with phases. Stages
    are attached within a given stage. When the pipeline is executed, it tries
    to avoid performing unnecessary work so that we can speed up the rate at
    which some user-actions can be performed (such as running).
    
    You can invalidate phases for which those stages will need to be
    re-executed next time the pipeline advances to that phase.
    
    Stages can override some of this behavior by attaching to the ::query
    signal to determine their "completed" status. If work needs to be
    performed asynchronously, it can put a "hold" on the stage and release
    the hold after the asynchronous work is completed. The pipeline will
    pause to wait for thie operation to complete.
    
    You likely want to use some of the pre-created build stage subclasses
    such as for creating directories, launching subprocesses, or performing
    transfers.

 libide/Makefile.am                            |   13 +
 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-stage-launcher.c |  456 +++++
 libide/buildsystem/ide-build-stage-launcher.h |   57 +
 libide/buildsystem/ide-build-stage-mkdirs.c   |  186 ++
 libide/buildsystem/ide-build-stage-mkdirs.h   |   48 +
 libide/buildsystem/ide-build-stage-private.h  |   39 +
 libide/buildsystem/ide-build-stage-transfer.c |  183 ++
 libide/buildsystem/ide-build-stage-transfer.h |   35 +
 libide/buildsystem/ide-build-stage.c          |  899 +++++++++
 libide/buildsystem/ide-build-stage.h          |  204 ++
 libide/ide-types.h                            |    4 +-
 libide/ide.h                                  |    6 +
 16 files changed, 4911 insertions(+), 1 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index add82bd..f59585c 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -34,6 +34,12 @@ libide_1_0_la_public_headers =                            \
        buildsystem/ide-build-command.h                   \
        buildsystem/ide-build-command-queue.h             \
        buildsystem/ide-build-manager.h                   \
+       buildsystem/ide-build-pipeline.h                  \
+       buildsystem/ide-build-pipeline-addin.h            \
+       buildsystem/ide-build-stage.h                     \
+       buildsystem/ide-build-stage-launcher.h            \
+       buildsystem/ide-build-stage-mkdirs.h              \
+       buildsystem/ide-build-stage-transfer.h            \
        buildsystem/ide-build-result-addin.h              \
        buildsystem/ide-build-result.h                    \
        buildsystem/ide-build-system.h                    \
@@ -203,6 +209,12 @@ libide_1_0_la_public_sources =                            \
        buildsystem/ide-build-command.c                   \
        buildsystem/ide-build-command-queue.c             \
        buildsystem/ide-build-manager.c                   \
+       buildsystem/ide-build-pipeline.c                  \
+       buildsystem/ide-build-pipeline-addin.c            \
+       buildsystem/ide-build-stage.c                     \
+       buildsystem/ide-build-stage-launcher.c            \
+       buildsystem/ide-build-stage-mkdirs.c              \
+       buildsystem/ide-build-stage-transfer.c            \
        buildsystem/ide-build-result-addin.c              \
        buildsystem/ide-build-result.c                    \
        buildsystem/ide-build-system.c                    \
@@ -375,6 +387,7 @@ libide_1_0_la_SOURCES =                                   \
        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                 \
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-build-stage-mkdirs.h b/libide/buildsystem/ide-build-stage-mkdirs.h
new file mode 100644
index 0000000..3f25659
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-mkdirs.h
@@ -0,0 +1,48 @@
+/* ide-build-stage-mkdirs.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_BUILD_STAGE_MKDIRS_H
+#define IDE_BUILD_STAGE_MKDIRS_H
+
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_MKDIRS (ide_build_stage_mkdirs_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE, BUILD_STAGE_MKDIRS, 
IdeBuildStage)
+
+struct _IdeBuildStageMkdirsClass
+{
+  IdeBuildStageClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+IdeBuildStage *ide_build_stage_mkdirs_new      (IdeContext          *context);
+void           ide_build_stage_mkdirs_add_path (IdeBuildStageMkdirs *self,
+                                                const gchar         *path,
+                                                gboolean             with_parents,
+                                                gint                 mode);
+
+G_END_DECLS
+
+#endif /* IDE_BUILD_STAGE_MKDIRS_H */
diff --git a/libide/buildsystem/ide-build-stage-private.h b/libide/buildsystem/ide-build-stage-private.h
new file mode 100644
index 0000000..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-build-stage-transfer.h b/libide/buildsystem/ide-build-stage-transfer.h
new file mode 100644
index 0000000..8ae4566
--- /dev/null
+++ b/libide/buildsystem/ide-build-stage-transfer.h
@@ -0,0 +1,35 @@
+/* ide-build-stage-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 IDE_BUILD_STAGE_TRANSFER_H
+#define IDE_BUILD_STAGE_TRANSFER_H
+
+#include "buildsystem/ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_TRANSFER (ide_build_stage_transfer_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE, BUILD_STAGE_TRANSFER, 
IdeBuildStage)
+
+IdeBuildStageTransfer *ide_build_stage_transfer_new (IdeContext  *context,
+                                                     IdeTransfer *transfer);
+
+G_END_DECLS
+
+#endif /* 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/ide-types.h b/libide/ide-types.h
index 24dc1c3..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;
 
diff --git a/libide/ide.h b/libide/ide.h
index 3f2b9fb..467c0d9 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -38,6 +38,12 @@ G_BEGIN_DECLS
 #include "buildsystem/ide-build-command.h"
 #include "buildsystem/ide-build-command-queue.h"
 #include "buildsystem/ide-build-manager.h"
+#include "buildsystem/ide-build-pipeline.h"
+#include "buildsystem/ide-build-pipeline-addin.h"
+#include "buildsystem/ide-build-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-result-addin.h"
 #include "buildsystem/ide-build-result.h"
 #include "buildsystem/ide-build-system.h"


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