diff --git a/doc/man/ade-build.1 b/doc/man/ade-build.1
new file mode 100644
index 0000000000000000000000000000000000000000..7cdc643f7f95327f9b569e42773f6744223bd46b
--- /dev/null
+++ b/doc/man/ade-build.1
@@ -0,0 +1,36 @@
+.TH ADE\-BUILD 1 25/11/2016 0.1612.0 Apertis\ Development\ Tools\ Manual
+
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+
+.SH NAME
+
+ade-build \- Build an Apertis project
+
+.SH SYNOPSIS
+.sp
+.nf
+\fIade build\fR [--verbose]
+.fi
+.sp
+.SH DESCRIPTION
+.sp
+Build the project for the configured target\&. It basically wraps "make all"\&.
+.sp
+.SH OPTIONS
+.sp
+.PP
+\fI\-\-verbose\fR
+.RS 4
+Command will produce verbose output\&.
+.RE
+.sp
+.SH SEE ALSO
+
+ade(1)
+
+.SH COPYRIGHT
+
+Copyright (c) 2016 Collabora Ltd.
diff --git a/doc/man/ade-configure.1 b/doc/man/ade-configure.1
new file mode 100644
index 0000000000000000000000000000000000000000..be2df922552c73bd8bccf3cce8fc9567a2dd1935
--- /dev/null
+++ b/doc/man/ade-configure.1
@@ -0,0 +1,72 @@
+.TH ADE\-CONFIGURE 1 25/11/2016 0.1612.0 Apertis\ Development\ Tools\ Manual
+
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+
+.SH NAME
+
+ade-configure \- Configure an Apertis project
+
+.SH SYNOPSIS
+.sp
+.nf
+\fIade configure\fR [--force] [--debug] --simulator [<args>]
+\fIade configure\fR [--force] [--debug] [--path=<sysroot-path>] --sysroot=<sysroot-id> [<args>]
+\fIade configure\fR [--force] [--debug] [--path=<sysroot-path>] --device=[<user>[:<pass>]@]<host>[:<port>] [<args>]
+.fi
+.sp
+.SH DESCRIPTION
+.sp
+Configure the project prior to building it\&.
+.sp
+.SH OPTIONS
+.sp
+.PP
+\fI\-\-force\fR
+.RS 4
+Force the project to get configured even if nothing has changed that would
+trigger the need to do so\&.
+.RE
+.PP
+\fI\-\-debug\fR
+.RS 4
+Enable debug symbols and disable optimizations for easier debugging\&.
+.RE
+.PP
+\fI\-\-path\fR
+.RS 4
+Specify the installation path for sysroots\&. By default, the path is
+/opt/sysroot\&.
+.sp
+.RE
+.PP
+\fI\-\-simulator\fR or \fI\-\-native\fR
+.RS 4
+Configure the project to be built with native architecture and run into
+the simulator\&.
+.sp
+.RE
+.PP
+\fI\-\-sysroot\fR
+.RS 4
+Configure the project to be built with the given sysroot
+(e\&.g\&. apertis\-16\&.12\-armhf)\&.
+.sp
+.RE
+.PP
+\fI\-\-device\fR
+.RS 4
+Configure the project to be built for the given target device
+(e\&.g\&. user@192\&.168\&.1\&.98)\&. The proper sysroot to use is chosen
+according to the device's image version and architecture\&.
+.sp
+.RE
+.SH SEE ALSO
+
+ade(1)
+
+.SH COPYRIGHT
+
+Copyright (c) 2016 Collabora Ltd.
diff --git a/doc/man/ade.1 b/doc/man/ade.1
index d7ea803791c98ff7f20328e2eaa1e184d6be0a25..c3abccf79a2b79bcb4825218a23505df25d9ba42 100644
--- a/doc/man/ade.1
+++ b/doc/man/ade.1
@@ -53,7 +53,22 @@ Manages the set of installed sysroots needed for cross-compilation\&.
 See \fBade-sysroot(1)\fR
 .sp
 .RE
+.PP
+\fIconfigure\fR
+.RS 4
+Configure the project prior to building it\&.
+.sp
+See \fBade-configure(1)\fR
 .sp
+.RE
+.PP
+\fIbuild\fR
+.RS 4
+Build the project for simulator or target device\&.
+.sp
+See \fBade-build(1)\fR
+.sp
+.RE
 .SH SEE ALSO
 
 ade-sysroot(1)
diff --git a/tools/ade b/tools/ade
index f3329eaa9c7642af2ea06d79a698e7b8f376dc03..3aa7029e2a5e3b6efeca9f1a2b179c75a729bd04 100755
--- a/tools/ade
+++ b/tools/ade
@@ -14,13 +14,16 @@ import argparse
 import argcomplete
 import configparser
 import glob
+import json
 import logging
 import os
 import pathlib
 import paramiko
 import re
 import shutil
+import stat
 import struct
+import subprocess
 import sys
 import tarfile
 import tempfile
@@ -71,6 +74,19 @@ class InvalidSysrootArchiveError(Exception):
         return self.message
 
 
+class InvalidProjectError(Exception):
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return self.message
+
+
+class NotConfiguredError(Exception):
+    def __init__(self):
+        pass
+
+
 class NotInstalledError(Exception):
     def __init__(self):
         pass
@@ -80,6 +96,7 @@ class NotSupportedError(Exception):
     def __init__(self):
         pass
 
+
 def is_valid_url(url):
     result = urlparse(url)
     return result.scheme and result.netloc and result.path
@@ -560,6 +577,123 @@ class SysrootManager:
         return f
 
 
+class Project:
+
+    def __init__(self, path=None, bundle_id=None):
+        self.bundle_id = None
+
+        if not path:
+            path = os.getcwd()
+        self.root = self.find_root(path)
+        self.parse_info()
+
+        # Override project properties if specified
+        if bundle_id:
+            self.bundle_id = bundle_id
+
+        self.check_validity()
+
+    def check_validity(self):
+        if not self.bundle_id:
+            raise InvalidProjectError("Property 'BundleId' is not specified")
+
+    def find_root(self, path):
+        p = pathlib.Path(path)
+        p.resolve()
+        while str(p) != p.root:
+            for filename in ['autogen.sh', 'configure']:
+                if p.joinpath(filename).exists() and \
+                        p.joinpath(filename).lstat().st_mode & stat.S_IEXEC:
+                    return str(p)
+            p = p.parent
+        raise InvalidProjectError("No configuration script found")
+
+    def parse_info(self):
+        path = os.path.join(self.root, 'SDKConfig', 'ProjectInfo.json')
+        if os.path.exists(path):
+            try:
+                with open(path) as f:
+                    info = json.load(f)
+                    self.bundle_id = info['BundleId']
+            except:
+                pass
+
+    def is_configured(self):
+        return os.path.exists(os.path.join(self.root, "config.status"))
+
+    def autoreconf(self):
+        env = os.environ.copy()
+        env['NOCONFIGURE'] = '1'
+        args = ["./autogen.sh"]
+        p = subprocess.Popen(args, cwd=self.root, env=env)
+        p.wait()
+
+    def configure(self, sysroot, debug=False, force=False, cflags=[], ldflags=[], args=[]):
+        env = os.environ.copy()
+        args  = ["./configure"]
+        args += ["--prefix=/Application/" + self.bundle_id]
+        args += ["--sysconfdir=/var/Applications/" + self.bundle_id + "/etc"]
+        args += ["--localstatedir=/var/Applications/" + self.bundle_id + "/var"]
+
+        if debug:
+            cflags += ["-g"]
+            cflags += ["-O0"]
+
+        if sysroot:
+            cflags += ["--sysroot=" + sysroot.path]
+            cflags += ["-I" + os.path.join(sysroot.path, '/usr/include')]
+            ldflags += ["--sysroot=" + sysroot.path]
+            args += ["--host=" + TargetTriplet(sysroot.version.arch).triplet]
+            args += ["--with-sysroot=" + sysroot.path]
+            self.set_pkg_config_vars(env, sysroot)
+
+        if not os.path.exists(os.path.join(self.root, "configure")):
+            self.autoreconf()
+
+        env['CFLAGS'] = ' '.join(cflags)
+        env['LDFLAGS'] = ' '.join(ldflags)
+
+        p = subprocess.Popen(args, cwd=self.root, env=env)
+        p.wait()
+
+    def build(self, verbose=False):
+        if not self.is_configured():
+            raise NotConfiguredError
+        args = ["make"]
+        if verbose:
+            args += ["V=1"]
+        p = subprocess.Popen(args, cwd=self.root)
+        p.wait()
+
+    def set_pkg_config_vars(self, env, sysroot):
+        def join_paths(paths):
+            triplet = TargetTriplet(sysroot.version.arch).triplet
+            return ":".join(paths).replace("${SYSROOT}", sysroot.path) \
+                                  .replace("${TRIPLET}", triplet)
+
+        # Abort is env already contains PKG_CONFIG_LIBDIR
+        if os.getenv('PKG_CONFIG_LIBDIR'):
+            return
+
+        pkgconfig_dir = ["${SYSROOT}/usr/lib/${TRIPLET}/pkgconfig",
+                         "${SYSROOT}/usr/share/pkgconfig"]
+        env['PKG_CONFIG_DIR'] = join_paths(pkgconfig_dir)
+
+        pkgconfig_path = ["${SYSROOT}/usr/lib/${TRIPLET}/pkgconfig",
+                          "${SYSROOT}/usr/share/pkgconfig",
+                          "${SYSROOT}/usr/lib/pkgconfig",
+                          "/usr/lib/${TRIPLET}/pkgconfig",
+                          "/usr/${TRIPLET}/lib/pkgconfig"]
+        env['PKG_CONFIG_PATH'] = join_paths(pkgconfig_path)
+
+        pkgconfig_libdir  = ["${SYSROOT}/usr/lib/${TRIPLET}/pkgconfig",
+                             "${SYSROOT}/usr/share/pkgconfig"]
+        env['PKG_CONFIG_LIBDIR'] = join_paths(pkgconfig_libdir)
+
+        pkgconfig_sysroot = ["${SYSROOT}"]
+        env['PKG_CONFIG_SYSROOT_DIR'] = join_paths(pkgconfig_sysroot)
+
+
 class Ade:
 
     def __init__(self):
@@ -578,6 +712,8 @@ class Ade:
         self.sysroot = None
         self.device = None
 
+        self.bundle_id = None
+
         self.sysroot_version = None
         self.distro = None
         self.release = None
@@ -587,6 +723,8 @@ class Ade:
         self.password = ''
 
         self.force = False
+        self.debug = False
+        self.verbose = False
 
     def get_sdk(self):
         try:
@@ -868,6 +1006,38 @@ class Ade:
             if self.format == 'parseable':
                 print("ImageVersion:{0}".format(self.target.version.get_tag()))
 
+    def do_configure(self):
+        target = self.get_target()
+        sysroot = None
+
+        if isinstance(target, Sysroot):
+            sysroot = target
+        elif isinstance(target, Device):
+            sdk = self.get_sdk()
+            version = self.target.version
+            if sdk and version.is_compatible(sdk.version):
+                sysroot = None # Native compilation; SDK version matches device image version
+            else:
+                sysroot = self.get_sysroot_manager().get_installed(version)
+                if not sysroot:
+                    self.die("No sysroot currently installed for {0}{1}{2}"
+                             .format(Colors.OKBLUE, version.get_name(), Colors.ENDC))
+
+        try:
+            project = Project(os.getcwd(), bundle_id=self.bundle_id)
+            project.configure(sysroot, debug=self.debug, force=self.force, args=self.args)
+        except Exception as e:
+            self.die("Couldn't configure project: {0}".format(e))
+
+    def do_build(self):
+        try:
+            project = Project()
+            project.build(self.verbose)
+        except InvalidProjectError as e:
+            self.die("Invalid project: {0}".format(e))
+        except NotConfiguredError:
+            self.die("Project is not configured; run 'configure' command first")
+
     def info(self, message):
         if self.format == 'friendly':
             print(message)
@@ -940,8 +1110,26 @@ if __name__ == '__main__':
     parser = sysroot_subparsers.add_parser('uninstall', help='Uninstall sysroot',
                                            parents=[sysroot_id_parser])
 
+    # Configure parser
+    configure_parser = subparsers.add_parser('configure', help="Configure application")
+    configure_parser.add_argument('--force', help="Force configuration", action='store_true')
+    configure_parser.add_argument('--debug', help="Enable debug symbols", action='store_true')
+    configure_parser.add_argument('--bundle-id', help="Apertis bundle ID (e.g. org.apertis.App)")
+    group = configure_parser.add_mutually_exclusive_group()
+    group.add_argument('--simulator', '--native', help="Use simulator as target", action='store_true', dest='simulator')
+    group.add_argument('--sysroot', help="Use sysroot as target (e.g. apertis-16.09-armhf)")
+    group.add_argument('--device', help="Use device as target (e.g. user:pass@192.168.1.98)")
+    configure_parser.add_argument('args', help="Configure options", nargs=argparse.REMAINDER)
+
+    # Build parser
+    build_parser = subparsers.add_parser('build', help="Build application")
+    build_parser.add_argument('--verbose', help="Verbose output", action='store_true')
+    build_parser.add_argument('args', help="Configure options", nargs=argparse.REMAINDER)
+
     argcomplete.autocomplete(root_parser)
 
     obj = Ade()
-    root_parser.parse_args(namespace=obj)
+    args, extra = root_parser.parse_known_args(namespace=obj)
+    if hasattr(obj, 'args'):
+        obj.args += extra
     obj.run()