Squashed 'yocto-poky/' changes from 7b86c77..c8a4ed9

b1f23d1 build-appliance-image: Update to jethro head revision
7fe17a2 qemu: Security fix CVE-2016-2198
50700a7 qemu: Security fix CVE-2016-2197
1f0e615 libgcrypt: Security fix CVE-2015-7511
dc5f155 uclibc: Security fix CVE-2016-2225
ef13511 uclibc: Security fix CVE-2016-2224
ae57ea0 libbsd: Security fix CVE-2016-2090
eb9666a glibc: Security fix CVE-2015-7547
5b12268 build-appliance-image: Update to jethro head revision
a3a374a curl: Secuirty fix CVE-2016-0755
f4341a9 curl: Security fix CVE-2016-0754
35f4306 nettle: Security fix CVE-2015-8804
3e8a07b nettle: Security fix CVE-2015-8803 and CVE-2015-8805
5ffc326 socat: Security fix CVE-2016-2217
5cc5f99 libpng: Security fix CVE-2015-8472
21a816c libpng: Security fix CVE-2015-8126
6a0fbfa foomatic-filters: Security fixes CVE-2015-8327
d57aaf7 foomatic-filters: Security fix CVE-2015-8560
941874a build-appliance-image: Update to jethro head revision
d74a3cb cross-localedef-native: add ABI breaking glibc patch
12fae23 build-appliance-image: Update to jethro head revision
67ac9d6 e2fsprogs: Ensure we use the right mke2fs.conf when restoring from sstate
5812fc9 build-appliance-image: Update to jethro head revision
3de2492 ref-manual: Updated host package install requirements CentOS
79de8cf toaster-manual: Updated the "Installation" to have TOASTER_DIR information
a23d262 toaster-manual: Updated instructions for production setup.
b6def81 linux-yocto: Update SRCREV for genericx86* for 4.1, fixes CVE-2016-0728
db0f8ac linux-yocto: Update SRCREV for genericx86* for 3.19, fixes CVE-2016-0728
c8122a0 linux-yocto: Update SRCREV for genericx86* for 3.14, fixes CVE-2016-0728
cdeb241 meta-yocto-bsp: Remove uvesafb (v86d) from generic x86 features
52cd219 yocto-bsp: Set SRCREV meta/machine revisions to AUTOREV
a88d6cb yocto-bsp: Set KTYPE to user selected base branch
4e74b36 yocto-bsp: Avoid duplication of user patches ({{=machine}}-user-patches.scc)
6680773 yocto-bsp: Default kernel version to 4.1 on x86_64
4c075e7 piglit: don't use /tmp to write generated sources to
ee52ac6 gen-lockedsig-cache: fix bad destination path joining
e9f95df linux-yocto: Update SRCREV for qemux86* for 4.1, fixes CVE-2016-0728
e63bab1 linux-yocto: Update SRCREV for qemux86* for 3.19, fixes CVE-2016-0728
64a4920 linux-yocto: Update SRCREV for qemux86* for 3.14, fixes CVE-2016-0728
5b043da libpng12: update URL that no longer exists
655c8a5 libpng: update URL that no longer exists
96fda8c busybox: fix build of last applet
ae037d9 ghostscript: add dependency for pnglibconf.h
26eb877 gcr: Require x11 DISTRO_FEATURE
e632cdb uClibc: enable utmp for shadow compatibility
e8c9613 git: Security fix CVE-2015-7545
108ea6d glibc-locale: fix QA warning
9a88c1d grub: Security fix CVE-2015-8370
443b09a gdk-pixbuf: Security fix CVE-2015-7674
6c91068 librsvg: Security fix CVE-2015-7558
9fd2349 bind: Security fix CVE-2015-8461
5a40d9f bind: Security fix CVE-2015-8000
1bbf183 libxml2: Security fix CVE-2015-8710
2ec6d1d libxml2: Security fix CVE-2015-8241
55aafb5 dpkg: Security fix CVE-2015-0860
029948b tzdata: update to 2016a
2bcf141 tzcode: update to 2016a
cc3a391 kernel-yocto: fix checkout bare-cloned kernel repositories
049be17 libpcre: bug fixes include security
5e94ac7 qemu: Security fix CVE-2015-7295
7ee1828 qemu: Security fix CVE-2016-1568
ca6ec2e qemu: Security fix CVE-2015-8345
b55a677 qemu: Security fix CVE-2015-7512
4922f47 qemu: Security fix CVE-2015-7504
3ec0e95 qemu: Security fix CVE-2015-8504
942ce53 openssl: Security fix CVE-2016-0701
ce8ae1c openssl: Security fix CVE-2015-3197
080e027 tiff: Security fix CVE-2015-8784
c6ae9c1 tiff: Security fix CVE-2015-8781
049b7db bind: CVE-2015-8704 and CVE-2015-8705
d632a92 rpmresolve.c: Fix unfreed pointers that keep DB opened
5b993ed openssh: CVE-2016-1907
27ee5b4 glibc: CVE-2015-8776
a4134af glibc: CVE-2015-9761
e10ec6f glibc: CVE-2015-8779
a5a965d glibc: CVE-2015-8777.patch
2fb7ee2 bitbake: toaster: make runbuilds loop
b9ad87b nativesdk-buildtools-perl-dummy: Bump PR
0a1c63a nativesdk-buildtools-perl-dummy: properly set PACKAGE_ARCH
d4b400e nativesdk-buildtools-perl-dummy: fix rebuilding when SDKMACHINE changes
8c8c4ed Revert "gstreamer1.0-plugins-good.inc: add gudev back to PACKAGECONFIG"
b832202 Revert "gstreamer: Deal with merge conflict which breaks systemd builds"
dd0ba9e build-appliance-image: Update to jethro head revision
325d205 gstreamer: Deal with merge conflict which breaks systemd builds
53b114b build-appliance-image: Update to jethro head revision
02be35d poky.conf: Bump version for 2.0.1 jethro release
f5551f8 ref-manual: Updated the list of supported image types.
aa179ae dev-manual: Added three new wic option descriptions.
20007c8 dev-manual: Added the --overhead-factor wic option description.
2dd7f46 dev-manual: Added the --extra-space wic option description.
81cc737 dev-manual: Added wic --notable option description.
2b1dce5 dev-manual:
a6f5293 kernel/kernel-arch: Explicitly mapping between i386/x86_64 and x86 for kernel ARCH
e79a538 openssh: update to 7.1p2
b171076 devtool: reset: do clean for multiple recipes at once with -a
255115f devtool: sdk-update: fix error checking
3f69105 devtool: sdk-update: fix metadata update step
5ba94af devtool: sdk-update: fix not using updateserver config file option
d03d145 classes/populate_sdk_ext: disable signature warnings
00ff950 classes/populate_sdk_ext: fix cascading from preparation failure
22446c6 scripts/oe-publish-sdk: add missing call to git update-server-info
8597a61 devtool: use cp instead of shutil.copytree
95cc641 buildhistory: fix not recording SDK information
84d48ac recipetool: create: fix error when extracting source to a specified directory
4369329 recipetool: create: detect when specified URL returns a web page
4c3191f recipetool: create: prevent attempting to unpack entire DL_DIR
caca77e recipetool: create: fix do_install handling for makefile-only software
383159e recipetool: create: avoid traceback on fetch error
be40baa recipetool: create: handle https://....git URLs
a897bfd devtool: sdk-update: fix traceback without update server set
9c4b61e classes/populate_sdk_ext: error out of install if buildtools install fails
4c07dd2 gstreamer1.0-plugins-good.inc: add gudev back to PACKAGECONFIG
83b72d8 linux-yocto: Update Genericx86* BSP to 4.1.15 kernel
44639bd libaio: don't disable linking to the system libraries
a0be9bd linux-yocto/4.1: update to v4.1.15
53f0290 libxml2: security fix CVE-2015-5312
f4b0c49 libxml2: security fix CVE-2015-8242
fb409c9 libxml2: security fix CVE-2015-7500
55d097a libxml2: security fix CVE-2015-7499
8e6b2d6 libxml2: security fix CVE-2015-7497
332eb1d libxml2: security fix CVE-2015-7498
cbc4e83 libxml2: security fix CVE-2015-8035
c4b71e1 libxml2: security fix CVE-2015-7942
fdea03d libxml2: security fix CVE-2015-8317
6fc1109 libxml2: security fix CVE-2015-7941
9eb4ce0 openssl: fix for CVE-2015-3195
6880f82 openssl: fix for CVE-2015-3194
7dcaa84 openssl: fix for CVE-2015-3193
435139b logrotate: do not move binary logrotate to /usr/bin
5f49c0a cairo: fix license for cairo-script-interpreter
a29ec81 glibc: Fix ld.so / prelink interface for ELF_RTYPE_CLASS_EXTERN_PROTECTED_DATA
b1e980f gcc: Update default Power GCC settings to use secure-plt
ed82690 prelink: Fix various prelink issues on IA32, ARM, and MIPS.
9a620da autotools: Allow recipe-individual configure scripts
f828071 toolchain-scripts.bbclass: unset command_not_found_handle
49858bd devtool: upgrade: fetch remote repository before checking out new revision
d213452 devtool: upgrade: remove erroneous error when not renaming recipe
fec97f6 devtool: upgrade: fix updating PV and SRCREV
3b4f659 devtool: upgrade: fix removing other recipes from workspace on reset
61a7de0 devtool: include do_patch in SRCTREECOVEREDTASKS
82c0072 toolchain-shar-extract.sh: do not allow $ in paths for ext SDK
f181e72 scripts/gen-lockedsig-cache: improve output
4b5d4ca toolchain-shar-extract.sh: proper fix for additional env setup scripts
d2ea8f1 toolchain-shar-relocate: don't assume last state of env_setup_script is good
02ef437 populate_sdk_ext.bbclass: Be more permissive on the name of the buildtools
3653b17 classes/populate_sdk_ext: fail if SDK_ARCH != BUILD_ARCH
8879571 classes/populate_sdk_ext: tweak reporting of workspace exclusion
eeda3c6 classes/populate_sdk_ext: make it clear when SDK installation has failed
dee9fbe classes/populate_sdk_ext: tidy up preparation log file writing
d001d46 classes/license: fix intermittent license collection warning
777451c classes/metadata_scm: fix git errors showing up on non-git repositories
cb0ca72 oeqa/selftest/layerappend: fix test if build directory is not inside COREBASE
8970ad6 oeqa/selftest/devtool: fix test if build directory is not inside COREBASE
4f7fdd0 classes/distrodata: split SRC_URI properly before determining type
3b7df55 uninative.bbclass: Choose the correct loader based on BUILD_ARCH
f3d7c3f openssl: sanity check that the bignum module is present
96b1b5c glibc: Backported a patch to fix glibc's bug(18589)
7aecb57 directfb.inc: force bfd linker for armv7a
75ca2c8 texinfo: don't create dependency on INHERIT variable
02c7b3f package_manager.py: define info_dir and status_file when OPKGLIBDIR isn't the default
003c94f libsdl2: require GLES when building Wayland support
ad6db01 gst-plugins-bad: add PACKAGECONFIGs for voamrwbenc, voaacenc, resindvd
f0d87fe gstreamer1.0-plugins-good: fix PACKAGECONFIG for gudev and add one for v4l2 and libv4l2
35f34a6 gstreamer1.0-plugins-bad: fix dependencies for uvch264 PACKAGECONFIG
3b77e20 gstreamer1.0-plugins-{base,good}: update PACKAGECONFIGs
e2d4412 libunwind: fix build for qemuarm
ef69078 guile, mailx, gcc, opensp, gstreamer1.0-libav, libunwind: disable thumb where it fails for qemuarm
4700e40 icu: force arm mode
743ee04 libxcb: Add a workaround for gcc5 bug on mips
8a3deca bitbake: fetch: use orig localpath when calling orig method
0073b23 yocto-bsp: Typo on the file extension
71dbbcd bsp-guide: Updated the license statement.
41f1026 dev-manual: Correction to the KVM stuff in the runqemu commands.
38e3c6e mega-manual: Added four new figures for GUI example.
b99ec28 poky.ent: Fixed POKYVERSION variable.
c670dc7 yocto-project-qs, ref-manual, poky.ent: CentOS Package updates
b968190 dev-manual: Updated runqemu command options list
1278753 toaster-manual: Removed SDKMACHINE from the json file example.
7b25b70 ref-manual: Updated list of supported distros.
d9423fb ref-manual: Updated the GCC 5 migration section for 2.0
347347a bitbake: lib/bb/utils: improve edit_bblayers_conf() handling of bblayers.conf formatting
5935783 bitbake: lib/bb/utils: fix error in edit_metadata() when deleting first line
7fdad70 rpcbind: Security Advisory - rpcbind - CVE-2015-7236
0cb2fa5 subversion: fix CVE-2015-3187
5b52e9b subversion: fix CVE-2015-3184
59bdde4 linux-firmware: rtl8192cx: Add latest available firmware
8ad2bcc init-install-efi: fix script for gummiboot loader
c3087bd init-install-efi: fix script for eMMC installation
d2bf9fb pulseaudio: Fix HDMI profile selection
0556c58 allarch: Force TARGET_*FLAGS variable values
e683dac libsndfile: fix CVE-2014-9756
092757e libxslt: CVE-2015-7995
dab5555 unzip: rename patch to reflect CVE fix
1753d4a readline: rename patch to contain CVE reference
9dd3422 libarchive: rename patch to reflect CVE
1401976 binutils: Fix octeon3 disassembly patch
a54a0db opkg: add cache filename length fixes
fc45dea build-appliance-image: Update to jethro head revision
e14498b meta-yocto/distro: Updated SANITY_TESTED_DISTROS.
01bba74 meta-yocto/distro: Updated SANITY_TESTED_DISTROS.
e1aa897 build-appliance-image: Update to jethro head revision
96cab33 unzip: CVE-2015-7696, CVE-2015-7697
1b2a942 vte: fix DoS from malicious escape sequence (CVE-2012-2738)
370a291 build-appliance-image: Update to jethro head revision
00911c9 linux-yocto_4.1: Update SRCREV for genericx86*
c86957a glibc: Allow 64 bit atomics for x86
b02c5f6 local.conf.sample: Disable image-prelink by default
1630dbb ref-manual: Applied a correction to the GCC 5 migration 2.0 section.
37677d6 ref-manual: Updated ADT Installer Extras
a79e303 kernel-dev: Added cross-reference to .config information
e03b19b ref-manual: Applied review updates to 2.0 migration section.
a0791c1 bitbake: toasterui: Create per-build logs
290534d bitbake: build/utils: Add BB_TASK_IONICE_LEVEL support
3ebf761 bitbake: cooker: Ensure BB_CONSOLE remains correct over server resets
5b19b71 bitbake: bb/ui: Use getSetVariable command for BB_CONSOLELOG
acc7b4d bitbake: command: Add getSetVariable command
c8051c5 bitbake: bitbake-user-manual: Added new description for BB_TASK_IONICE_LEVEL
183290a bitbake: bitbake-user-manual: Added BBTARGETS variable description.
66d3c35 bitbake: toaster: templates Add meaningful title tags
5724b2a perl: Remove errornous extra path-specs for Module::Build based modules
884cf7a perl: Correct path for vendorlib, vendorarch, sitelib and sitearch
2d0c499 perl: fix Perl5 module builds
24cfcc4 runqemu-export-rootfs: update location of unfsd binary
da386d3 gtk-icon-cache: pass the native libdir to the intercept
63a0311 connman: Move wired-setup to ${datadir}
1c3c76d useradd-staticids.bbclass: Do not require trailing colons
8a0d8ee toaster manual: Updated the set up and use chapter
f19b52c ref-manual: Updates to the 1.8 to 2.0 Migration section.
b73da6b toaster-manual: Added new Toaster functionality descriptions.
947e156 ref-manual: Updated the rootfs*.bbclass description.
62e200e bitbake: toaster: orm Fix restrictive LogMessage message length
78f935d bitbake: toaster: Remove all navigation when not in build mode
c5f147b bitbake: toaster: Run tests in build mode
1d17109 bitbake: toaster: Hide builds for non-cli projects in analysis mode
a580479 bitbake: toaster: Hide top bar buttons in analysis mode
1ec2ec3 bitbake: toaster: Show mode-appropriate landing page
bbac0f0 bitbake: toaster: Add BUILD_MODE flag to context
851f0d8 bitbake: toaster: add get_or_create_targets API
dcd9cd0 bitbake: fetcher: svn: Add support for checkout to a custom path
4ab7202 bitbake: cooker: preserve pre and post configs
fdfdfc8 oeqa/utils/decorators: fix missing keyword arguments on decorators
a2d5b7a classes/gtk-icon-cache: don't pass STAGING_LIBDIR_NATIVE to intercepts
5171329 intercepts/update_icon_cache: use STAGING_DIR_NATIVE from environment
d18d902 lib/oe/rootfs: tell intercepts where the native sysroot is
9336e1f subversion: add explicit dependency on file-replacement-native for native builds
19358d0 rpm: add explicit dependency on file-replacement-native for native builds
698c3de file: don't replace host file when built natively
83a2bde sanity: check that the host has file installed
43c46e9 bitbake: add file-native to ASSUME_PROVIDED
2925cd9 Revert "runqemu-export-rootfs: update location of unfsd binary"
d023d99 populate_sdk_base: Ensure PKGDATA_DIR exists
9b956c4 Perl: Use CC version not $Config(gccversion)
0f75740 wic/utils/oe/misc.py: Preserve PATH when running native tools
273bcb4 mtools_4.0.18.bb: Use create_wrapper() for mcopy
031d464 scripts/oe-pkgdata-util: Fix variable name in error handling
d8d4ce7 Add 850 codepage to uninative-tarball
c1d5e89 e2fsprogs: backport a patch to fix filetype for hardlink
426a9b7 oeqa/selftest: Added testcase decorators.
835525c runqemu-ifup: Check if the tap interface is set up correctly
b13c0be qemurunner: Show the output of runqemu script
9846275 runqemu-internal: Enable support for use virtio devices.
304c956 linux-yocto{, -rt}: Enable support for virtio drivers in qemu machines.
eebcbe1 runqemu: Enable support for kvm without vhost in x86 and x86_64
135d094 prserv.bbclass: remove it since it is null
c509c78 initscripts/sysfs.sh: Mount devtmpfs on /dev/ if needed
022f8cc image-mklibs.bbclass: update i586 TARGET_ARCH test to i*86
d492a70 base.bbclass: considering multilib when setting LICENSE_EXCLUSION
54b7471 gcc-target.inc: Add support for executable thats may have a suffix
0d69a171 cairo: backport fix for compatibility with OpenGL ES 2.0
64b5e3e mesa-demos: fix deadlock in sharedtex_mt
dc8495f bzip2: fix bunzip2 -qt returns 0 for corrupt archives
5bf1430 gnome-desktop: add xkeyboard-config dependency
48443cc gtk+3: Do not try to initialize GL without libgl
59fdbae classes/insane: rename invalid-pkgconfig QA check to invalid-packageconfig
73e1d33 uclibc: Implement syncfs and AT_EMPTY_PATH for all and O_PATH for arm
2e4575d systemd: Fix build with uclibc
40911f4 libtirpc: Fix a bug exposed by uclibc
d90d3e8 libpam: Fix build with uclibc
32c8625 coreutils: Do not use host paths in getloadavg.m4
20b7d87 coreutils-6.9: Add missing dependency on virtual/libiconv
8bb6436 uclibc: Fix build with gcc5
e5e8fce libtirpc: Refresh uclibc patches
fd66dd1 rpcbind: Fix build with uclibc
369c536 scripts/oe-publish-sdk: create directory before making git repo
8a555fe rootfs.py: add more info to the warning message
787253f package signing: automatically export public keys
579e254 package_manager: fail if signed feeds are enabled for ipk or dpkg
835e755 Add new bbclass for package feed signing
822844d sign_rpm.bbclass: make RPM_GPG_NAME a mandatory setting
48d60fc sign_rpm.bbclass: be more verbose in case of error
dbb9af6 package_manager: support GPG_PATH variable
b682fca sign_rpm.bbclass: introduce GPG_PATH variable
8ccbc26 apr: remove conflict with ccache
5e42593 linux-yocto: nf_tables: Add nf_tables feature
1c2fdd9 linux-yocto/3.19: fix ARM boot with gcc5.x
3bab714 linux-yocto: skip kernel meta data branches when finding machine branch
1561d0d kern-tools: avoid duplicate .scc file processing
47dcee2 linux-yocto/4.1: drm/i915: Fix the VBT child device parsing for BSW
380f2c6 linux-yocto: axxia configuration updates
505a826 build-appliance-image: Update to jethro head revision
7d30d67 ref-manual: Updated the allarch class description.
a8674ae ref-manual: Updated the MACHINE_ESSENTIAL_EXTRA_RRECOMMENDS variable
e7c8c79 ref-manual: Added the 1.8 to 2.0 migration section.
cd48ccc dev-manual: Added notes to clarify use of pkg-config
dc9e4cb ref-manual: Added correct class name as part of pkgconfig description
5bc8fa6 ref-manual: Fixed typo in 1.6 migration section for BitBake
2fe3809 ref-manual, dev-manual: Applied feedback to edit several classes
359b7fb ref-manual: Added three PACKAGE_FEED_* variable descriptions
2f4e90c toaster-manual: Updated the json file example bits to be current
66653cb ref-manual: Updated the image-swab.bbclass description
d66cf20 toaster-manual: New section on PREFERRED_VERSION
4b9daa8 ref-manual: Added many new class descriptions.
ae0d508 toaster-manual: Added note for creating virtual environment
98d7d24 toaster-manual: Updates to example toasterconf.json file
b263a3e dev-manual: Added CentOS packages to enable runtime tests on QEMU
9abc72c adt-manual: Fixed PMS typo
2e7d650 ref-manual: Updates to clarify Fetcher URL directory parameters
7facee6 toaster-manual: Updated the section for setting up virtual env.
10970a6 dev-manual: Added package requirements for runtime QEMU testing
acacf6b ref-manual: Added linuxloader.bbclass reference description.
8fc90a7 Makefile: Updated the make file to not create toaster-manual pdf
0889848 dev-manual: Updated devtool build --help example
1944d28 documentation: Updated files to support 2.0 release.
8d2a6f0 toaster-manual: Removed "dizzy" and replaced with "jethro"
3bff581 ref-manual: Added descriptions for 5 new variables and 2 tasks.
a87268e dev-manual: Updated the Marking Packages information.
1c7f462 ref-manual: Added oe-seltest package requirements sections.
3d82046 adt-manual: Updated the build toolchain section with more detail.
54b4aff adt-manual: Updated some hard-coded distro values
196210f dev-manual: Updated the multilib example.
3930f04 ref-manual: Updated the EXCLUDE_FROM_SHLIBS description
0d1c86b ref-manual: Updated EXCLUDE_FROM_SHLIBS description.
eea7521 ref-manual: Updated distrodata.bbclass example
2eaf843 ref-manual: Added new description for PACKAGE_EXCLUDE_COMPLEMENTARY
97298fb dev-manual: Fixed typo in path for wic plugins
05d8101 ref-manual: Added new EXCLUDE_FROM_SHLIB variable
316d432 ref-manual: Added new variable description for SKIP_FILEDEPS
a1b25e6 yocto-project-qs, ref-manual: Replaced "yum" with "dnf"
d284fba ref-manual: Added cross-reference phrase to some variables
5a226f7 dev-manual: Changed multilib example
6ca549f dev-manual: Added note about building out Autotools projects
92b26ad archiver.bbclass: Fixes and improves archiver class for kernel and gcc packages
2d00803 oeqa/selftest: improve config writing and cleanup
1881564 oeqa/selftest/wic: remove numbers from test names
2ac34d2 oeqa/selftest: clean up selftest.inc in teardown
a66ed33 oeqa/selftest/wic: fix cleaning
b67b1a4 oeqa/selftest/wic: corrected testcase decorator for test18_iso_image
e191120 oeqa/selftest: verify that devtool can use plugins in other layers
b8a9728 oeqa/selftest/buildoptions: Use the correct script for cleaning the workdir
94decbc oeqa/selftest/bbtests: Updated bitbake TCs
322c324 oeqa/selftest/bbtests: clean up local DL_DIR/SSTATE_DIR safely
cf311a7 oeqa/utils/ftools: From functions that expect data, check if None
900639c oeqa/utils/ftools: Ignore the exception if file does not exist
2e91cbd oeqa/selftest/manifest.py: Test support for manifests
c9bef34 useradd_base.bbclass: Do not warn without a reason
accb59e qemu: disable Valgrind
ac1bc7d i2c-tools: fix inverted RDEPENDS
35c043b rpm: remove spurious build dependencies
41cbfd7 gcc-5.2: Fix various _FOR_BUILD and related variables
a27da70 sudo: fix file permission for /etc/pam.d/sudo
abeaed9 openssh: fix file permission for /etc/pam.d/sshd
96a5cfd sanity.bbclass: expand warning when chmod fails
409e6e0 populate SDK: prepare calling of bb.utils for exceptions
db55d31 devtool: handle virtual providers
8578bc1 libc-package: Fix localedef multilib dependency issues
0942aff toolchain-shar-extract.sh: print full-length title underline
9630fc1 classes/populate_sdk_ext: detect and warn if running in OE environment
254ff38 classes/populate_sdk_ext: add note to env setup script
9a81ba7 classes/populate_sdk_ext: prevent image construction from executing on install
ec5ec35 classes/populate_sdk_ext: consistent indentation
b8f7042 oeqa/runtime: Fix setUp and tearDown methods
3327401 oetest: Add tearDownLocal class
3b7853a test-empty-image: rename from core-image-empty
5febb1d scripts/gen-lockedsig-cache: fix race with temp file creation
3b5d6ff image-live: make SYSLINUX_ROOT changable in image recipes
5009966 toolchain-shar-extract.sh: provide proper path for env_setup_script
ae7703f classes/base: provide hints on PACKAGECONFIG error
5a02ec2 devtool: extract: fix error handling
3aac110 metadata_scm: rewrite git hash logic
59668f2 linux-yocto-custom: fix typo in Upstream-Status tag
c52dcb0 grub-efi, gummiboot: Emit correct path in startup.nsh
f9d29ab coreutils: fix for native and nativesdk
b1a7405 gcc-4.x: fix wrong warning when using the universal zero initializer {0}
402723e tzdata: reinstate changes reverted in 2014c upgrade
3770461 build-compare: drop PATCHTOOL setting
4846260 common-licenses: use correct GFDL-1.1 license text
a9053ac bitbake: toaster: Add tests for error message display on the build dashboard
2517987 bitbake: toaster: Modify "New build" button behaviour for cli builds project
56d4c84 bitbake: toaster: Clean up template code
d96cedf bitbake: toaster: More linting of tests
7c8877e bitbake: toaster: Show tooltip next to cli builds project name in all builds
7670234 bitbake: toaster: Hide tabs and add info popups for command line builds
da4c614 bitbake: toaster: Make the builds view the project page for "command line builds"
ef6fc2b bitbake: toaster: Replace "Run again" button with help text for cli builds
7467b68 bitbake: toaster: Exclude "command line builds" project from projects typeahead
b5624c7 bitbake: toaster: Show 'not applicable' for default project machine and release
3c4c984 bitbake: toaster: Reorganise and lint tests
3ba43f2 bitbake: fetch2/hg: Include missing errno import
6fa3fec bitbake: cooker: normalize build targets
5effe8f bitbake: toaster: Allow any text input to machine configuration variable
320d05e bitbake: toaster: exit or return depending on the mode
2e2e40c bitbake: toaster: set TOASTER_MANAGED variable
a73895e bitbake: toaster: get rid of SRCFILE
779539c bitbake: toaster: use path to the script to guess config path
eb8b2b9 bitbake: toaster: Guard against builds with no targets
65e8bde bitbake: toaster: Remove Toaster exceptions section of build dashboard
93f0b61 bitbake: toaster: Record critical errors
069a611 bitbake: toaster: Test that exception isn't thrown by project page
026e981 bitbake: toaster: Check whether buildrequest exists before using it
1feeb8e bitbake: toaster: Always run bldcontrol migrations
ae82d77 bitbake: toaster: buildinfohelper Detect command line builds
596c219 bitbake: toaster: Disable add layer button when input is empty
24e5a17 bitbake: toaster: Have 'Version' next to recipe name
c895838 bitbake: toaster: Improve directory structure layout
2f52ef4 bitbake: toaster: importlayer Update property names for importlayer api calls
556c0ea lib/oe/image.py: Fix dependency handling for compressed types
d302c98 bitbake: toaster: Fix missing tooltips from layers on project configuration page
7e5464b bitbake: toaster: Fix broken test case
2e375e6 bitbake: toaster: exclude recipes with empty names
fa3e82d bitbake: toaster: delete recipe if it can't be saved
82675fc bitbake: toaster: Remove project name from latest project builds
6aeaca1 bitbake: toaster: test get_alldeps API
0fb6be0 bitbake: toaster: fix orm tests
dea679a bitbake: toaster: fix NameError
6e0c0fd bitbake: toaster: use get_alldeps in layerdetails renderer
bd2ec77 bitbake: toaster: implement API to get full list of deps
05594f8 bash: Disable custom memory allocator
adbbab7 icu: fix install race
b1d0aab webkitgtk, gcr, libsecret: force ARM mode
67d6500 gtk+3: gtk3-demo needs libgl
f385ed1 lib/oe/distro_check: Remove '_proxy' on dict values used by urllib.open
4bf7b7d cups: fix non-deterministic xinetd behaviour
32dbf71 cronie: clean up bugtracker info
6396d6a irda-utils: clean up bugtracker info
8d5878b screen: fix CVE-2015-6806
acdc2db kbd: provide a workaround for build failures
67959b9 machine/qemu: Fix OpenGL/GLX support with xserver-xorg.
fedff4f busybox.inc: remove redundant @DATADIR@ replacement
78b9d2d insane.bbclass: remove misleading path in warning
8995a30 iptables: only check libnetfilter-conntrack when libnfnetlink is enabled
e35c404 bitbake: toaster: Don't descend into directories for cached_layers
d9528d9 toasterconf: update meta-yocto to jethro and drop dizzy
2d6701f bitbake: toaster: Update JS unit tests
ab896df bitbake: toaster: Fix stale layer state buttons
41a5f82 bitbake: toaster: tables Add the recipe filter back into the Recipe table
2bebcd4 bitbake: toaster: Fix typo in returning pk list of layer versions in current project
d6d680d bitbake: toaster: layerdetails update build recipe button class name
7794b57 bitbake: toaster: Hide "Download build log" button if log doesn't exist
8c69539 bitbake: toaster: fix naming for clone directory
41286f4 bitbake: toaster: buildinfohelper Skip packages we have no build info about
97d0006 bitbake: toaster: buildinfohelper associate build data with built_recipe
0dcc963 bitbake: toaster: remove bashisms so script works in dash as well
8068aa3 bitbake: toaster: get rid of interactivity in bldcontrol
7d7823e bitbake: toaster: check for configuration file and exit if not found
315989c bitbake: toaster: remove layer and build dir interactive questions
489d5ff bitbake: toaster: removed superuser question from startup
c7d1dab bitbake: toaster: orm Machines filter don't pass self in as parameter
dd957fe bitbake: toaster: Rationalise mimetype guessing to fix artifact downloads
ce9011a bitbake: toaster: Use Python's mimetypes module
466bbec bitbake: toaster: display warnings for bad "IMAGE_FSTYPES" values
8b7d846 bitbake: toaster: Set default columns in recipes tables
9daf6ef bitbake: toaster: Comment out broken sorting and filters
b661f53 bitbake: toaster: Don't HTTP cache ToasterTable responses
a3742a0 bitbake: toaster: Don't add new history entries when table data loads
fa68ae0 bitbake: toaster: use meaningful logging levels
bd8b27b bitbake: toaster: ignore ReachableStamps event
ceeb52a linux-yocto: Update SRCREV for genericx86* BSPs
7766265 os-release: fix do_compile() when RPM signing is enabled
9a02df0 readline: actually apply readline63-003 (aka CVE-2014-2524)
a856580 rpm: fix return without value in patch
49bf4b1 Revert "qemu-native: Enable temporary debug info as default."
ad8c021 linux-yocto/4.1: drm/i915 backports
48e5579 oeqa/utils/qemurunner: Add support for Unicode from qemu
1f99452 report-error.bbclass: Support Unicode reports
b25af33 udev: add PROVIDES = "libgudev"
a0d9d2d lib/oe/image.py: Add image generation for companion debug filesystem
8ee9a93 package_manager.py: sort output of OpkgPkgsList().list
37c54af ThunderX: Add initial tune file
a0e7311 tzdata: update to 2015g
931dda4 tzcode: update to 2015g
8cacd22 recipetool: create: fix change in path structure if --extract-to path exists
e961688 devtool: update-recipe: avoid updating patches that have not changed
07fc8c2 oe-selftest: wic: fix LocalSetup
eac61f3 build-appliance-image: Update to jethro head revision
c9bdcf5 oeqa/runexported: Replaced optionparser with argparse.
038ae3f systemd: remove glib-2.0 build dependency
0516cd2 webkitgtk: Add some PACKAGECONFIG options.
dff30d2 fontcache: allow to pass extra parameters and environment to fc-cache
d5ce2f5 webkitgtk: Use ON/OFF for cmake switches.
ebd5035 testimage: Added IO commands to dumps
b73a35e distro-alias.inc: Updated for jethro 2.0 release
b7f9cde build-appliance-image: Update to jethro head revision
cf8ad8d toaster: Special case the openembedded-core layer to avoid duplicates
20b888b build-appliance-image: Update to jethro head revision
8fb5a5a bitbake: bitbake/lib: Update version to 1.28.0
0eca7ff build-appliance-image: Update to jethro head revision
34fede6 poky.conf: Bump version for 2.0 jethro release
a7329e1 Revert "oeqa/runtime: Added one runtime testcase in connman."
c2e78e3 qemu: Drop BROKEN usage
e788961 smart:cache.py: getPackages() matches name + arch
f3e57ba devtool: modify: use correct local files directory name
7cb0765 xuser-account: Take over xuser specific D-Bus policy
cdaa8fd bluez5: Use upstream D-Bus policy
e4a4961 ptest: run-ptest not required to run do_install_ptest
12cd705 distrodata: Take account proxies on distrodata tasks
f047ee8 devtool: update-recipe: enable var history tracking
979de77 lib/oeqa/selftest/yoctobsp: Basic tests for yocto-bsp script
e20d8b8 scripts/lib/bsp/engine: Indent the karch properties when stored into a file
f2933cc yocto-bsp: Update templates to 4.1 kernel
8283a57 scrips/lib/bsp/engine: List properties to stdout when output parameter is omitted
b355a5e scripts/yocto-bsp: Exit successfully when asking for help
ad9ee3d meta-yocto-bsp: bump to linux-yocto 4.1 for the non-x86 BSPs
cdc57f6 bitbake: siggen: Make it clear why nostamp tasks signatures don't match
1630f0a bitbake: runqueue: Add handling of virtual/xxx provider mappings
0b96e6f bitbake: taskdata: Add a function to return the virtual/ mapping data
40fae32 bitbake: cookerdata: Rename BBPKGS -> BBTARGETS
1e467b3 bitbake: bitbake-worker: Guard against multiprocessing corruption of event data
e5b9c2a oeqa/selftest/wic: Use SetupLocal instead of Setup
4266cc9 kernel.bbclass: fix the bug of checking the existing sections in do_strip()
ec1146e linux-yocto_{3.14,3.19,4.1}: qemuarm enable virtio drivers
2ea0e4c runqemu-internal: qemuarm enable usage of virtio devices
a23239a gnome-doc-utils: xslt - don't install Makefiles
f671163 apr-utils: cleanup buildpaths for target stuffs
f68d739 apr: cleanup buildpaths from target stuffs
a7ac905 curl: cleanup buildpaths from curl-config
833bfd3 dropbear: fix key generation when systemd is in use and rootfs is readonly
d592abd image.bbclass: tweak the key location for dropbear when rootfs is readonly
299806d openssh: fix sshd key generation when systemd is in use and rootfs is readonly
006497e image.bbclass: when building a readonly rootfs, tweak ssh settings regardless of init system in use
f1e2515 lttng-tools: Drop KERNELDIR reference
381a7bd meta-ide-support: No need to mark as nostamp anymore
ab9d2bb adt-installer: No need to mark as nostamp
d8ab563 distutils3: Avoid MACHINE specific checksums
a0d6322 gstreamer-omx: Improve variable expansion of ${S}
c71bd57 bitbake.conf: Exclude sstate-outputdirs flag from checksums
f02cbc6 deploy: Mark deploy tasks as MACHINE specific
a0435bf layer.conf: Add SIGGEN exclusion for oprofile kernel dependency
f4a8917 layer.conf: Improve siggen exclusion to handle virtual/libc
6fe4fd2 multilib_global: Add handling of SIGGEN variables for multilib
2c19695 lib/oe/sstate: Add tasks_resolved handler for virtual/xxx mappings
ff17f14 oeqa/selftest/sstatetests: Add test that MACHINE doesn't change target sigs
d822764 meta-selftest: Add qemux86copy machine
6cfc7c0 oeqa/selftest/sstatetests: Add check for same sigs for SDKMACHINE
5dbd061 multilib.conf: Ensure MACHINE doesn't change target sigs
71fdb36 gcc-multilib-config: Ensure SDK_ARCH doesn't change target sigs
c9ea0c6 lib/oe/package_manager: Handle empty package list in opkg case
ec504e0 oeqa/utils/decorators: Append the testname without the full path
8fe5b48 kern-tools: fix multi-layer patch application
b054506 linux-yocto/4.1: braswell bug fixes
c6c093b linux-yocto/4.1: update to 4.1.8 -stable
a502a2d linux-yocto-rt/4.1: integrate axxia BSP
38f0ffa meta: fix build with gettext 0.16.1
56c0fdf hostap-utils: Use C99 stddefs in defining local typedefs
34707c2 linux-yocto-custom: Update for newer kernel
df09a6f oetest: Change logic of a failed test
7a6cb2e cwautomacros: cleanup buildpath in autogen.sh
1222eb1 oeqa/runexported: Fix a problem with ssh_target_log existing in folder.
cb93670 qemurunner: Sanitize output from qemu and qemu pid
ba0f6ca oeqa/testimage: Add ability to run single test from suite.
3e40688 recipes-extended: remove duplicate recipe and .wks
6f2047a runqemu-internal: Make sure two serial ports always exist
385a5e8 cross-canadian.bbclass: big-endian ARM is also gnueabi.
7c96fcf openssl: fix ptest failures
d9ce095 python-async: inherit setuptools
adb6987 util-linux: add runuser PAM config files to fix runuser error
9549f57 oeqa/decorators: Fixed a problem with decorator logs link.
790b6c7 oeqa/selftest/wic: Added testcase decorator to all testcases + fixed minor typos.
ffd4bd6 toolchain-shar-extract: Correct environment-setup script names for multilib
249b810 lsb: add lsbinitscripts and util-linux rdepends
c7548b5 systemd: add PACKAGECONFIG for qrencode
3b04553 opkg: create opkg.lock in /run instead of /var/run
c275627 toolchain-shar-relocate.sh: make it faster
434665d populate_sdk_base: Simplify postprocess commands
5bfcd13 classes/meta: Add DISTRO_FEATURES check for gtk+/gtk3+
5b629a9 devtool: modify: make bitbake use local files from srctree
e9bae50 devtool: better support for local source files
a74fa38 devtool: file mover function that creates target dir
109c09b devtool: update_recipe: refactor patch generation
c976028 devtool: update-recipe: add new patches in correct order
2f8440b oe-selftest: devtool: add method for checking repo status
0a9f59e oe-selftest: devtool: add method for checking srctree repo
afb0142 oe-selftest: devtool: add setup() method
31c3078 oe.patch.GitApplyTree: add paths argument to extractPatches
d5e2dd4 recipeutils: implement get_recipe_local_files()
4bc3f09 bitbake: toaster: move clones into subdirectory
9e1516d bitbake: toaster: make clone directory name unique
552fd83 bitbake: toaster: fix reimporting module
55dc927 bitbake: toaster: fix bug in resetting git repository
6939340 bitbake: toaster: use git reset --hard instead of rebase
3d73dfa bitbake: toaster: don't use --single-branch when cloning
226e7da bitbake: utils: only add layer once in edit_bblayers_conf()
d48b7ef bitbake: toaster: display most recent builds for projects
f902dc6 bitbake: toaster: orm remove the complicated querying on the ORM
fe29297 bitbake: Revert "bitbake: toaster: don't re-create Target objects"
e6d967d bitbake: toaster: buildinfohelper Create a copy of the built layer and recipe
17fe16b bitbake: toaster: tables show all recipes in the layerdetails even duplicates
aed6d2e bitbake: toaster: Prioroitise the layer more generic vcs reference over the sha
922503f bitbake: toaster: Create a relationship between build information and toaster layers
0bc0a44 bitbake: toaster: Special case the openembedded-core layer to avoid duplicates
e68f63a bitbake: toaster: Add test cases for new Image customisation features
d98c771 bitbake: toaster: Add Image customisation frontend feature
37948cc bitbake: toaster: Add ToasterTables for Image customisation feature
a3ff4b2 bitbake: toaster: Add new ReST API for Image Customisation feature
28153ac bitbake: toaster: Fix indentation of jsunittests view
60f3ddb bitbake: toaster: implement decorator for REST responses
a7f43bd bitbake: toaster: add toggle for enabling image customisation feeature
3ff6401 bitbake: toaster: Add CustomImageRecipe model
8948d04 bitbake: toaster: ToasterTable remove unused class definition
c1157cf bitbake: toaster: add nocache option to the ToasterTable widget
1cafc39 bitbake: toaster: widgets ToasterTable Add more info to search field exception
c71bbad bitbake: toaster: widgets ToasterTable add logger to notify when cache hit
934f8d7 bitbake: toaster: create custom layer and recipes for Image customisation
340b398 bitbake: toaster: tables Move the title and name into the widget
e1851fe bitbake: toaster: make a workaround for old style index
f78f902 bitbake: prserv/serv.py: Better messaging when starting/stopping the server with port=0
134b267 bitbake: prserv/serv: Close the DB connection out of class destructor
caf422c multilib: Add TARGET_VENDOR to saved variables list
3af9f06 oeqa/sdk/gcc: Fix makefile test
00f0d2b gdk-pixbuf: Only apply native cleaning in normal task, not setscene
452237b runqemu-export-rootfs: update location of unfsd binary
aa1253f runqemu: don't complain about conflicting machines if they are equal
994915b oeqa/testimage: Remove absolute path to oeqa from json
f8da3b6 iproute2: fix the configure process
218d9f4 gcc-multilib-config: Expand ccargs variable
be13cdb Empty image: core-image-empty recipe
2bbec56 Empty image:rootfs.py:handle empty PACKAGE_INSTALL
4562f3f gstreamer1.0-plugins-bad: change glimagesink rank to marginal
677a463 linux-yocto/4.1: rt update to 4.1.x-rt8
cdd9c4c linux-yocto/4.1: common-pc-drivers: add CONFIG_PATA_SCH
9028d93 ltp: replace 'inline' with 'static inline' for gcc 5.x
5942dfe waffle: Fix build with musl
cfa3ed0 cups: fix pam configuration file's permission
8227d49 busybox: Use CC instead of bare LD to be the Linker
a3c4817 busybox: Use UTMPX instead of legacy UTMP
ea031f0 distrodata: handle recipes with empty or absent SRC_URI in checkpkg()
5cc44fe recipeutils.py: don't hardcode the upstream version as 1.0 when SRC_URI is empty or absent
320500e oeqa/parselogs: Updated log parser whitelist.
adeba9a connman: Don't use a blanket "allow" D-Bus policy
907c8a7 connman: Depend on xuser-account unconditionally
1b146c5 byacc: add missing patch header
5fd3089 sstate: run recipe-provided hooks outside of ${B}
3fb464f oeqa/decorators: Add timestamp to decorator logs.
5f371e5 image types: add hdddirect
ca52ca0 packagegroup-core-standalone-sdk-target: ensure libatomic is in SDK
6d68ba9 glibc/mmc-utils: Rename 'BRANCH' variable to 'SRCBRANCH' for clearness
c5aab3f sanity.bbclass: show warning when chmod fails
5702a19 systemd: apply persistent storage udev rules also for /dev/hd*
cb24cbb rpm: search for gpg if gpg2 is not found
217cccd openssl: Add mapping for nios2
3408d0d qemurunner: Handle qemu start failure correctly
79e3418 gcc-runtime: Add multilib C++ header mapping
09af262 oeqa/oetest: Fix SDK command execution
5d4f39f mulitlib: Ensure SDKTARGETSYSROOT is set correctly
c356961 gtk-icon-cache/pixbufcache: don't set GDK_PIXBUF_MODULEDIR
4a36842 librsvg: tell configure where gdk-pixbuf-query-loaders is
8a12632 gdk-pixbuf: move gdk-pixbuf-query-loaders to $libdir for multilib safety
b070778 gdk-pixbuf: move gdk-pixbuf-pixdata to gdk-pixbuf-dev
7fb583a multilib: Drop populate_sdk variable manipulation
eb7b1a5 package_manager.py: make rpm install mutilib pkgs corectly
5a51fb2 bitbake: prserv/serv: Start/Stop daemon using ip instead of host
2687b24 gdk-pixbuf: Avoid rebuild failures
94184a0 systemd: fix tmpfiles location when multilib in use
179ee77 p11-kit: configure without trust-paths
c7624b4 oe-pkgdata-util: avoid returning skipped packages
dd11f5c toolchain-shar-extract.sh: remove checkbashism
99fc786 archiver: stamp-base is dead, remove it
ce7bc12 gcc-shared-source: Set empty SRC_URI
47ef201 libgcc.inc: package baremetal multilib libraries
aff7e72 meta-selftest: add error recipe and error-image
261e68c libksba: fix pkgconfig patch
3235a64 systemd: disable problematic GCC 5.2 optimizations
6e7ed5e Revert "systemd: disable problematic GCC 5.2 optimizations"
9673278 oeqa/selftest/archiver: Test that archiver filters on recipe name
6807327 oeqa/utils/dump: Add default commands and directory
5d31e94 webkitgtk: add REQUIRED_DISTRO_FEATURES
8733b53 oeqa/runexported: Removed DEPLOY_DIR as mandatory.
f1e7fb0 oeqa/oetest: Remove bb as requirement for oetest.
d70c5cb gcc-5.2: disable isl
66dca4b kmod: Change SRCREV to fix return code in error path
61e77c7 oeqa/runtime/parselogs.py: Fix dmesg log retrieve in sato
dd26efb insane.bbclass: make package_qa_clean_path return a relative path
bdbd8b4 devtool: upgrade: use shutil.move instead of os.rename
346784b devtool: runqemu: avoid recipe parse
85d8b4a devtool: second fix for running from a different directory
6363a95 guile: cleanup buildpaths and add RDEPENDS on pkgconfig
6d1447b gmp: Use __gnu_inline__ attribute in 4.2.1
42dc902 pseudo_1.7.4.bb: fix f*open()
9f66aa1 bitbake: toaster: start script warning text formatting small improvement
c6eaef0 bitbake: tinfoil: remove logging handler at shutdown
fb26ea3 bitbake: toaster: remove time from builds in progress
15b482b bitbake: toaster: Add fake entry to Target_File for filesystem root
767fe69 bitbake: toaster: layerdetails Fix back button tab behaviour
4c0320f bitbake: toaster: UI test improvements
4c5af77 bitbake: toaster: support selenium testing from mac OS X
e6c4970 bitbake: toaster: add 2 UI tests
f6a70ad bitbake: toaster: change UI to show tasks
08000eb bitbake: toaster: don't re-create Target objects
ea37358 bitbake: toaster: store task name in Target objects
524ddd8 oeqa/utils/qemurunner.py: Remove duplicate message on LoggingThread start
376ce71 oeqa/utils/qemurunner.py: Fix HIGH CPU usage on LoggingThread
6c0066c devtool: add search command
0613301 devtool: add basic means of running runqemu within the extensible SDK
c4181c6 devtool / recipetool: add handling for binary-only packages
76084cd devtool: build-image: delete bbappend at end of build
ef197f9 devtool: build-image: improve image recipe handling
8f67bb7 devtool: build-image: tell user where to find output files
afb9340 devtool: build-image: fix recipe/package terminology
d736518 devtool: add: move important "recipe created" message to the end
3bd0f33 devtool: add: set up fetched source as a git repository by default
e759b0b devtool: better handling for recipes that don't unpack source
a34f733 devtool: fix extracting source for work-shared recipes
5bc437b devtool: show proper error when extracting source for recipes with disabled unpack task
210d959 recipetool: create: fix handling of URIs containing #
a35ad72 recipetool: create: fix creating empty shell functions
30c7e7a devtool: add: properly handle separate build directory
99fc284 devtool / lib/oe/recipeutils: ensure we can parse without bbappends
5d1a117 devtool: add: ensure --color=never turns off recipetool colour output
ae788fb devtool: check that source tree still exists
99cd79d scripts/contrib: add devtool stress tester
e0b9a96 lib/oe/patch: fix for git am not cleaning up after itself
8fb70c6 classes/externalsrc: fix setting of deps varflag as a string
586291f classes/externalsrc: scale back warning to a plain note
72810f9 toolchain-shar-extract.sh: show progress when extracting SDK
0dc9299 classes/populate_sdk_ext: drop work-config.inc
3a08728 classes/populate_sdk_ext: allow custom configuration for extensible SDK
b853dde classes/populate_sdk_ext: fix missing environment settings if running installer with sh
374e1fe lib/oe/recipeutils: properly split unexpanded variable values
7fb3fb9 linux-yocto/4.1: hid, bluetooth, aufs and yaffs2 updates
9241ec5 image_types.bbclass: Don't try to create ubi symlink twice
266e417 oeqa/selftest: buildoptions.py Removed unused imports
329d09f systemd: disable problematic GCC 5.2 optimizations
554c817 libgpg-error: Add support for nios2
84e1100 pixman: Fix missing FE_DIVBYZERO on nios2
9baffc1 libtool: Fix nios2 support
ba1e0ee linux-yocto: depend on libgcc for nios2
8efff24 kernel-arch: Add nios2 to valid archs
4d9af35 siteinfo: Add nios2-linux
76a8c74 insane: Add nios2 support
6adffd0 autotools: fix traversal bug in aclocal copying
6a02bbd python3-debugger: Adds pkgutils dependency to pdb
a7dd758 python3-debugger: fix importlib dependency
0e5a911 libsdl: depends on libglu when both x11 and opengl
d762ea1 lttng-tools: sessiond: disable: match app event by name
c8a7d76 testimage.bbclass: Fix break introduced with SIGTERM handling
7d166a6 sysstat: Include needed headers explicitly
d36384e connman: Fix build with musl
0df9b98 quota: Replace using -I= with STAGING_INCDIR
433a7a0 opkg: Include stdio.h for FILE definition
5aadabf syslinux: Dont bypass gcc driver for dependency generation options
05b9a0c gnu-efi, syslinux: Support gcc < 4.7
cdfd96e gummiboot: Fix build warnings seen with gcc5
0141652 qt4: Fix kmap2qmap build with clang
6b73a05 xz: Correctly specify GPL-3.0 with autoconf exception
a96069d insane.bbclass: drop extra line-feed in pkgname check
10fb575 insane.bbclass: show PN and relative path in package_qa_check_host_user
5624889 package.bbclass: add summary line to installed-vs-shipped QA check
d6e40e8 initramfs-framework: better error reporting for invalid root boot parameter
288a9ff initramfs-framework: fix "support dropping into shell on failure"
5ff7e8d qt4: remove already merged patch
9578b09 gdk-pixbuf: remove redundant libx11 DEPENDS line
fe70aa4 runqemu-internal: For qemumicroblaze use the QEMU provided device tree
9aaf7e3 runqemu-internal: Fix qemu networking for qemuzynq an qemumicroblaze
be493ba libpcre: Allow building 16 and 32bit libpcre versions
f32a6e1 oe-git-proxy: Allow socks4 as protocol in $ALL_PROXY
18309f0 oe-git-proxy: Correct the parsing of a port in $ALL_PROXY
c035f35 oe-git-proxy: Allow explicit IP addresses in $NO_PROXY
bbe06b4 oeqa/testimage: Enhance -v switch in testimage
e0b38f2 wic-image-minimal: add dependency to .wks
dd7726f wic: fix partition size calculation
219d73a wic: use ext4 in wic-image-minimal.wks
ce2cb45 wic: add dependencies to wic-image-minimal recipe
a66f586 testimage.bbclass: Don't require an image manifest
39c11d8 gstreamer1.0: Fix basesink drop buffer error
5f13793 grep: fix install if bindir == base_bindir
b17c02f gzip: fix install if bindir == base_bindir
b6f8ea1 cpio: fix install if bindir == base_bindir
fe0cdab tar: fix install if bindir == base_bindir
c6b52f3 bind: fix too long error from gen
81d65df ccache: fix file name too long
cdbe5c9 bitbake.conf: update APACHE_MIRROR
12772c8 linux-yocto/4.1: hid-core: Avoid uninitialized buffer access
88b11e6 kern-tools: optimize patching peformance
0864782 linux-yocto/4.1: aufs, yaffs2 and driver fixes

git-subtree-dir: yocto-poky
git-subtree-split: c8a4ed9a63de6124c8a3cceb80c7db48f12f7aea
diff --git a/bitbake/lib/bb/__init__.py b/bitbake/lib/bb/__init__.py
index 1f7946e..ac62d26 100644
--- a/bitbake/lib/bb/__init__.py
+++ b/bitbake/lib/bb/__init__.py
@@ -21,7 +21,7 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-__version__ = "1.27.1"
+__version__ = "1.28.0"
 
 import sys
 if sys.version_info < (2, 7, 3):
diff --git a/bitbake/lib/bb/build.py b/bitbake/lib/bb/build.py
index 948c395..22428a6 100644
--- a/bitbake/lib/bb/build.py
+++ b/bitbake/lib/bb/build.py
@@ -413,6 +413,13 @@
         nice = int(nice) - curnice
         newnice = os.nice(nice)
         logger.debug(1, "Renice to %s " % newnice)
+    ionice = localdata.getVar("BB_TASK_IONICE_LEVEL", True)
+    if ionice:
+        try:
+            cls, prio = ionice.split(".", 1)
+            bb.utils.ioprio_set(os.getpid(), int(cls), int(prio))
+        except:
+            bb.warn("Invalid ionice level %s" % ionice)
 
     bb.utils.mkdirhier(tempdir)
 
diff --git a/bitbake/lib/bb/command.py b/bitbake/lib/bb/command.py
index 398c1d6..74106d1 100644
--- a/bitbake/lib/bb/command.py
+++ b/bitbake/lib/bb/command.py
@@ -181,6 +181,16 @@
         value = str(params[1])
         command.cooker.data.setVar(varname, value)
 
+    def getSetVariable(self, command, params):
+        """
+        Read the value of a variable from data and set it into the datastore
+        which effectively expands and locks the value.
+        """
+        varname = params[0]
+        result = self.getVariable(command, params)
+        command.cooker.data.setVar(varname, result)
+        return result
+
     def setConfig(self, command, params):
         """
         Set the value of variable in configuration
diff --git a/bitbake/lib/bb/cooker.py b/bitbake/lib/bb/cooker.py
index a0d7d59..4df8881 100644
--- a/bitbake/lib/bb/cooker.py
+++ b/bitbake/lib/bb/cooker.py
@@ -255,6 +255,11 @@
         self.state = state.initial
         self.caches_array = []
 
+        # Need to preserve BB_CONSOLELOG over resets
+        consolelog = None
+        if hasattr(self, "data"):
+            consolelog = self.data.getVar("BB_CONSOLELOG", True)
+
         if CookerFeatures.BASEDATASTORE_TRACKING in self.featureset:
             self.enableDataTracking()
 
@@ -281,6 +286,8 @@
         self.data = self.databuilder.data
         self.data_hash = self.databuilder.data_hash
 
+        if consolelog:
+            self.data.setVar("BB_CONSOLELOG", consolelog)
 
         # we log all events to a file if so directed
         if self.configuration.writeeventlog:
@@ -531,6 +538,11 @@
         for o in options:
             if o in ['prefile', 'postfile']:
                 clean = False
+                server_val = getattr(self.configuration, "%s_server" % o)
+                if not options[o] and server_val:
+                    # restore value provided on server start
+                    setattr(self.configuration, o, server_val)
+                    continue
             setattr(self.configuration, o, options[o])
         for k in bb.utils.approved_variables():
             if k in environment and k not in self.configuration.env:
@@ -1391,10 +1403,28 @@
         build.reset_cache()
         self.buildSetVars()
 
+        # If we are told to do the None task then query the default task
+        if (task == None):
+            task = self.configuration.cmd
+
+        if not task.startswith("do_"):
+            task = "do_%s" % task
+
         taskdata, runlist, fulltargetlist = self.buildTaskData(targets, task, self.configuration.abort)
 
         buildname = self.data.getVar("BUILDNAME", False)
-        bb.event.fire(bb.event.BuildStarted(buildname, fulltargetlist), self.data)
+
+        # make targets to always look as <target>:do_<task>
+        ntargets = []
+        for target in fulltargetlist:
+            if ":" in target:
+                if ":do_" not in target:
+                    target = "%s:do_%s" % tuple(target.split(":", 1))
+            else:
+                target = "%s:%s" % (target, task)
+            ntargets.append(target)
+
+        bb.event.fire(bb.event.BuildStarted(buildname, ntargets), self.data)
 
         rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
         if 'universe' in targets:
diff --git a/bitbake/lib/bb/cookerdata.py b/bitbake/lib/bb/cookerdata.py
index f19c283..671c0cb 100644
--- a/bitbake/lib/bb/cookerdata.py
+++ b/bitbake/lib/bb/cookerdata.py
@@ -63,9 +63,9 @@
             raise Exception("Unable to set configuration option 'cmd' on the server: %s" % error)
 
         if not self.options.pkgs_to_build:
-            bbpkgs, error = server.runCommand(["getVariable", "BBPKGS"])
+            bbpkgs, error = server.runCommand(["getVariable", "BBTARGETS"])
             if error:
-                raise Exception("Unable to get the value of BBPKGS from the server: %s" % error)
+                raise Exception("Unable to get the value of BBTARGETS from the server: %s" % error)
             if bbpkgs:
                 self.options.pkgs_to_build.extend(bbpkgs.split())
 
@@ -129,6 +129,8 @@
         self.extra_assume_provided = []
         self.prefile = []
         self.postfile = []
+        self.prefile_server = []
+        self.postfile_server = []
         self.debug = 0
         self.cmd = None
         self.abort = True
diff --git a/bitbake/lib/bb/fetch2/__init__.py b/bitbake/lib/bb/fetch2/__init__.py
index 288a1c8..a9c044b 100644
--- a/bitbake/lib/bb/fetch2/__init__.py
+++ b/bitbake/lib/bb/fetch2/__init__.py
@@ -955,7 +955,7 @@
                 origud.method.download(origud, ld)
                 if hasattr(origud.method,"build_mirror_data"):
                     origud.method.build_mirror_data(origud, ld)
-            return ud.localpath
+            return origud.localpath
         # Otherwise the result is a local file:// and we symlink to it
         if not os.path.exists(origud.localpath):
             if os.path.islink(origud.localpath):
diff --git a/bitbake/lib/bb/fetch2/hg.py b/bitbake/lib/bb/fetch2/hg.py
index bbb4ed9..3b743ff 100644
--- a/bitbake/lib/bb/fetch2/hg.py
+++ b/bitbake/lib/bb/fetch2/hg.py
@@ -28,6 +28,7 @@
 import sys
 import logging
 import bb
+import errno
 from bb import data
 from bb.fetch2 import FetchMethod
 from bb.fetch2 import FetchError
diff --git a/bitbake/lib/bb/fetch2/svn.py b/bitbake/lib/bb/fetch2/svn.py
index 1733c2b..8a29193 100644
--- a/bitbake/lib/bb/fetch2/svn.py
+++ b/bitbake/lib/bb/fetch2/svn.py
@@ -54,6 +54,11 @@
 
         ud.module = ud.parm["module"]
 
+        if not "path_spec" in ud.parm:
+            ud.path_spec = ud.module
+        else:
+            ud.path_spec = ud.parm["path_spec"]
+
         # Create paths to svn checkouts
         relpath = self._strip_leading_slashes(ud.path)
         ud.pkgdir = os.path.join(data.expand('${SVNDIR}', d), ud.host, relpath)
@@ -102,7 +107,7 @@
 
             if command == "fetch":
                 transportuser = ud.parm.get("transportuser", "")
-                svncmd = "%s co %s %s://%s%s/%s%s %s" % (ud.basecmd, " ".join(options), proto, transportuser, svnroot, ud.module, suffix, ud.module)
+                svncmd = "%s co %s %s://%s%s/%s%s %s" % (ud.basecmd, " ".join(options), proto, transportuser, svnroot, ud.module, suffix, ud.path_spec)
             elif command == "update":
                 svncmd = "%s update %s" % (ud.basecmd, " ".join(options))
             else:
@@ -149,7 +154,7 @@
 
         os.chdir(ud.pkgdir)
         # tar them up to a defined filename
-        runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.module), d, cleanup = [ud.localpath])
+        runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.path_spec), d, cleanup = [ud.localpath])
 
     def clean(self, ud, d):
         """ Clean SVN specific files and dirs """
diff --git a/bitbake/lib/bb/main.py b/bitbake/lib/bb/main.py
index 8762f72..c8530fc 100755
--- a/bitbake/lib/bb/main.py
+++ b/bitbake/lib/bb/main.py
@@ -383,6 +383,13 @@
         # Collect the feature set for the UI
         featureset = getattr(ui_module, "featureSet", [])
 
+    if configParams.server_only:
+        for param in ('prefile', 'postfile'):
+            value = getattr(configParams, param)
+            if value:
+                setattr(configuration, "%s_server" % param, value)
+                param = "%s_server" % param
+
     if not configParams.remote_server:
         # we start a server with a given configuration
         server = start_server(servermodule, configParams, configuration, featureset)
diff --git a/bitbake/lib/bb/runqueue.py b/bitbake/lib/bb/runqueue.py
index 2b71eed..878028a 100644
--- a/bitbake/lib/bb/runqueue.py
+++ b/bitbake/lib/bb/runqueue.py
@@ -797,6 +797,15 @@
                         st = "do_%s" % st
                     invalidate_task(fn, st, True)
 
+        # Create and print to the logs a virtual/xxxx -> PN (fn) table
+        virtmap = taskData.get_providermap()
+        virtpnmap = {}
+        for v in virtmap:
+            virtpnmap[v] = self.dataCache.pkg_fn[virtmap[v]]
+            bb.debug(2, "%s resolved to: %s (%s)" % (v, virtpnmap[v], virtmap[v]))
+        if hasattr(bb.parse.siggen, "tasks_resolved"):
+            bb.parse.siggen.tasks_resolved(virtmap, virtpnmap, self.dataCache)
+
         # Iterate over the task list and call into the siggen code
         dealtwith = set()
         todeal = set(range(len(self.runq_fnid)))
diff --git a/bitbake/lib/bb/siggen.py b/bitbake/lib/bb/siggen.py
index 2985272..0352e45 100644
--- a/bitbake/lib/bb/siggen.py
+++ b/bitbake/lib/bb/siggen.py
@@ -80,6 +80,7 @@
         self.taskdeps = {}
         self.runtaskdeps = {}
         self.file_checksum_values = {}
+        self.taints = {}
         self.gendeps = {}
         self.lookupcache = {}
         self.pkgnameextract = re.compile("(?P<fn>.*)\..*")
@@ -199,11 +200,14 @@
         if 'nostamp' in taskdep and task in taskdep['nostamp']:
             # Nostamp tasks need an implicit taint so that they force any dependent tasks to run
             import uuid
-            data = data + str(uuid.uuid4())
+            taint = str(uuid.uuid4())
+            data = data + taint
+            self.taints[k] = "nostamp:" + taint
 
         taint = self.read_taint(fn, task, dataCache.stamp[fn])
         if taint:
             data = data + taint
+            self.taints[k] = taint
             logger.warn("%s is tainted from a forced run" % k)
 
         h = hashlib.md5(data).hexdigest()
@@ -247,6 +251,10 @@
         if taint:
             data['taint'] = taint
 
+        if runtime and k in self.taints:
+            if 'nostamp:' in self.taints[k]:
+                data['taint'] = self.taints[k]
+
         fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(sigfile), prefix="sigtask.")
         try:
             with os.fdopen(fd, "wb") as stream:
diff --git a/bitbake/lib/bb/taskdata.py b/bitbake/lib/bb/taskdata.py
index 5fab704..4d12b33 100644
--- a/bitbake/lib/bb/taskdata.py
+++ b/bitbake/lib/bb/taskdata.py
@@ -612,6 +612,18 @@
                 break
         # self.dump_data()
 
+    def get_providermap(self):
+        virts = []
+        virtmap = {}
+
+        for name in self.build_names_index:
+            if name.startswith("virtual/"):
+                virts.append(name)
+        for v in virts:
+            if self.have_build_target(v):
+                virtmap[v] = self.fn_index[self.get_provider(v)[0]]
+        return virtmap
+
     def dump_data(self):
         """
         Dump some debug information on the internal data structures
diff --git a/bitbake/lib/bb/tests/utils.py b/bitbake/lib/bb/tests/utils.py
index 9171509..a035ccf 100644
--- a/bitbake/lib/bb/tests/utils.py
+++ b/bitbake/lib/bb/tests/utils.py
@@ -376,3 +376,206 @@
         (updated, newlines) = bb.utils.edit_metadata(self._origfile.splitlines(True), varlist, handle_var)
         self.assertTrue(updated, 'List should be updated but isn\'t')
         self.assertEqual(newlines, newfile5.splitlines(True))
+
+
+class EditBbLayersConf(unittest.TestCase):
+
+    def _test_bblayers_edit(self, before, after, add, remove, notadded, notremoved):
+        with tempfile.NamedTemporaryFile('w', delete=False) as tf:
+            tf.write(before)
+            tf.close()
+            try:
+                actual_notadded, actual_notremoved = bb.utils.edit_bblayers_conf(tf.name, add, remove)
+                with open(tf.name) as f:
+                    actual_after = f.readlines()
+                self.assertEqual(after.splitlines(True), actual_after)
+                self.assertEqual(notadded, actual_notadded)
+                self.assertEqual(notremoved, actual_notremoved)
+            finally:
+                os.remove(tf.name)
+
+
+    def test_bblayers_remove(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/subpath/layer3 \
+  /home/user/path/layer4 \
+  "
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/subpath/layer3 \
+  /home/user/path/layer4 \
+  "
+"""
+        self._test_bblayers_edit(before, after,
+                                 None,
+                                 '/home/user/path/layer2',
+                                 [],
+                                 [])
+
+
+    def test_bblayers_add(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/subpath/layer3 \
+  /home/user/path/layer4 \
+  "
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/subpath/layer3 \
+  /home/user/path/layer4 \
+  /other/path/to/layer5 \
+  "
+"""
+        self._test_bblayers_edit(before, after,
+                                 '/other/path/to/layer5/',
+                                 None,
+                                 [],
+                                 [])
+
+
+    def test_bblayers_add_remove(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/subpath/layer3 \
+  /home/user/path/layer4 \
+  "
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/layer4 \
+  /other/path/to/layer5 \
+  "
+"""
+        self._test_bblayers_edit(before, after,
+                                 ['/other/path/to/layer5', '/home/user/path/layer2/'], '/home/user/path/subpath/layer3/',
+                                 ['/home/user/path/layer2'],
+                                 [])
+
+
+    def test_bblayers_add_remove_home(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  ~/path/layer1 \
+  ~/path/layer2 \
+  ~/otherpath/layer3 \
+  ~/path/layer4 \
+  "
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS = " \
+  ~/path/layer2 \
+  ~/path/layer4 \
+  ~/path2/layer5 \
+  "
+"""
+        self._test_bblayers_edit(before, after,
+                                 [os.environ['HOME'] + '/path/layer4', '~/path2/layer5'],
+                                 [os.environ['HOME'] + '/otherpath/layer3', '~/path/layer1', '~/path/notinlist'],
+                                 [os.environ['HOME'] + '/path/layer4'],
+                                 ['~/path/notinlist'])
+
+
+    def test_bblayers_add_remove_plusequals(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS += " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  "
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS += " \
+  /home/user/path/layer2 \
+  /home/user/path/layer3 \
+  "
+"""
+        self._test_bblayers_edit(before, after,
+                                 '/home/user/path/layer3',
+                                 '/home/user/path/layer1',
+                                 [],
+                                 [])
+
+
+    def test_bblayers_add_remove_plusequals2(self):
+        before = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS += " \
+  /home/user/path/layer1 \
+  /home/user/path/layer2 \
+  /home/user/path/layer3 \
+  "
+BBLAYERS += "/home/user/path/layer4"
+BBLAYERS += "/home/user/path/layer5"
+"""
+        after = r"""
+# A comment
+
+BBPATH = "${TOPDIR}"
+BBFILES ?= ""
+BBLAYERS += " \
+  /home/user/path/layer2 \
+  /home/user/path/layer3 \
+  "
+BBLAYERS += "/home/user/path/layer5"
+BBLAYERS += "/home/user/otherpath/layer6"
+"""
+        self._test_bblayers_edit(before, after,
+                                 ['/home/user/otherpath/layer6', '/home/user/path/layer3'], ['/home/user/path/layer1', '/home/user/path/layer4', '/home/user/path/layer7'],
+                                 ['/home/user/path/layer3'],
+                                 ['/home/user/path/layer7'])
diff --git a/bitbake/lib/bb/tinfoil.py b/bitbake/lib/bb/tinfoil.py
index 1ea46d8..7aa653f 100644
--- a/bitbake/lib/bb/tinfoil.py
+++ b/bitbake/lib/bb/tinfoil.py
@@ -36,13 +36,13 @@
 
         # Set up logging
         self.logger = logging.getLogger('BitBake')
-        console = logging.StreamHandler(output)
-        bb.msg.addDefaultlogFilter(console)
+        self._log_hdlr = logging.StreamHandler(output)
+        bb.msg.addDefaultlogFilter(self._log_hdlr)
         format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
         if output.isatty():
             format.enable_color()
-        console.setFormatter(format)
-        self.logger.addHandler(console)
+        self._log_hdlr.setFormatter(format)
+        self.logger.addHandler(self._log_hdlr)
 
         self.config = CookerConfiguration()
         configparams = TinfoilConfigParameters(parse_only=True)
@@ -88,6 +88,7 @@
         self.cooker.shutdown(force=True)
         self.cooker.post_serve()
         self.cooker.unlockBitbake()
+        self.logger.removeHandler(self._log_hdlr)
 
 class TinfoilConfigParameters(ConfigParameters):
 
diff --git a/bitbake/lib/bb/ui/buildinfohelper.py b/bitbake/lib/bb/ui/buildinfohelper.py
index 6e313fe..78f1e92 100644
--- a/bitbake/lib/bb/ui/buildinfohelper.py
+++ b/bitbake/lib/bb/ui/buildinfohelper.py
@@ -66,6 +66,7 @@
 
     def __init__(self):
         self.layer_version_objects = []
+        self.layer_version_built = []
         self.task_objects = {}
         self.recipe_objects = {}
 
@@ -94,8 +95,8 @@
 
         created = False
         if not key in vars(self)[dictname].keys():
-            vars(self)[dictname][key] = clazz.objects.create(**kwargs)
-            created = True
+            vars(self)[dictname][key], created = \
+                clazz.objects.get_or_create(**kwargs)
 
         return (vars(self)[dictname][key], created)
 
@@ -161,8 +162,6 @@
             build.bitbake_version=build_info['bitbake_version']
             build.save()
 
-            Target.objects.filter(build = build).delete()
-
         else:
             build = Build.objects.create(
                                     project = prj,
@@ -183,18 +182,26 @@
 
         return build
 
-    def create_target_objects(self, target_info):
-        assert 'build' in target_info
-        assert 'targets' in target_info
-
-        targets = []
-        for tgt_name in target_info['targets']:
-            tgt_object = Target.objects.create( build = target_info['build'],
-                                    target = tgt_name,
-                                    is_image = False,
-                                    )
-            targets.append(tgt_object)
-        return targets
+    @staticmethod
+    def get_or_create_targets(target_info):
+        result = []
+        for target in target_info['targets']:
+            task = ''
+            if ':' in target:
+                target, task = target.split(':', 1)
+            if task.startswith('do_'):
+                task = task[3:]
+            if task == 'build':
+                task = ''
+            obj, created = Target.objects.get_or_create(build=target_info['build'],
+                                                        target=target)
+            if created:
+                obj.is_image = False
+                if task:
+                    obj.task = task
+                obj.save()
+            result.append(obj)
+        return result
 
     def update_build_object(self, build, errors, warnings, taskfailures):
         assert isinstance(build,Build)
@@ -269,23 +276,66 @@
 
         assert not recipe_information['file_path'].startswith("/")      # we should have layer-relative paths at all times
 
-        recipe_object, created = self._cached_get_or_create(Recipe, layer_version=recipe_information['layer_version'],
+
+        def update_recipe_obj(recipe_object):
+            object_changed = False
+            for v in vars(recipe_object):
+                if v in recipe_information.keys():
+                    object_changed = True
+                    vars(recipe_object)[v] = recipe_information[v]
+
+            if object_changed:
+                recipe_object.save()
+
+        recipe, created = self._cached_get_or_create(Recipe, layer_version=recipe_information['layer_version'],
                                      file_path=recipe_information['file_path'], pathflags = recipe_information['pathflags'])
-        if created and must_exist:
-            raise NotExisting("Recipe object created when expected to exist", recipe_information)
 
-        object_changed = False
-        for v in vars(recipe_object):
-            if v in recipe_information.keys():
-                object_changed = True
-                vars(recipe_object)[v] = recipe_information[v]
+        update_recipe_obj(recipe)
 
-        if object_changed:
-            recipe_object.save()
+        built_recipe = None
+        # Create a copy of the recipe for historical puposes and update it
+        for built_layer in self.layer_version_built:
+            if built_layer.layer == recipe_information['layer_version'].layer:
+                built_recipe, c = self._cached_get_or_create(Recipe,
+                        layer_version=built_layer,
+                        file_path=recipe_information['file_path'],
+                        pathflags = recipe_information['pathflags'])
+                update_recipe_obj(built_recipe)
+                break
 
-        return recipe_object
+
+        # If we're in analysis mode then we are wholly responsible for the data
+        # and therefore we return the 'real' recipe rather than the build
+        # history copy of the recipe.
+        if  recipe_information['layer_version'].build is not None and \
+            recipe_information['layer_version'].build.project == \
+                Project.objects.get_default_project():
+            return recipe
+
+        return built_recipe
 
     def get_update_layer_version_object(self, build_obj, layer_obj, layer_version_information):
+        if isinstance(layer_obj, Layer_Version):
+            # We already found our layer version for this build so just
+            # update it with the new build information
+            logger.debug("We found our layer from toaster")
+            layer_obj.local_path = layer_version_information['local_path']
+            layer_obj.save()
+            self.layer_version_objects.append(layer_obj)
+
+            # create a new copy of this layer version as a snapshot for
+            # historical purposes
+            layer_copy, c = Layer_Version.objects.get_or_create(build=build_obj,
+                            layer=layer_obj.layer,
+                            commit=layer_version_information['commit'],
+                            local_path = layer_version_information['local_path'],
+                            )
+            logger.info("created new historical layer version %d", layer_copy.pk)
+
+            self.layer_version_built.append(layer_copy)
+
+            return layer_obj
+
         assert isinstance(build_obj, Build)
         assert isinstance(layer_obj, Layer)
         assert 'branch' in layer_version_information
@@ -293,14 +343,20 @@
         assert 'priority' in layer_version_information
         assert 'local_path' in layer_version_information
 
+        # If we're doing a command line build then associate this new layer with the
+        # project to avoid it 'contaminating' toaster data
+        project = None
+        if build_obj.project == Project.objects.get_default_project():
+            project = build_obj.project
+
         layer_version_object, _ = Layer_Version.objects.get_or_create(
-                                    build = build_obj,
-                                    layer = layer_obj,
-                                    branch = layer_version_information['branch'],
-                                    commit = layer_version_information['commit'],
-                                    priority = layer_version_information['priority'],
-                                    local_path = layer_version_information['local_path'],
-                                    )
+                                  build = build_obj,
+                                  layer = layer_obj,
+                                  branch = layer_version_information['branch'],
+                                  commit = layer_version_information['commit'],
+                                  priority = layer_version_information['priority'],
+                                  local_path = layer_version_information['local_path'],
+                                  project=project)
 
         self.layer_version_objects.append(layer_version_object)
 
@@ -335,8 +391,15 @@
                     localdirname = os.path.join(bc.be.sourcedir, localdirname)
                 #logger.debug(1, "Localdirname %s lcal_path %s" % (localdirname, layer_information['local_path']))
                 if localdirname.startswith(layer_information['local_path']):
+                  # If the build request came from toaster this field
+                  # should contain the information from the layer_version
+                  # That created this build request.
+                    if brl.layer_version:
+                        return brl.layer_version
+
                     # we matched the BRLayer, but we need the layer_version that generated this BR; reverse of the Project.schedule_build()
                     #logger.debug(1, "Matched %s to BRlayer %s" % (pformat(layer_information["local_path"]), localdirname))
+
                     for pl in buildrequest.project.projectlayer_set.filter(layercommit__layer__name = brl.name):
                         if pl.layercommit.layer.vcs_url == brl.giturl :
                             layer = pl.layercommit.layer
@@ -353,26 +416,29 @@
         files = filedata['files']
         syms = filedata['syms']
 
-        # we insert directories, ordered by name depth
+        # always create the root directory as a special case;
+        # note that this is never displayed, so the owner, group,
+        # size, permission are irrelevant
+        tf_obj = Target_File.objects.create(target = target_obj,
+                                            path = '/',
+                                            size = 0,
+                                            owner = '',
+                                            group = '',
+                                            permission = '',
+                                            inodetype = Target_File.ITYPE_DIRECTORY)
+        tf_obj.save()
+
+        # insert directories, ordered by name depth
         for d in sorted(dirs, key=lambda x:len(x[-1].split("/"))):
             (user, group, size) = d[1:4]
             permission = d[0][1:]
             path = d[4].lstrip(".")
+
+            # we already created the root directory, so ignore any
+            # entry for it
             if len(path) == 0:
-                # we create the root directory as a special case
-                path = "/"
-                tf_obj = Target_File.objects.create(
-                        target = target_obj,
-                        path = path,
-                        size = size,
-                        inodetype = Target_File.ITYPE_DIRECTORY,
-                        permission = permission,
-                        owner = user,
-                        group = group,
-                        )
-                tf_obj.directory = tf_obj
-                tf_obj.save()
                 continue
+
             parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1])
             if len(parent_path) == 0:
                 parent_path = "/"
@@ -461,6 +527,12 @@
         errormsg = ""
         for p in packagedict:
             searchname = p
+            if p not in pkgpnmap:
+                logger.warning("Image packages list contains %p, but is"
+                               " missing from all packages list where the"
+                               " metadata comes from. Skipping...", p)
+                continue
+
             if 'OPKGN' in pkgpnmap[p].keys():
                 searchname = pkgpnmap[p]['OPKGN']
 
@@ -504,13 +576,20 @@
                 elif deptype == 'recommends':
                     tdeptype = Package_Dependency.TYPE_TRECOMMENDS
 
-                packagedeps_objs.append(Package_Dependency( package = packagedict[p]['object'],
-                                        depends_on = packagedict[px]['object'],
-                                        dep_type = tdeptype,
-                                        target = target_obj))
+                try:
+                    packagedeps_objs.append(Package_Dependency(
+                        package = packagedict[p]['object'],
+                        depends_on = packagedict[px]['object'],
+                        dep_type = tdeptype,
+                        target = target_obj))
+                except KeyError as e:
+                    logger.warn("Could not add dependency to the package %s "
+                                "because %s is an unknown package", p, px)
 
         if len(packagedeps_objs) > 0:
             Package_Dependency.objects.bulk_create(packagedeps_objs)
+        else:
+            logger.info("No package dependencies created")
 
         if len(errormsg) > 0:
             logger.warn("buildinfohelper: target_package_info could not identify recipes: \n%s", errormsg)
@@ -686,6 +765,7 @@
     def __init__(self, server, has_build_history = False):
         self.internal_state = {}
         self.internal_state['taskdata'] = {}
+        self.internal_state['targets'] = []
         self.task_order = 0
         self.autocommit_step = 1
         self.server = server
@@ -704,7 +784,7 @@
     ## methods to convert event/external info into objects that the ORM layer uses
 
 
-    def _get_build_information(self, consolelogfile):
+    def _get_build_information(self, build_log_path):
         build_info = {}
         # Generate an identifier for each new build
 
@@ -713,7 +793,7 @@
         build_info['distro_version'] = self.server.runCommand(["getVariable", "DISTRO_VERSION"])[0]
         build_info['started_on'] = timezone.now()
         build_info['completed_on'] = timezone.now()
-        build_info['cooker_log_path'] = consolelogfile
+        build_info['cooker_log_path'] = build_log_path
         build_info['build_name'] = self.server.runCommand(["getVariable", "BUILDNAME"])[0]
         build_info['bitbake_version'] = self.server.runCommand(["getVariable", "BB_VERSION"])[0]
 
@@ -764,8 +844,15 @@
                 if not localdirname.startswith("/"):
                     localdirname = os.path.join(bc.be.sourcedir, localdirname)
                 if path.startswith(localdirname):
+                    # If the build request came from toaster this field
+                    # should contain the information from the layer_version
+                    # That created this build request.
+                    if brl.layer_version:
+                        return brl.layer_version
+
                     #logger.warn("-- managed: matched path %s with layer %s " % (path, localdirname))
                     # we matched the BRLayer, but we need the layer_version that generated this br
+
                     for lvo in self.orm_wrapper.layer_version_objects:
                         if brl.name == lvo.layer.name:
                             return lvo
@@ -774,7 +861,7 @@
         logger.warn("Could not match layer version for recipe path %s : %s", path, self.orm_wrapper.layer_version_objects)
 
         #mockup the new layer
-        unknown_layer, _ = Layer.objects.get_or_create(name="__FIXME__unidentified_layer", layer_index_url="")
+        unknown_layer, _ = Layer.objects.get_or_create(name="Unidentified layer", layer_index_url="")
         unknown_layer_version_obj, _ = Layer_Version.objects.get_or_create(layer = unknown_layer, build = self.internal_state['build'])
 
         # append it so we don't run into this error again and again
@@ -847,9 +934,9 @@
                 logger.warn("buildinfohelper: cannot identify layer exception:%s ", nee)
 
 
-    def store_started_build(self, event, consolelogfile):
+    def store_started_build(self, event, build_log_path):
         assert '_pkgs' in vars(event)
-        build_information = self._get_build_information(consolelogfile)
+        build_information = self._get_build_information(build_log_path)
 
         build_obj = self.orm_wrapper.create_build_object(build_information, self.brbe, self.project)
 
@@ -869,7 +956,7 @@
         target_information['targets'] = event._pkgs
         target_information['build'] = build_obj
 
-        self.internal_state['targets'] = self.orm_wrapper.create_target_objects(target_information)
+        self.internal_state['targets'] = self.orm_wrapper.get_or_create_targets(target_information)
 
         # Save build configuration
         data = self.server.runCommand(["getAllKeysWithFlags", ["doc", "func"]])[0]
@@ -996,7 +1083,7 @@
             task_information['disk_io'] = taskstats['disk_io']
             if 'elapsed_time' in taskstats:
                 task_information['elapsed_time'] = taskstats['elapsed_time']
-            self.orm_wrapper.get_update_task_object(task_information, True)  # must exist
+            self.orm_wrapper.get_update_task_object(task_information)
 
     def update_and_store_task(self, event):
         assert 'taskfile' in vars(event)
@@ -1097,15 +1184,22 @@
         # for all image targets
         for target in self.internal_state['targets']:
             if target.is_image:
+                pkgdata = BuildInfoHelper._get_data_from_event(event)['pkgdata']
+                imgdata = BuildInfoHelper._get_data_from_event(event)['imgdata'][target.target]
+                filedata = BuildInfoHelper._get_data_from_event(event)['filedata'][target.target]
+
                 try:
-                    pkgdata = BuildInfoHelper._get_data_from_event(event)['pkgdata']
-                    imgdata = BuildInfoHelper._get_data_from_event(event)['imgdata'][target.target]
                     self.orm_wrapper.save_target_package_information(self.internal_state['build'], target, imgdata, pkgdata, self.internal_state['recipes'])
-                    filedata = BuildInfoHelper._get_data_from_event(event)['filedata'][target.target]
+                except KeyError as e:
+                    logger.warn("KeyError in save_target_package_information"
+                                "%s ", e)
+
+                try:
                     self.orm_wrapper.save_target_file_information(self.internal_state['build'], target, filedata)
-                except KeyError:
-                    # we must have not got the data for this image, nothing to save
-                    pass
+                except KeyError as e:
+                    logger.warn("KeyError in save_target_file_information"
+                                "%s ", e)
+
 
 
 
@@ -1306,7 +1400,9 @@
 
         log_information = {}
         log_information['build'] = self.internal_state['build']
-        if event.levelno == formatter.ERROR:
+        if event.levelno == formatter.CRITICAL:
+            log_information['level'] = LogMessage.CRITICAL
+        elif event.levelno == formatter.ERROR:
             log_information['level'] = LogMessage.ERROR
         elif event.levelno == formatter.WARNING:
             log_information['level'] = LogMessage.WARNING
@@ -1319,6 +1415,7 @@
         log_information['pathname'] = event.pathname
         log_information['lineno'] = event.lineno
         logger.info("Logging error 2: %s", log_information)
+
         self.orm_wrapper.create_logmessage(log_information)
 
     def close(self, errorcode):
diff --git a/bitbake/lib/bb/ui/knotty.py b/bitbake/lib/bb/ui/knotty.py
index 2bee242..90c3183 100644
--- a/bitbake/lib/bb/ui/knotty.py
+++ b/bitbake/lib/bb/ui/knotty.py
@@ -230,7 +230,7 @@
     if error:
         logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error)
         raise BaseException(error)
-    consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"])
+    consolelogfile, error = server.runCommand(["getSetVariable", "BB_CONSOLELOG"])
     if error:
         logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error)
         raise BaseException(error)
diff --git a/bitbake/lib/bb/ui/toasterui.py b/bitbake/lib/bb/ui/toasterui.py
index e0c278b..3d26150 100644
--- a/bitbake/lib/bb/ui/toasterui.py
+++ b/bitbake/lib/bb/ui/toasterui.py
@@ -21,6 +21,7 @@
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 from __future__ import division
+import time
 import sys
 try:
     import bb
@@ -43,8 +44,6 @@
 logger = logging.getLogger("ToasterLogger")
 interactive = sys.stdout.isatty()
 
-
-
 def _log_settings_from_server(server):
     # Get values of variables which control our output
     includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
@@ -59,12 +58,56 @@
     if error:
         logger.error("Unable to get the value of BB_CONSOLELOG variable: %s", error)
         raise BaseException(error)
-    return includelogs, loglines, consolelogfile
+    return consolelogfile
 
+# create a log file for a single build and direct the logger at it;
+# log file name is timestamped to the millisecond (depending
+# on system clock accuracy) to ensure it doesn't overlap with
+# other log file names
+#
+# returns (log file, path to log file) for a build
+def _open_build_log(log_dir):
+    format_str = "%(levelname)s: %(message)s"
 
-def main(server, eventHandler, params ):
+    now = time.time()
+    now_ms = int((now - int(now)) * 1000)
+    time_str = time.strftime('build_%Y%m%d_%H%M%S', time.localtime(now))
+    log_file_name = time_str + ('.%d.log' % now_ms)
+    build_log_file_path = os.path.join(log_dir, log_file_name)
+
+    build_log = logging.FileHandler(build_log_file_path)
+
+    logformat = bb.msg.BBLogFormatter(format_str)
+    build_log.setFormatter(logformat)
+
+    bb.msg.addDefaultlogFilter(build_log)
+    logger.addHandler(build_log)
+
+    return (build_log, build_log_file_path)
+
+# stop logging to the build log if it exists
+def _close_build_log(build_log):
+    if build_log:
+        build_log.flush()
+        build_log.close()
+        logger.removeHandler(build_log)
+
+def main(server, eventHandler, params):
+    # set to a logging.FileHandler instance when a build starts;
+    # see _open_build_log()
+    build_log = None
+
+    # set to the log path when a build starts
+    build_log_file_path = None
+
     helper = uihelper.BBUIHelper()
 
+    # TODO don't use log output to determine when bitbake has started
+    #
+    # WARNING: this log handler cannot be removed, as localhostbecontroller
+    # relies on output in the toaster_ui.log file to determine whether
+    # the bitbake server has started, which only happens if
+    # this logger is setup here (see the TODO in the loop below)
     console = logging.StreamHandler(sys.stdout)
     format_str = "%(levelname)s: %(message)s"
     formatter = bb.msg.BBLogFormatter(format_str)
@@ -73,8 +116,6 @@
     logger.addHandler(console)
     logger.setLevel(logging.INFO)
 
-    _, _, consolelogfile = _log_settings_from_server(server)
-
     # verify and warn
     build_history_enabled = True
     inheritlist, _ = server.runCommand(["getVariable", "INHERIT"])
@@ -87,8 +128,9 @@
         logger.error("ToasterUI can only work in observer mode")
         return 1
 
-
+    # set to 1 when toasterui needs to shut down
     main.shutdown = 0
+
     interrupted = False
     return_value = 0
     errors = 0
@@ -98,25 +140,31 @@
 
     buildinfohelper = BuildInfoHelper(server, build_history_enabled)
 
-    if buildinfohelper.brbe is not None and consolelogfile:
-        # if we are under managed mode we have no other UI and we need to write our own file
-        bb.utils.mkdirhier(os.path.dirname(consolelogfile))
-        conlogformat = bb.msg.BBLogFormatter(format_str)
-        consolelog = logging.FileHandler(consolelogfile)
-        bb.msg.addDefaultlogFilter(consolelog)
-        consolelog.setFormatter(conlogformat)
-        logger.addHandler(consolelog)
-
+    # write our own log files into bitbake's log directory;
+    # we're only interested in the path to the parent directory of
+    # this file, as we're writing our own logs into the same directory
+    consolelogfile = _log_settings_from_server(server)
+    log_dir = os.path.dirname(consolelogfile)
+    bb.utils.mkdirhier(log_dir)
 
     while True:
         try:
             event = eventHandler.waitEvent(0.25)
             if first:
                 first = False
+
+                # TODO don't use log output to determine when bitbake has started
+                #
+                # this is the line localhostbecontroller needs to
+                # see in toaster_ui.log which it uses to decide whether
+                # the bitbake server has started...
                 logger.info("ToasterUI waiting for events")
 
             if event is None:
                 if main.shutdown > 0:
+                    # if shutting down, close any open build log first
+                    _close_build_log(build_log)
+
                     break
                 continue
 
@@ -125,19 +173,32 @@
             # pylint: disable=protected-access
             # the code will look into the protected variables of the event; no easy way around this
 
+            # we treat ParseStarted as the first event of toaster-triggered
+            # builds; that way we get the Build Configuration included in the log
+            # and any errors that occur before BuildStarted is fired
+            if isinstance(event, bb.event.ParseStarted):
+                if not (build_log and build_log_file_path):
+                    build_log, build_log_file_path = _open_build_log(log_dir)
+                continue
+
             if isinstance(event, bb.event.BuildStarted):
-                buildinfohelper.store_started_build(event, consolelogfile)
+                # command-line builds don't fire a ParseStarted event,
+                # so we have to start the log file for those on BuildStarted instead
+                if not (build_log and build_log_file_path):
+                    build_log, build_log_file_path = _open_build_log(log_dir)
+
+                buildinfohelper.store_started_build(event, build_log_file_path)
 
             if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)):
                 buildinfohelper.update_and_store_task(event)
-                logger.warn("Logfile for task %s", event.logfile)
+                logger.info("Logfile for task %s", event.logfile)
                 continue
 
             if isinstance(event, bb.build.TaskBase):
                 logger.info(event._message)
 
             if isinstance(event, bb.event.LogExecTTY):
-                logger.warn(event.msg)
+                logger.info(event.msg)
                 continue
 
             if isinstance(event, logging.LogRecord):
@@ -145,10 +206,12 @@
                     event.levelno = formatter.ERROR
 
                 buildinfohelper.store_log_event(event)
+
                 if event.levelno >= formatter.ERROR:
                     errors = errors + 1
                 elif event.levelno == formatter.WARNING:
                     warnings = warnings + 1
+
                 # For "normal" logging conditions, don't show note logs from tasks
                 # but do show them if the user has changed the default log level to
                 # include verbose/debug messages
@@ -169,8 +232,6 @@
             # timing and error informations from the parsing phase in Toaster
             if isinstance(event, (bb.event.SanityCheckPassed, bb.event.SanityCheck)):
                 continue
-            if isinstance(event, bb.event.ParseStarted):
-                continue
             if isinstance(event, bb.event.ParseProgress):
                 continue
             if isinstance(event, bb.event.ParseCompleted):
@@ -246,6 +307,12 @@
                     errorcode = 1
                     logger.error("Command execution failed: %s", event.error)
 
+                # turn off logging to the current build log
+                _close_build_log(build_log)
+
+                # reset ready for next BuildStarted
+                build_log = None
+
                 # update the build info helper on BuildCompleted, not on CommandXXX
                 buildinfohelper.update_build_information(event, errors, warnings, taskfailures)
                 buildinfohelper.close(errorcode)
@@ -254,7 +321,6 @@
 
                 # we start a new build info
                 if buildinfohelper.brbe is not None:
-
                     logger.debug("ToasterUI under BuildEnvironment management - exiting after the build")
                     server.terminateServer()
                 else:
@@ -296,8 +362,9 @@
                 continue
 
             if isinstance(event, bb.cooker.CookerExit):
-                # exit when the server exits
-                break
+                # shutdown when bitbake server shuts down
+                main.shutdown = 1
+                continue
 
             # ignore
             if isinstance(event, (bb.event.BuildBase,
@@ -308,14 +375,15 @@
                                   bb.event.OperationProgress,
                                   bb.command.CommandFailed,
                                   bb.command.CommandExit,
-                                  bb.command.CommandCompleted)):
+                                  bb.command.CommandCompleted,
+                                  bb.event.ReachableStamps)):
                 continue
 
             if isinstance(event, bb.event.DepTreeGenerated):
                 buildinfohelper.store_dependency_information(event)
                 continue
 
-            logger.error("Unknown event: %s", event)
+            logger.warn("Unknown event: %s", event)
             return_value += 1
 
         except EnvironmentError as ioerror:
@@ -335,7 +403,7 @@
             if tb is not None:
                 curr = tb
                 while curr is not None:
-                    logger.warn("Error data dump %s\n%s\n" , traceback.format_tb(curr,1), pformat(curr.tb_frame.f_locals))
+                    logger.error("Error data dump %s\n%s\n" , traceback.format_tb(curr,1), pformat(curr.tb_frame.f_locals))
                     curr = curr.tb_next
 
             # save them to database, if possible; if it fails, we already logged to console.
@@ -347,9 +415,8 @@
             # make sure we return with an error
             return_value += 1
 
-    if interrupted:
-        if return_value == 0:
-            return_value += 1
+    if interrupted and return_value == 0:
+        return_value += 1
 
     logger.warn("Return value is %d", return_value)
     return return_value
diff --git a/bitbake/lib/bb/utils.py b/bitbake/lib/bb/utils.py
index 91faa49..31ec2b7 100644
--- a/bitbake/lib/bb/utils.py
+++ b/bitbake/lib/bb/utils.py
@@ -1177,7 +1177,7 @@
             if not skip:
                 if checkspc:
                     checkspc = False
-                    if newlines[-1] == '\n' and line == '\n':
+                    if newlines and newlines[-1] == '\n' and line == '\n':
                         # Squash blank line if there are two consecutive blanks after a removal
                         continue
                 newlines.append(line)
@@ -1201,7 +1201,19 @@
 
 
 def edit_bblayers_conf(bblayers_conf, add, remove):
-    """Edit bblayers.conf, adding and/or removing layers"""
+    """Edit bblayers.conf, adding and/or removing layers
+    Parameters:
+        bblayers_conf: path to bblayers.conf file to edit
+        add: layer path (or list of layer paths) to add; None or empty
+            list to add nothing
+        remove: layer path (or list of layer paths) to remove; None or
+            empty list to remove nothing
+    Returns a tuple:
+        notadded: list of layers specified to be added but weren't
+            (because they were already in the list)
+        notremoved: list of layers that were specified to be removed
+            but weren't (because they weren't in the list)
+    """
 
     import fnmatch
 
@@ -1210,6 +1222,13 @@
             pth = pth[:-1]
         return pth
 
+    approved = bb.utils.approved_variables()
+    def canonicalise_path(pth):
+        pth = remove_trailing_sep(pth)
+        if 'HOME' in approved and '~' in pth:
+            pth = os.path.expanduser(pth)
+        return pth
+
     def layerlist_param(value):
         if not value:
             return []
@@ -1218,48 +1237,80 @@
         else:
             return [remove_trailing_sep(value)]
 
-    notadded = []
-    notremoved = []
-
     addlayers = layerlist_param(add)
     removelayers = layerlist_param(remove)
 
     # Need to use a list here because we can't set non-local variables from a callback in python 2.x
     bblayercalls = []
+    removed = []
+    plusequals = False
+    orig_bblayers = []
+
+    def handle_bblayers_firstpass(varname, origvalue, op, newlines):
+        bblayercalls.append(op)
+        if op == '=':
+            del orig_bblayers[:]
+        orig_bblayers.extend([canonicalise_path(x) for x in origvalue.split()])
+        return (origvalue, None, 2, False)
 
     def handle_bblayers(varname, origvalue, op, newlines):
-        bblayercalls.append(varname)
         updated = False
         bblayers = [remove_trailing_sep(x) for x in origvalue.split()]
         if removelayers:
             for removelayer in removelayers:
-                matched = False
                 for layer in bblayers:
-                    if fnmatch.fnmatch(layer, removelayer):
+                    if fnmatch.fnmatch(canonicalise_path(layer), canonicalise_path(removelayer)):
                         updated = True
-                        matched = True
                         bblayers.remove(layer)
+                        removed.append(removelayer)
                         break
-                if not matched:
-                    notremoved.append(removelayer)
-        if addlayers:
+        if addlayers and not plusequals:
             for addlayer in addlayers:
                 if addlayer not in bblayers:
                     updated = True
                     bblayers.append(addlayer)
-                else:
-                    notadded.append(addlayer)
+            del addlayers[:]
 
         if updated:
+            if op == '+=' and not bblayers:
+                bblayers = None
             return (bblayers, None, 2, False)
         else:
             return (origvalue, None, 2, False)
 
-    edit_metadata_file(bblayers_conf, ['BBLAYERS'], handle_bblayers)
+    with open(bblayers_conf, 'r') as f:
+        (_, newlines) = edit_metadata(f, ['BBLAYERS'], handle_bblayers_firstpass)
 
     if not bblayercalls:
         raise Exception('Unable to find BBLAYERS in %s' % bblayers_conf)
 
+    # Try to do the "smart" thing depending on how the user has laid out
+    # their bblayers.conf file
+    if bblayercalls.count('+=') > 1:
+        plusequals = True
+
+    removelayers_canon = [canonicalise_path(layer) for layer in removelayers]
+    notadded = []
+    for layer in addlayers:
+        layer_canon = canonicalise_path(layer)
+        if layer_canon in orig_bblayers and not layer_canon in removelayers_canon:
+            notadded.append(layer)
+    notadded_canon = [canonicalise_path(layer) for layer in notadded]
+    addlayers[:] = [layer for layer in addlayers if canonicalise_path(layer) not in notadded_canon]
+
+    (updated, newlines) = edit_metadata(newlines, ['BBLAYERS'], handle_bblayers)
+    if addlayers:
+        # Still need to add these
+        for addlayer in addlayers:
+            newlines.append('BBLAYERS += "%s"\n' % addlayer)
+        updated = True
+
+    if updated:
+        with open(bblayers_conf, 'w') as f:
+            f.writelines(newlines)
+
+    notremoved = list(set(removelayers) - set(removed))
+
     return (notadded, notremoved)
 
 
@@ -1310,3 +1361,27 @@
     result = cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signum)
     if result != 0:
         raise PrCtlError('prctl failed with error code %s' % result)
+
+#
+# Manually call the ioprio syscall. We could depend on other libs like psutil
+# however this gets us enough of what we need to bitbake for now without the
+# dependency
+#
+_unamearch = os.uname()[4]
+IOPRIO_WHO_PROCESS = 1
+IOPRIO_CLASS_SHIFT = 13
+
+def ioprio_set(who, cls, value):
+    NR_ioprio_set = None
+    if _unamearch == "x86_64":
+      NR_ioprio_set = 251
+    elif _unamearch[0] == "i" and _unamearch[2:3] == "86":
+      NR_ioprio_set = 289
+
+    if NR_ioprio_set:
+        ioprio = value | (cls << IOPRIO_CLASS_SHIFT)
+        rc = cdll['libc.so.6'].syscall(NR_ioprio_set, IOPRIO_WHO_PROCESS, who, ioprio)
+        if rc != 0:
+            raise ValueError("Unable to set ioprio, syscall returned %s" % rc)
+    else:
+        bb.warn("Unable to set IO Prio for arch %s" % _unamearch)
diff --git a/bitbake/lib/prserv/db.py b/bitbake/lib/prserv/db.py
index 4379580..36c9f7b 100644
--- a/bitbake/lib/prserv/db.py
+++ b/bitbake/lib/prserv/db.py
@@ -248,7 +248,7 @@
         self.connection.execute("PRAGMA journal_mode = WAL;")
         self._tables={}
 
-    def __del__(self):
+    def disconnect(self):
         self.connection.close()
 
     def __getitem__(self,tblname):
diff --git a/bitbake/lib/prserv/serv.py b/bitbake/lib/prserv/serv.py
index 5c0ffb9..eafc3aa 100644
--- a/bitbake/lib/prserv/serv.py
+++ b/bitbake/lib/prserv/serv.py
@@ -3,6 +3,7 @@
 from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
 import threading
 import Queue
+import socket
 
 try:
     import sqlite3
@@ -37,7 +38,6 @@
 class PRServer(SimpleXMLRPCServer):
     def __init__(self, dbfile, logfile, interface, daemon=True):
         ''' constructor '''
-        import socket
         try:
             SimpleXMLRPCServer.__init__(self, interface,
                                         logRequests=False, allow_none=True)
@@ -148,7 +148,7 @@
         while not self.quit:
             self.handle_request()
         self.handlerthread.join()
-        self.table.sync()
+        self.db.disconnect()
         logger.info("PRServer: stopping...")
         self.server_close()
         return
@@ -289,7 +289,8 @@
         return self.host, self.port
 
 def start_daemon(dbfile, host, port, logfile):
-    pidfile = PIDPREFIX % (host, port)
+    ip = socket.gethostbyname(host)
+    pidfile = PIDPREFIX % (ip, port)
     try:
         pf = file(pidfile,'r')
         pid = int(pf.readline().strip())
@@ -302,12 +303,21 @@
                             % pidfile)
         return 1
 
-    server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (host,port))
+    server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
     server.start()
+
+    # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
+    # the one the server actually is listening, so at least warn the user about it
+    _,rport = server.getinfo()
+    if port != rport:
+        sys.stdout.write("Server is listening at port %s instead of %s\n"
+                         % (rport,port))
     return 0
 
 def stop_daemon(host, port):
-    pidfile = PIDPREFIX % (host, port)
+    import glob
+    ip = socket.gethostbyname(host)
+    pidfile = PIDPREFIX % (ip, port)
     try:
         pf = file(pidfile,'r')
         pid = int(pf.readline().strip())
@@ -316,11 +326,23 @@
         pid = None
 
     if not pid:
-        sys.stderr.write("pidfile %s does not exist. Daemon not running?\n"
-                        % pidfile)
+        # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
+        # so at least advise the user which ports the corresponding server is listening
+        ports = []
+        portstr = ""
+        for pf in glob.glob(PIDPREFIX % (ip,'*')):
+            bn = os.path.basename(pf)
+            root, _ = os.path.splitext(bn)
+            ports.append(root.split('_')[-1])
+        if len(ports):
+            portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
+
+        sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
+                         % (pidfile,portstr))
+        return 1
 
     try:
-        PRServerConnection(host, port).terminate()
+        PRServerConnection(ip, port).terminate()
     except:
         logger.critical("Stop PRService %s:%d failed" % (host,port))
 
diff --git a/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py b/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
index a9909b8..b5cf559 100644
--- a/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
+++ b/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
@@ -23,9 +23,11 @@
 import os
 import sys
 import re
+import shutil
 from django.db import transaction
 from django.db.models import Q
 from bldcontrol.models import BuildEnvironment, BRLayer, BRVariable, BRTarget, BRBitbake
+from orm.models import CustomImageRecipe, Layer, Layer_Version, ProjectLayer
 import subprocess
 
 from toastermain import settings
@@ -179,15 +181,9 @@
         logger.debug("localhostbecontroller: Stopped bitbake server")
 
     def getGitCloneDirectory(self, url, branch):
-        """ Utility that returns the last component of a git path as directory
-        """
-        import re
-        components = re.split(r'[:\.\/]', url)
-        base = components[-2] if components[-1] == "git" else components[-1]
-
+        """Construct unique clone directory name out of url and branch."""
         if branch != "HEAD":
-            return "_%s_%s.toaster_cloned" % (base, branch)
-
+            return "_toaster_clones/_%s_%s" % (re.sub('[:/@%]', '_', url), branch)
 
         # word of attention; this is a localhost-specific issue; only on the localhost we expect to have "HEAD" releases
         # which _ALWAYS_ means the current poky checkout
@@ -197,7 +193,7 @@
         return local_checkout_path
 
 
-    def setLayers(self, bitbakes, layers):
+    def setLayers(self, bitbakes, layers, targets):
         """ a word of attention: by convention, the first layer for any build will be poky! """
 
         assert self.be.sourcedir is not None
@@ -222,23 +218,26 @@
         logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos))
 
 
-        # 2. find checked-out git repos in the sourcedir directory that may help faster cloning
+        # 2. Note for future use if the current source directory is a
+        # checked-out git repos that could match a layer's vcs_url and therefore
+        # be used to speed up cloning (rather than fetching it again).
 
         cached_layers = {}
-        for ldir in os.listdir(self.be.sourcedir):
-            fldir = os.path.join(self.be.sourcedir, ldir)
-            if os.path.isdir(fldir):
+
+        try:
+            for remotes in self._shellcmd("git remote -v", self.be.sourcedir).split("\n"):
                 try:
-                    for line in self._shellcmd("git remote -v", fldir).split("\n"):
-                        try:
-                            remote = line.split("\t")[1].split(" ")[0]
-                            if remote not in cached_layers:
-                                cached_layers[remote] = fldir
-                        except IndexError:
-                            pass
-                except ShellCmdException:
-                    # ignore any errors in collecting git remotes
+                    remote = remotes.split("\t")[1].split(" ")[0]
+                    if remote not in cached_layers:
+                        cached_layers[remote] = self.be.sourcedir
+                except IndexError:
                     pass
+        except ShellCmdException:
+            # ignore any errors in collecting git remotes this is an optional
+            # step
+            pass
+
+        logger.info("Using pre-checked out source for layer %s", cached_layers)
 
         layerlist = []
 
@@ -260,13 +259,14 @@
                     self._shellcmd("git remote remove origin", localdirname)
                     self._shellcmd("git remote add origin \"%s\"" % giturl, localdirname)
                 else:
-                    logger.debug("localhostbecontroller: cloning %s:%s in %s" % (giturl, commit, localdirname))
-                    self._shellcmd("git clone \"%s\" --single-branch --branch \"%s\" \"%s\"" % (giturl, commit, localdirname))
+                    logger.debug("localhostbecontroller: cloning %s in %s" % (giturl, localdirname))
+                    self._shellcmd('git clone "%s" "%s"' % (giturl, localdirname))
 
             # branch magic name "HEAD" will inhibit checkout
             if commit != "HEAD":
                 logger.debug("localhostbecontroller: checking out commit %s to %s " % (commit, localdirname))
-                self._shellcmd("git fetch --all && git checkout \"%s\" && git rebase \"origin/%s\"" % (commit, commit) , localdirname)
+                ref = commit if re.match('^[a-fA-F0-9]+$', commit) else 'origin/%s' % commit
+                self._shellcmd('git fetch --all && git reset --hard "%s"' % ref, localdirname)
 
             # take the localdirname as poky dir if we can find the oe-init-build-env
             if self.pokydirname is None and os.path.exists(os.path.join(localdirname, "oe-init-build-env")):
@@ -299,6 +299,51 @@
         if not os.path.exists(bblayerconf):
             raise BuildSetupException("BE is not consistent: bblayers.conf file missing at %s" % bblayerconf)
 
+        # 6. create custom layer and add custom recipes to it
+        layerpath = os.path.join(self.be.sourcedir, "_meta-toaster-custom")
+        if os.path.isdir(layerpath):
+            shutil.rmtree(layerpath) # remove leftovers from previous builds
+        for target in targets:
+            try:
+                customrecipe = CustomImageRecipe.objects.get(name=target.target,
+                                                             project=bitbakes[0].req.project)
+            except CustomImageRecipe.DoesNotExist:
+                continue # not a custom recipe, skip
+
+            # create directory structure
+            for name in ("conf", "recipes"):
+                path = os.path.join(layerpath, name)
+                if not os.path.isdir(path):
+                    os.makedirs(path)
+
+            # create layer.oonf
+            config = os.path.join(layerpath, "conf", "layer.conf")
+            if not os.path.isfile(config):
+                with open(config, "w") as conf:
+                    conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
+
+            # create recipe
+            recipe = os.path.join(layerpath, "recipes", "%s.bb" % target.target)
+            with open(recipe, "w") as recipef:
+                recipef.write("require %s\n" % customrecipe.base_recipe.recipe.file_path)
+                packages = [pkg.name for pkg in customrecipe.packages.all()]
+                if packages:
+                    recipef.write('IMAGE_INSTALL = "%s"\n' % ' '.join(packages))
+
+            # create *Layer* objects needed for build machinery to work
+            layer = Layer.objects.get_or_create(name="Toaster Custom layer",
+                                                summary="Layer for custom recipes",
+                                                vcs_url="file://%s" % layerpath)[0]
+            breq = target.req
+            lver = Layer_Version.objects.get_or_create(project=breq.project, layer=layer,
+                                                       dirpath=layerpath, build=breq.build)[0]
+            ProjectLayer.objects.get_or_create(project=breq.project, layercommit=lver,
+                                               optional=False)
+            BRLayer.objects.get_or_create(req=breq, name=layer.name, dirpath=layerpath,
+                                          giturl="file://%s" % layerpath)
+        if os.path.isdir(layerpath):
+            layerlist.append(layerpath)
+
         BuildEnvironmentController._updateBBLayers(bblayerconf, layerlist)
 
         self.islayerset = True
@@ -316,7 +361,7 @@
 
     def triggerBuild(self, bitbake, layers, variables, targets):
         # set up the buid environment with the needed layers
-        self.setLayers(bitbake, layers)
+        self.setLayers(bitbake, layers, targets)
         self.writeConfFile("conf/toaster-pre.conf", variables)
         self.writeConfFile("conf/toaster.conf", raw = "INHERIT+=\"toaster buildhistory\"")
 
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py b/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py
index b2c573c..5e70437 100644
--- a/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py
+++ b/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py
@@ -4,7 +4,7 @@
 from bldcontrol.models import BuildRequest, BuildEnvironment, BRError
 from orm.models import ToasterSetting, Build
 import os
-import sys, traceback
+import traceback
 
 def DN(path):
     if path is None:
@@ -21,7 +21,7 @@
         super(Command, self).__init__(*args, **kwargs)
         self.guesspath = DN(DN(DN(DN(DN(DN(DN(__file__)))))))
 
-    def _find_first_path_for_file(self, startdirectory, filename, level = 0):
+    def _find_first_path_for_file(self, startdirectory, filename, level=0):
         if level < 0:
             return None
         dirs = []
@@ -38,7 +38,7 @@
                 return ret
         return None
 
-    def _recursive_list_directories(self, startdirectory, level = 0):
+    def _recursive_list_directories(self, startdirectory, level=0):
         if level < 0:
             return []
         dirs = []
@@ -50,49 +50,23 @@
         except OSError:
             pass
         for j in dirs:
-                dirs = dirs + self._recursive_list_directories(j, level - 1)
+            dirs = dirs + self._recursive_list_directories(j, level - 1)
         return dirs
 
 
-    def _get_suggested_sourcedir(self, be):
-        if be.betype != BuildEnvironment.TYPE_LOCAL:
-            return ""
-        return DN(DN(DN(self._find_first_path_for_file(self.guesspath, "toasterconf.json", 4))))
-
-    def _get_suggested_builddir(self, be):
-        if be.betype != BuildEnvironment.TYPE_LOCAL:
-            return ""
-        return DN(self._find_first_path_for_file(DN(self.guesspath), "bblayers.conf", 4))
-
     def _verify_build_environment(self):
-        # refuse to start if we have no build environments
-        while BuildEnvironment.objects.count() == 0:
-            print(" !! No build environments found. Toaster needs at least one build environment in order to be able to run builds.\n" +
-                "You can manually define build environments in the database table bldcontrol_buildenvironment.\n" +
-                "Or Toaster can define a simple localhost-based build environment for you.")
-
-            i = raw_input(" --  Do you want to create a basic localhost build environment ? (Y/n) ");
-            if not len(i) or i.startswith("y") or i.startswith("Y"):
-                BuildEnvironment.objects.create(pk = 1, betype = 0)
-            else:
-                raise Exception("Toaster cannot start without build environments. Aborting.")
-
+        # provide a local build env. This will be extended later to include non local
+        if BuildEnvironment.objects.count() == 0:
+            BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL)
 
         # we make sure we have builddir and sourcedir for all defined build envionments
         for be in BuildEnvironment.objects.all():
             be.needs_import = False
             def _verify_be():
                 is_changed = False
-                print("\nVerifying the build environment. If the local build environment is not properly configured, you will be asked to configure it.")
 
                 def _update_sourcedir():
-                    suggesteddir = self._get_suggested_sourcedir(be)
-                    if len(suggesteddir) > 0:
-                        be.sourcedir = raw_input("This is the directory Toaster uses to check out the source code of the layers you will build. Toaster will create new clones of the layers, so existing content in the chosen directory will not be changed.\nToaster suggests you use \"%s\" as your layers checkout directory. If you select this directory, a layer like \"meta-intel\" will end up in \"%s/meta-intel\".\nPress Enter to select \"%s\" or type the full path to a different directory. If you provide your own directory, it must be a parent of the cloned directory for the sources you are using to run Toaster: " % (suggesteddir, suggesteddir, suggesteddir))
-                    else:
-                        be.sourcedir = raw_input("Toaster needs to know in which directory it should check out the source code of the layers you will build. The directory should be a parent of the cloned directory for the sources you are using to run Toaster. Toaster will create new clones of the layers, so existing content in the chosen directory will not be changed.\nType the full path to the directory (for example: \"%s\": " % os.environ.get('HOME', '/tmp/'))
-                    if len(be.sourcedir) == 0 and len(suggesteddir) > 0:
-                        be.sourcedir = suggesteddir
+                    be.sourcedir = os.environ.get('TOASTER_DIR')
                     return True
 
                 if len(be.sourcedir) == 0:
@@ -103,23 +77,13 @@
                     print "\n -- Validation: The layers checkout directory must be set to an absolute path."
                     is_changed = _update_sourcedir()
 
-                if not be.sourcedir in DN(__file__):
-                    print "\n -- Validation: The layers checkout directory must be a parent of the current checkout."
-                    is_changed = _update_sourcedir()
-
                 if is_changed:
                     if be.betype == BuildEnvironment.TYPE_LOCAL:
                         be.needs_import = True
                     return True
 
                 def _update_builddir():
-                    suggesteddir = self._get_suggested_builddir(be)
-                    if len(suggesteddir) > 0:
-                        be.builddir = raw_input("Toaster needs to know where your build directory is located.\nThe build directory is where all the artifacts created by your builds will be stored. Toaster suggests \"%s\".\nPress Enter to select \"%s\" or type the full path to a different directory: " % (suggesteddir, suggesteddir))
-                    else:
-                        be.builddir = raw_input("Toaster needs to know where is your build directory.\nThe build directory is where all the artifacts created by your builds will be stored. Type the full path to the directory (for example: \" %s/build\")" % os.environ.get('HOME','/tmp/'))
-                    if len(be.builddir) == 0 and len(suggesteddir) > 0:
-                        be.builddir = suggesteddir
+                    be.builddir = os.environ.get('TOASTER_DIR')+"/build"
                     return True
 
                 if len(be.builddir) == 0:
@@ -138,79 +102,51 @@
 
 
                 if be.needs_import:
-                    print "\nToaster can use a SINGLE predefined configuration file to set up default project settings and layer information sources.\n"
+                    try:
+                        config_file = os.environ.get('TOASTER_CONF')
+                        print "\nImporting file: %s" % config_file
+                        from loadconf import Command as LoadConfigCommand
 
-                    # find configuration files
-                    config_files = []
-                    for dirname in self._recursive_list_directories(be.sourcedir,2):
-                        if os.path.exists(os.path.join(dirname, ".templateconf")):
-                            import subprocess
-                            proc = subprocess.Popen('bash -c ". '+os.path.join(dirname, ".templateconf")+'; echo \"\$TEMPLATECONF\""', shell=True, stdout=subprocess.PIPE)
-                            conffilepath, stderroroutput = proc.communicate()
-                            proc.wait()
-                            if proc.returncode != 0:
-                                raise Exception("Failed to source TEMPLATECONF: %s" % stderroroutput)
+                        LoadConfigCommand()._import_layer_config(config_file)
+                        # we run lsupdates after config update
+                        print "\nLayer configuration imported. Updating information from the layer sources, please wait.\nYou can re-update any time later by running bitbake/lib/toaster/manage.py lsupdates"
+                        from django.core.management import call_command
+                        call_command("lsupdates")
 
-                            conffilepath = os.path.join(conffilepath.strip(), "toasterconf.json")
-                            candidatefilepath = os.path.join(dirname, conffilepath)
-                            if "toaster_cloned" in candidatefilepath:
-                                continue
-                            if os.path.exists(candidatefilepath):
-                                config_files.append(candidatefilepath)
-
-                    if len(config_files) > 0:
-                        print "Toaster will list now the configuration files that it found. Select the number to use the desired configuration file."
-                        for cf in config_files:
-                            print "  [%d] - %s" % (config_files.index(cf) + 1, cf)
-                        print "\n  [0] - Exit without importing any file"
-                        try:
-                                i = raw_input("\nEnter your option: ")
-                                if len(i) and (int(i) - 1 >= 0 and int(i) - 1 < len(config_files)):
-                                    print "\nImporting file: %s" % config_files[int(i)-1]
-                                    from loadconf import Command as LoadConfigCommand
-
-                                    LoadConfigCommand()._import_layer_config(config_files[int(i)-1])
-                                    # we run lsupdates after config update
-                                    print "\nLayer configuration imported. Updating information from the layer sources, please wait.\nYou can re-update any time later by running bitbake/lib/toaster/manage.py lsupdates"
-                                    from django.core.management import call_command
-                                    call_command("lsupdates")
-
-                                    # we don't look for any other config files
-                                    return is_changed
-                        except Exception as e:
-                            print "Failure while trying to import the toaster config file: %s" % e
-                            traceback.print_exc(e)
-                    else:
-                        print "\nToaster could not find a configuration file. You need to configure Toaster manually using the web interface, or create a configuration file and use\n  bitbake/lib/toaster/managepy.py loadconf [filename]\n command to load it. You can use https://wiki.yoctoproject.org/wiki/File:Toasterconf.json.txt.patch as a starting point."
-
-
-
+                        # we don't look for any other config files
+                        return is_changed
+                    except Exception as e:
+                        print "Failure while trying to import the toaster config file %s: %s" %\
+                            (config_file, e)
+                        traceback.print_exc(e)
 
                 return is_changed
 
-            while (_verify_be()):
+            while _verify_be():
                 pass
         return 0
 
     def _verify_default_settings(self):
         # verify that default settings are there
-        if ToasterSetting.objects.filter(name = 'DEFAULT_RELEASE').count() != 1:
-            ToasterSetting.objects.filter(name = 'DEFAULT_RELEASE').delete()
-            ToasterSetting.objects.get_or_create(name = 'DEFAULT_RELEASE', value = '')
+        if ToasterSetting.objects.filter(name='DEFAULT_RELEASE').count() != 1:
+            ToasterSetting.objects.filter(name='DEFAULT_RELEASE').delete()
+            ToasterSetting.objects.get_or_create(name='DEFAULT_RELEASE', value='')
         return 0
 
     def _verify_builds_in_progress(self):
         # we are just starting up. we must not have any builds in progress, or build environments taken
-        for b in BuildRequest.objects.filter(state = BuildRequest.REQ_INPROGRESS):
-            BRError.objects.create(req = b, errtype = "toaster", errmsg = "Toaster found this build IN PROGRESS while Toaster started up. This is an inconsistent state, and the build was marked as failed")
+        for b in BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS):
+            BRError.objects.create(req=b, errtype="toaster",
+                                   errmsg=
+                                   "Toaster found this build IN PROGRESS while Toaster started up. This is an inconsistent state, and the build was marked as failed")
 
-        BuildRequest.objects.filter(state = BuildRequest.REQ_INPROGRESS).update(state = BuildRequest.REQ_FAILED)
+        BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS).update(state=BuildRequest.REQ_FAILED)
 
-        BuildEnvironment.objects.update(lock = BuildEnvironment.LOCK_FREE)
+        BuildEnvironment.objects.update(lock=BuildEnvironment.LOCK_FREE)
 
         # also mark "In Progress builds as failures"
         from django.utils import timezone
-        Build.objects.filter(outcome = Build.IN_PROGRESS).update(outcome = Build.FAILED, completed_on = timezone.now())
+        Build.objects.filter(outcome=Build.IN_PROGRESS).update(outcome=Build.FAILED, completed_on=timezone.now())
 
         return 0
 
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
index 718e144..48dc618 100644
--- a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
+++ b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
@@ -5,6 +5,7 @@
 from bldcontrol.models import BuildRequest, BuildEnvironment, BRError, BRVariable
 import os
 import logging
+import time
 
 logger = logging.getLogger("ToasterScheduler")
 
@@ -118,7 +119,7 @@
             br.save()
             # transpose target information
             for brtarget in br.brtarget_set.all():
-                Target.objects.create(build = br.build, target= brtarget.target)
+                Target.objects.create(build=br.build, target=brtarget.target, task=brtarget.task)
             # transpose the launch errors in ToasterExceptions
             for brerror in br.brerror_set.all():
                 LogMessage.objects.create(build = br.build, level = LogMessage.EXCEPTION, message = brerror.errmsg)
@@ -128,6 +129,12 @@
 
 
     def handle_noargs(self, **options):
-        self.cleanup()
-        self.archive()
-        self.schedule()
+        while True:
+            try:
+                self.cleanup()
+                self.archive()
+                self.schedule()
+            except:
+                pass
+
+            time.sleep(1)
diff --git a/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py b/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py
new file mode 100644
index 0000000..9b50bc1
--- /dev/null
+++ b/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'BRLayer.layer_version'
+        db.add_column(u'bldcontrol_brlayer', 'layer_version',
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Layer_Version'], null=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'BRLayer.layer_version'
+        db.delete_column(u'bldcontrol_brlayer', 'layer_version_id')
+
+
+    models = {
+        u'bldcontrol.brbitbake': {
+            'Meta': {'object_name': 'BRBitbake'},
+            'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']", 'unique': 'True'})
+        },
+        u'bldcontrol.brerror': {
+            'Meta': {'object_name': 'BRError'},
+            'errmsg': ('django.db.models.fields.TextField', [], {}),
+            'errtype': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}),
+            'traceback': ('django.db.models.fields.TextField', [], {})
+        },
+        u'bldcontrol.brlayer': {
+            'Meta': {'object_name': 'BRLayer'},
+            'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"})
+        },
+        u'bldcontrol.brtarget': {
+            'Meta': {'object_name': 'BRTarget'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'})
+        },
+        u'bldcontrol.brvariable': {
+            'Meta': {'object_name': 'BRVariable'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}),
+            'value': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        u'bldcontrol.buildenvironment': {
+            'Meta': {'object_name': 'BuildEnvironment'},
+            'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
+            'bbaddress': ('django.db.models.fields.CharField', [], {'max_length': '254', 'blank': 'True'}),
+            'bbport': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+            'bbstate': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'bbtoken': ('django.db.models.fields.CharField', [], {'max_length': '126', 'blank': 'True'}),
+            'betype': ('django.db.models.fields.IntegerField', [], {}),
+            'builddir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'lock': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'sourcedir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
+        },
+        u'bldcontrol.buildrequest': {
+            'Meta': {'object_name': 'BuildRequest'},
+            'build': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['orm.Build']", 'unique': 'True', 'null': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'environment': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildEnvironment']", 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'state': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
+        },
+        u'orm.bitbakeversion': {
+            'Meta': {'object_name': 'BitbakeVersion'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        },
+        u'orm.branch': {
+            'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.build': {
+            'Meta': {'object_name': 'Build'},
+            'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'completed_on': ('django.db.models.fields.DateTimeField', [], {}),
+            'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
+            'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'started_on': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        u'orm.layer': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'},
+            'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}),
+            'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'})
+        },
+        u'orm.layer_version': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}),
+            'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}),
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}),
+            'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.layersource': {
+            'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'},
+            'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}),
+            'sourcetype': ('django.db.models.fields.IntegerField', [], {})
+        },
+        u'orm.project': {
+            'Meta': {'object_name': 'Project'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
+        },
+        u'orm.release': {
+            'Meta': {'object_name': 'Release'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}),
+            'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        }
+    }
+
+    complete_apps = ['bldcontrol']
\ No newline at end of file
diff --git a/bitbake/lib/toaster/bldcontrol/models.py b/bitbake/lib/toaster/bldcontrol/models.py
index b61de58..ab41105 100644
--- a/bitbake/lib/toaster/bldcontrol/models.py
+++ b/bitbake/lib/toaster/bldcontrol/models.py
@@ -1,6 +1,6 @@
 from django.db import models
 from django.core.validators import MaxValueValidator, MinValueValidator
-from orm.models import Project, ProjectLayer, ProjectVariable, ProjectTarget, Build
+from orm.models import Project, ProjectLayer, ProjectVariable, ProjectTarget, Build, Layer_Version
 
 # a BuildEnvironment is the equivalent of the "build/" directory on the localhost
 class BuildEnvironment(models.Model):
@@ -39,40 +39,6 @@
     created     = models.DateTimeField(auto_now_add = True)
     updated     = models.DateTimeField(auto_now = True)
 
-
-    def get_artifact_type(self, path):
-        if self.betype == BuildEnvironment.TYPE_LOCAL:
-            try:
-                import magic
-
-                # fair warning: this is a mess; there are multiple competeing and incompatible
-                # magic modules floating around, so we try some of the most common combinations
-
-                try:    # we try ubuntu's python-magic 5.4
-                    m = magic.open(magic.MAGIC_MIME_TYPE)
-                    m.load()
-                    return m.file(path)
-                except AttributeError:
-                    pass
-
-                try:    # we try python-magic 0.4.6
-                    m = magic.Magic(magic.MAGIC_MIME)
-                    return m.from_file(path)
-                except AttributeError:
-                    pass
-
-                try:    # we try pip filemagic 1.6
-                    m = magic.Magic(flags=magic.MAGIC_MIME_TYPE)
-                    return m.id_filename(path)
-                except AttributeError:
-                    pass
-
-                return "binary/octet-stream"
-            except ImportError:
-                return "binary/octet-stream"
-        raise Exception("FIXME: artifact type not implemented for build environment type %s" % self.get_betype_display())
-
-
     def get_artifact(self, path):
         if self.betype == BuildEnvironment.TYPE_LOCAL:
             return open(path, "r")
@@ -137,6 +103,7 @@
     giturl      = models.CharField(max_length = 254)
     commit      = models.CharField(max_length = 254)
     dirpath     = models.CharField(max_length = 254)
+    layer_version = models.ForeignKey(Layer_Version, null=True)
 
 class BRBitbake(models.Model):
     req         = models.ForeignKey(BuildRequest, unique = True)    # only one bitbake for a request
diff --git a/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py b/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py
index 2a2078f..eac167b 100755
--- a/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py
+++ b/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py
@@ -221,6 +221,68 @@
     """
     return re.findall(r'([0-9]+)', s)
 
+# Below is decorator derived from toaster backend test code
+class NoParsingFilter(logging.Filter):
+    def filter(self, record):
+        return record.levelno == 100
+
+def LogResults(original_class):
+    orig_method = original_class.run
+
+    #rewrite the run method of unittest.TestCase to add testcase logging
+    def run(self, result, *args, **kws):
+        orig_method(self, result, *args, **kws)
+        passed = True
+        testMethod = getattr(self, self._testMethodName)
+
+        #if test case is decorated then use it's number, else use it's name
+        try:
+            test_case = testMethod.test_case
+        except AttributeError:
+            test_case = self._testMethodName
+
+        #create custom logging level for filtering.
+        custom_log_level = 100
+        logging.addLevelName(custom_log_level, 'RESULTS')
+        caller = os.path.basename(sys.argv[0])
+
+        def results(self, message, *args, **kws):
+            if self.isEnabledFor(custom_log_level):
+                self.log(custom_log_level, message, *args, **kws)
+        logging.Logger.results = results
+
+        logging.basicConfig(filename=os.path.join(os.getcwd(),'results-'+caller+'.log'),
+                            filemode='w',
+                            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+                            datefmt='%H:%M:%S',
+                            level=custom_log_level)
+        for handler in logging.root.handlers:
+            handler.addFilter(NoParsingFilter())
+#        local_log = logging.getLogger(caller)
+        local_log = logging.getLogger()
+
+        #check status of tests and record it
+        for (name, msg) in result.errors:
+            if self._testMethodName == str(name).split(' ')[0]:
+                local_log.results("Testcase "+str(test_case)+": ERROR")
+                local_log.results("Testcase "+str(test_case)+":\n"+msg+"\n\n\n")
+                passed = False
+        for (name, msg) in result.failures:
+            if self._testMethodName == str(name).split(' ')[0]:
+                local_log.results("Testcase "+str(test_case)+": FAILED")
+                local_log.results("Testcase "+str(test_case)+":\n"+msg+"\n\n\n")
+                passed = False
+        for (name, msg) in result.skipped:
+            if self._testMethodName == str(name).split(' ')[0]:
+                local_log.results("Testcase "+str(test_case)+": SKIPPED"+"\n\n\n")
+                passed = False
+        if passed:
+            local_log.results("Testcase "+str(test_case)+": PASSED"+"\n\n\n")
+
+    original_class.run = run
+    return original_class
+
+
 
 
 ###########################################
@@ -321,7 +383,10 @@
             log_path = log_dir + os.sep +  self.browser + '-' +\
                     item + '-' + add_name + '-' + str(self.screenshot_sequence) + '.png'
             if item == 'native':
-                os.system("scrot " + log_path)
+                if self.host_os == "linux":
+                    os.system("scrot " + log_path)
+                elif self.host_os=="darwin":
+                    os.system("screencapture -x " + log_path)
             elif item == 'selenium':
                 self.driver.get_screenshot_as_file(log_path)
             self.screenshot_sequence += 1
@@ -531,6 +596,7 @@
     def is_text_present (self, patterns):
         for pattern in patterns:
             if str(pattern) not in self.driver.page_source:
+                print pattern
                 return False
         return True
 
@@ -592,7 +658,7 @@
 # Note: to comply with the unittest framework, we call these test_xxx functions
 # from run_toastercases.py to avoid calling setUp() and tearDown() multiple times
 
-
+@LogResults
 class toaster_cases(toaster_cases_base):
         ##############
         #  CASE 901  #
@@ -627,12 +693,12 @@
             if is_list_inverted(column_list):
                 self.driver.find_element_by_link_text(key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key))
             else:
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list), msg=("%s column not sequenced" % key))
                 self.driver.find_element_by_link_text(key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_inverted(column_list))
+                self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key))
         self.log.info("case passed")
 
 
@@ -656,10 +722,10 @@
             # if nothing found, we still count it as "pass"
             if new_target_column_texts:
                 for text in new_target_column_texts:
-                    self.failUnless(text.find(pattern))
+                    self.assertTrue(text.find(pattern), msg=("%s item doesn't exist " % pattern))
             self.driver.find_element_by_css_selector("i.icon-remove").click()
             target_column_texts = self.get_table_column_text("class", "target")
-            self.failUnless(ori_target_column_texts == target_column_texts)
+            self.assertTrue(ori_target_column_texts == target_column_texts, msg=("builds changed after operations"))
 
 
         ##############
@@ -682,10 +748,9 @@
             try:
                 temp_element = self.find_element_by_text_in_table('otable', item)
                 # this is how we find "filter icon" in the same level as temp_element(where "a" means clickable, "i" means icon)
-                self.failUnless(temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']"))
+                self.assertTrue(temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']"))
             except Exception,e:
-                self.log.error(" %s cannot be found! %s" % (item, e))
-                self.failIf(True)
+                self.assertFalse(True, msg=(" %s cannot be found! %s" % (item, e)))
                 raise
         # step 5-6
         temp_element = self.find_element_by_link_text_in_table('otable', 'Outcome')
@@ -728,10 +793,12 @@
         # This is how we find the "default" rows-number!
         rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text)
         print rows_displayed
-        self.failUnless(self.get_table_element(self.table_name, rows_displayed))
-        self.failIf(self.get_table_element(self.table_name, rows_displayed + 1))
+        self.assertTrue(self.get_table_element(self.table_name, rows_displayed), msg=("not enough rows displayed"))
+        self.assertFalse(self.get_table_element(self.table_name, rows_displayed + 1), \
+                         msg=("more rows displayed than expected"))
         # Search text box background text is "Search tasks"
-        self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search tasks']"))
+        self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search tasks']"),\
+                        msg=("background text doesn't exist"))
 
         self.driver.find_element_by_id("search").clear()
         self.driver.find_element_by_id("search").send_keys("busybox")
@@ -760,22 +827,23 @@
             column_list = self.get_table_column_text("class", table_head_dict[key])
 # after 1st click, the list should be either sequenced or inverted, but we don't have a "default order" here
 # the point is, after another click, it should be another order
-# the fist case is special:this means every item in column_list is the same, so
+# the first case is special:this means every item in column_list is the same, so
 # after one click, either sequenced or inverted will be fine
             if (is_list_inverted(column_list) and is_list_sequenced(column_list)) \
                 or (not column_list) :
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list))
+                self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list), \
+                                msg=("%s column not in any order" % key))
             elif is_list_inverted(column_list):
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key))
             else:
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key))
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_inverted(column_list))
+                self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key))
 # step 8-10
         # filter dict: {link text name : filter table name in xpath}
         filter_dict = {'Executed':'filter_executed', 'Outcome':'filter_outcome', 'Cache attempt':'filter_cache_attempt'}
@@ -834,9 +902,9 @@
             self.find_element_by_link_text_in_table('nav', key).click()
             head_list = self.get_table_head_text('otable')
             for item in test_dict[key]['check_head_list']:
-                self.failUnless(item in head_list)
+                self.assertTrue(item in head_list, msg=("%s not in head row" % item))
             column_list = self.get_table_column_text('class', test_dict[key]['class'])
-            self.failUnless(is_list_inverted(column_list))
+            self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key))
 
             self.driver.find_element_by_id("edit-columns-button").click()
             for item2 in test_dict[key]['check_column_list']:
@@ -862,21 +930,24 @@
         self.driver.find_element_by_partial_link_text("Generated files").click()
         head_list = self.get_table_head_text('otable')
         for item in ['File', 'Size']:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("%s not in head row" % item))
         c_list = self.get_table_column_text('class', 'path')
-        self.failUnless(is_list_sequenced(c_list))
+        self.assertTrue(is_list_sequenced(c_list), msg=("column not in order"))
 # step 7
         self.driver.find_element_by_partial_link_text("Runtime dependencies").click()
         # save sceen here to observe...
         # note that here table name is not 'otable'
         head_list = self.get_table_head_text('dependencies')
         for item in ['Package', 'Version', 'Size']:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("%s not in head row" % item))
         c_list = self.get_table_column_text_by_column_number('dependencies', 1)
-        self.failUnless(is_list_sequenced(c_list))
+        self.assertTrue(is_list_sequenced(c_list), msg=("list not in order"))
         texts = ['Size', 'License', 'Recipe', 'Recipe version', 'Layer', \
-                     'Layer branch', 'Layer commit', 'Layer directory']
-        self.failUnless(self.is_text_present(texts))
+                     'Layer branch', 'Layer commit']
+        time.sleep(1)
+#        for text in texts:
+#            self.assertTrue(self.is_text_present(text), msg=("text %s not in page" % text))
+        self.assertTrue(self.is_text_present(texts), msg=("text  not in page"))
 
 
         ##############
@@ -898,8 +969,8 @@
         # This is how we find the "default" rows-number!
         rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text)
         print rows_displayed
-        self.failUnless(self.get_table_element(self.table_name, rows_displayed))
-        self.failIf(self.get_table_element(self.table_name, rows_displayed + 1))
+        self.assertTrue(self.get_table_element(self.table_name, rows_displayed))
+        self.assertFalse(self.get_table_element(self.table_name, rows_displayed + 1))
 
         # Check the default table is sorted by Recipe
         tasks_column_count = len(self.driver.find_elements_by_xpath("/html/body/div[2]/div/div[2]/div[2]/table/tbody/tr/td[1]"))
@@ -907,10 +978,10 @@
         default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1)
         #print default_column_list
 
-        self.failUnless(is_list_sequenced(default_column_list))
+        self.assertTrue(is_list_sequenced(default_column_list))
 
         # Search text box background text is "Search recipes"
-        self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
+        self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
 
         self.driver.find_element_by_id("search").clear()
         self.driver.find_element_by_id("search").send_keys(test_package1)
@@ -937,7 +1008,7 @@
 
         #self.driver.find_element_by_partial_link_text("zlib").click()
         #self.driver.back()
-        #self.failUnless(is_list_inverted(inverted_column_list))
+        #self.assertTrue(is_list_inverted(inverted_column_list))
         #self.find_element_by_link_text_in_table(self.table_name, 'Recipe').click()
 
         table_head_dict = {'Recipe':'recipe__name', 'Recipe file':'recipe_file', 'Section':'recipe_section', \
@@ -950,52 +1021,52 @@
                     or (not column_list) :
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list))
+                self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list))
                 self.driver.find_element_by_partial_link_text("acl").click()
                 self.driver.back()
-                self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list))
+                self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list))
                 # Search text box background text is "Search recipes"
-                self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
+                self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
                 self.driver.find_element_by_id("search").clear()
                 self.driver.find_element_by_id("search").send_keys(test_package2)
                 self.driver.find_element_by_id("search-button").click()
                 column_search_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_search_list) or is_list_inverted(column_search_list))
+                self.assertTrue(is_list_sequenced(column_search_list) or is_list_inverted(column_search_list))
                 self.driver.find_element_by_css_selector("i.icon-remove").click()
             elif is_list_inverted(column_list):
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list))
                 self.driver.find_element_by_partial_link_text("acl").click()
                 self.driver.back()
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list))
                 # Search text box background text is "Search recipes"
-                self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
+                self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
                 self.driver.find_element_by_id("search").clear()
                 self.driver.find_element_by_id("search").send_keys(test_package2)
                 self.driver.find_element_by_id("search-button").click()
                 column_search_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_sequenced(column_search_list))
+                self.assertTrue(is_list_sequenced(column_search_list))
                 self.driver.find_element_by_css_selector("i.icon-remove").click()
             else:
-                self.failUnless(is_list_sequenced(column_list))
+                self.assertTrue(is_list_sequenced(column_list))
                 self.find_element_by_link_text_in_table(self.table_name, key).click()
                 column_list = self.get_table_column_text("class", table_head_dict[key])
-                self.failUnless(is_list_inverted(column_list))
+                self.assertTrue(is_list_inverted(column_list))
                 try:
                     self.driver.find_element_by_partial_link_text("acl").click()
                 except:
                     self.driver.find_element_by_partial_link_text("zlib").click()
                 self.driver.back()
-                self.failUnless(is_list_inverted(column_list))
+                self.assertTrue(is_list_inverted(column_list))
                 # Search text box background text is "Search recipes"
-                self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
+                self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']"))
                 self.driver.find_element_by_id("search").clear()
                 self.driver.find_element_by_id("search").send_keys(test_package2)
                 self.driver.find_element_by_id("search-button").click()
                 column_search_list = self.get_table_column_text("class", table_head_dict[key])
                 #print column_search_list
-                self.failUnless(is_list_inverted(column_search_list))
+                self.assertTrue(is_list_inverted(column_search_list))
                 self.driver.find_element_by_css_selector("i.icon-remove").click()
 
         # Bug 5919
@@ -1011,7 +1082,7 @@
             #print tasks_column_count
             default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1)
             #print default_column_list
-            self.failUnless(is_list_sequenced(default_column_list))
+            self.assertTrue(is_list_sequenced(default_column_list))
 
         self.driver.find_element_by_id("edit-columns-button").click()
         self.driver.find_element_by_id("recipe_file").click()
@@ -1061,31 +1132,31 @@
         # step 3
         head_list = self.get_table_head_text('otable')
         for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']:
-            self.failUnless(item in head_list)
-        self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click()
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
+        self.driver.find_element_by_id("edit-columns-button").click()
         self.driver.find_element_by_id("depends_on").click()
         self.driver.find_element_by_id("layer_version__branch").click()
         self.driver.find_element_by_id("layer_version__layer__commit").click()
         self.driver.find_element_by_id("depends_by").click()
-        self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click()
+        self.driver.find_element_by_id("edit-columns-button").click()
         # check if columns selected above is shown
-        check_list = ['Dependencies', 'Layer branch', 'Layer commit', 'Layer directory', 'Reverse dependencies']
+        check_list = ['Dependencies', 'Layer branch', 'Layer commit', 'Reverse dependencies']
         head_list = self.get_table_head_text('otable')
         time.sleep(2)
         print head_list
         for item in check_list:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
         # un-check 'em all
-        self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click()
+        self.driver.find_element_by_id("edit-columns-button").click()
         self.driver.find_element_by_id("depends_on").click()
         self.driver.find_element_by_id("layer_version__branch").click()
         self.driver.find_element_by_id("layer_version__layer__commit").click()
         self.driver.find_element_by_id("depends_by").click()
-        self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click()
+        self.driver.find_element_by_id("edit-columns-button").click()
         # don't exist any more
         head_list = self.get_table_head_text('otable')
         for item in check_list:
-            self.failIf(item in head_list)
+            self.assertFalse(item in head_list, msg=("item %s should not be in head row" % item))
 
 
         ##############
@@ -1101,7 +1172,7 @@
         # step 3
         head_list = self.get_table_head_text('otable')
         for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
         # step 4
         self.driver.find_element_by_id("edit-columns-button").click()
         # save screen
@@ -1315,7 +1386,8 @@
         head_list = self.get_table_head_text('otable')
         print head_list
         print len(head_list)
-        self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description'])
+        self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \
+                        msg=("head row contents wrong"))
 # step 8
         # search other string. and click "Variable" to re-sort, check if table
         # head is still the same
@@ -1324,10 +1396,12 @@
         self.driver.find_element_by_id("search-button").click()
         self.find_element_by_link_text_in_table('otable', 'Variable').click()
         head_list = self.get_table_head_text('otable')
-        self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description'])
+        self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \
+                        msg=("head row contents wrong"))
         self.find_element_by_link_text_in_table('otable', 'Variable').click()
         head_list = self.get_table_head_text('otable')
-        self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description'])
+        self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \
+                        msg=("head row contents wrong"))
 
 
         ##############
@@ -1343,11 +1417,11 @@
         self.find_element_by_link_text_in_table('nav', 'Configuration').click()
         self.driver.find_element_by_link_text("BitBake variables").click()
         variable_list = self.get_table_column_text('class', 'variable_name')
-        self.failUnless(is_list_sequenced(variable_list))
+        self.assertTrue(is_list_sequenced(variable_list), msg=("list not in order"))
 # step 4
         self.find_element_by_link_text_in_table('otable', 'Variable').click()
         variable_list = self.get_table_column_text('class', 'variable_name')
-        self.failUnless(is_list_inverted(variable_list))
+        self.assertTrue(is_list_inverted(variable_list), msg=("list not inverted"))
         self.find_element_by_link_text_in_table('otable', 'Variable').click()
 # step 5
         # searching won't change the sequentiality
@@ -1355,7 +1429,7 @@
         self.driver.find_element_by_id("search").send_keys("lib")
         self.driver.find_element_by_id("search-button").click()
         variable_list = self.get_table_column_text('class', 'variable_name')
-        self.failUnless(is_list_sequenced(variable_list))
+        self.assertTrue(is_list_sequenced(variable_list), msg=("list not in order"))
 
 
         ##############
@@ -1369,7 +1443,7 @@
         # Step 2
         # default sequence in "Completed on" column is inverted
         c_list = self.get_table_column_text('class', 'completed_on')
-        self.failUnless(is_list_inverted(c_list))
+        self.assertTrue(is_list_inverted(c_list), msg=("list not inverted"))
         # step 3
         self.driver.find_element_by_id("edit-columns-button").click()
         self.driver.find_element_by_id("started_on").click()
@@ -1377,8 +1451,8 @@
         self.driver.find_element_by_id("time").click()
         self.driver.find_element_by_id("edit-columns-button").click()
         head_list = self.get_table_head_text('otable')
-        for item in ['Outcome', 'Target', 'Machine', 'Started on', 'Completed on', 'Failed tasks', 'Errors', 'Warnings', 'Warnings', 'Time']:
-            self.failUnless(item in head_list)
+        for item in ['Outcome', 'Recipe', 'Machine', 'Started on', 'Completed on', 'Failed tasks', 'Errors', 'Warnings', 'Warnings', 'Time']:
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
 
 
         ##############
@@ -1392,7 +1466,7 @@
         # Please refer to case 924 requirement
         # default sequence in "Completed on" column is inverted
         c_list = self.get_table_column_text('class', 'completed_on')
-        self.failUnless(is_list_inverted(c_list))
+        self.assertTrue(is_list_inverted(c_list), msg=("list not inverted"))
         # Step 4
         # click Errors , order in "Completed on" should be disturbed. Then hide
         # error column to check if order in "Completed on" can be restored
@@ -1403,7 +1477,7 @@
         # Note: without time.sleep here, there'll be unpredictable error..TBD
         time.sleep(1)
         c_list = self.get_table_column_text('class', 'completed_on')
-        self.failUnless(is_list_inverted(c_list))
+        self.assertTrue(is_list_inverted(c_list), msg=("list not inverted"))
 
 
         ##############
@@ -1419,7 +1493,7 @@
         self.find_element_by_link_text_in_table('nav', 'Packages').click()
         check_head_list = ['Package', 'Package version', 'Size', 'Recipe']
         head_list = self.get_table_head_text('otable')
-        self.failUnless(head_list == check_head_list)
+        self.assertTrue(head_list == check_head_list, msg=("head row not as expected"))
 # Step 4
         # pulldown menu
         option_ids = ['recipe__layer_version__layer__name', 'recipe__layer_version__branch', \
@@ -1448,7 +1522,7 @@
         self.find_element_by_link_text_in_table('nav', 'Packages').click()
         # column -- Package
         column_list = self.get_table_column_text_by_column_number('otable', 1)
-        self.failUnless(is_list_sequenced(column_list))
+        self.assertTrue(is_list_sequenced(column_list), msg=("list not in order"))
         self.find_element_by_link_text_in_table('otable', 'Size').click()
 
 
@@ -1470,7 +1544,7 @@
         self.driver.find_element_by_id("edit-columns-button").click()
         #get modified table header
         new_head = self.get_table_head_text('otable')
-        self.failUnless(head_list > new_head)
+        self.assertTrue(head_list > new_head)
 
         ##############
         #  CASE 943  #
@@ -1487,7 +1561,7 @@
         self.driver.find_element_by_id("search").send_keys("bash")
         self.driver.find_element_by_id("search-button").click()
         #check for the search result message "XX packages found"
-        self.failUnless(self.is_text_present("packages found"))
+        self.assertTrue(self.is_text_present("packages found"), msg=("no packages found text"))
 
 
         ##############
@@ -1508,11 +1582,12 @@
         self.driver.find_element_by_id("edit-columns-button").click()
         # otable is the recipes table here
         otable_head_text = self.get_table_head_text('otable')
-        for item in ["Layer", "Layer branch", "Layer commit", "Layer directory"]:
-            self.failIf(item not in otable_head_text)
+        for item in ["Layer", "Layer branch", "Layer commit"]:
+            self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item))
         # click the fist recipe, whatever it is
         self.get_table_element("otable", 1, 1).click()
-        self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit", "Recipe file"]))
+        self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit", "Recipe file"]), \
+                        msg=("text not in web page"))
 
         # step 2: test Packages page stuff. almost same as above
         self.driver.back()
@@ -1525,10 +1600,11 @@
         self.driver.find_element_by_id("edit-columns-button").click()
         otable_head_text = self.get_table_head_text("otable")
         for item in ["Layer", "Layer branch", "Layer commit"]:
-            self.failIf(item not in otable_head_text)
+            self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item))
         # click the fist recipe, whatever it is
         self.get_table_element("otable", 1, 1).click()
-        self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"]))
+        self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit"]), \
+                        msg=("text not in web page"))
 
         # step 3: test Packages core-image-minimal(images) stuff. almost same as above. Note when future element-id changes...
         self.driver.back()
@@ -1540,17 +1616,18 @@
         self.driver.find_element_by_id("edit-columns-button").click()
         otable_head_text = self.get_table_head_text("otable")
         for item in ["Layer", "Layer branch", "Layer commit"]:
-            self.failIf(item not in otable_head_text)
+            self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item))
         # click the fist recipe, whatever it is
         self.get_table_element("otable", 1, 1).click()
-        self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"]))
+        self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit"]), \
+                        msg=("text not in web page"))
 
         # step 4: check Configuration page
         self.driver.back()
         self.driver.find_element_by_link_text("Configuration").click()
         otable_head_text = self.get_table_head_text()
         for item in ["Layer", "Layer branch", "Layer commit"]:
-            self.failIf(item not in otable_head_text)
+            self.assertTrue(item not in otable_head_text, msg=("item %s should not be in head row" % item))
 
 
         ##############
@@ -1575,14 +1652,14 @@
             # Sure we can use driver.get(url) to refresh page, but since page will vary, we use click link text here
             self.driver.find_element_by_link_text(items).click()
             Select(self.driver.find_element_by_css_selector("select.pagesize")).select_by_visible_text(str(rows_displayed))
-            self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]"))
-            self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]"))
+            self.assertTrue(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]"))
+            self.assertFalse(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]"))
 
             # click 1st package, then go back to check if it's still those rows shown.
             self.driver.find_element_by_xpath(xpath_table + "/tr[1]/td[1]").click()
             self.driver.find_element_by_link_text(items).click()
-            self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]"))
-            self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]"))
+            self.assertTrue(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]"))
+            self.assertFalse(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]"))
 
 
         ##############
@@ -1673,7 +1750,7 @@
         check_list = ['Description', 'Set in file']
         head_list = self.get_table_head_text('otable')
         for item in check_list:
-            self.failIf(item in head_list)
+            self.assertFalse(item in head_list, msg=("item %s should not be in head row" % item))
         # check these 2 options and verify again
         self.driver.find_element_by_id('edit-columns-button').click()
         self.driver.find_element_by_xpath(xpath_option('description')).click()
@@ -1681,7 +1758,7 @@
         self.driver.find_element_by_id('edit-columns-button').click()
         head_list = self.get_table_head_text('otable')
         for item in check_list:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
 
 
         ##############
@@ -1703,7 +1780,7 @@
         self.driver.find_element_by_id("search-button").click()
         #get number of variables visible after search
         number_after_search = self.driver.find_element_by_class_name('page-header').text
-        self.failUnless(number_before_search > number_after_search)
+        self.assertTrue(number_before_search > number_after_search, msg=("items should be less after search"))
 
 
         ##############
@@ -1722,11 +1799,11 @@
             self.driver.find_element_by_partial_link_text("Directory structure")
         except Exception,e:
             self.log.error(e)
-            self.failIf(True)
+            self.assertFalse(True)
         # step 4
         head_list = self.get_table_head_text('otable')
         for item in ['Package', 'Package version', 'Size', 'Dependencies', 'Reverse dependencies', 'Recipe']:
-            self.failUnless(item in head_list)
+            self.assertTrue(item in head_list, msg=("item %s not in head row" % item))
         # step 5-6
         self.driver.find_element_by_id("edit-columns-button").click()
         selectable_class = 'checkbox'
@@ -1746,22 +1823,15 @@
             unselectable_list.append(element.text)
         # check them
         for item in selectable_check_list:
-            if item not in selectable_list:
-                self.log.error(" %s not found in dropdown menu \n" % item)
-                self.failIf(True)
+            self.assertTrue(item in selectable_list, msg=("%s not found in dropdown menu" % item))
         for item in unselectable_check_list:
-            if item not in unselectable_list:
-                self.log.error(" %s not found in dropdown menu \n" % item)
-                self.failIf(True)
+            self.assertTrue(item in unselectable_list, msg=("%s not found in dropdown menu" % item))
         self.driver.find_element_by_id("edit-columns-button").click()
         # step 7
         self.driver.find_element_by_partial_link_text("Directory structure").click()
         head_list = self.get_table_head_text('dirtable')
         for item in ['Directory / File', 'Symbolic link to', 'Source package', 'Size', 'Permissions', 'Owner', 'Group']:
-            if item not in head_list:
-                self.log.error(" %s not found in Directory structure table head \n" % item)
-                self.failIf(True)
-
+            self.assertTrue(item in head_list, msg=("%s not found in Directory structure table head" % item))
 
         ##############
         #  CASE 950  #
@@ -1791,12 +1861,11 @@
                 try:
                     self.find_element_by_link_text_in_table('nav', item)
                 except Exception:
-                    self.log.error("link  %s cannot be found in the page" % item)
-                    self.failIf(True)
+                    self.assertFalse(True, msg=("link  %s cannot be found in the page" % item))
             # step 6
             check_list_2 = ['Packages included', 'Total package size', \
                       'License manifest', 'Image files']
-            self.failUnless(self.is_text_present(check_list_2))
+            self.assertTrue(self.is_text_present(check_list_2), msg=("text not in web page"))
             self.driver.back()
         try:
             fail_icon = self.driver.find_element_by_xpath("//*[@class='icon-minus-sign error']")
@@ -1813,12 +1882,11 @@
                 try:
                     self.find_element_by_link_text_in_table('nav', item)
                 except Exception:
-                    self.log.error("link  %s cannot be found in the page" % item)
-                    self.failIf(True)
+                    self.assertFalse(True, msg=("link  %s cannot be found in the page" % item))
             # step 7 involved
             check_list_3 = ['Machine', 'Distro', 'Layers', 'Total number of tasks', 'Tasks executed', \
                       'Tasks not executed', 'Reuse', 'Recipes built', 'Packages built']
-            self.failUnless(self.is_text_present(check_list_3))
+            self.assertTrue(self.is_text_present(check_list_3), msg=("text not in web page"))
             self.driver.back()
 
 
@@ -1878,6 +1946,5 @@
                        tasks, recipes, packages need to run manually")
         self.driver.find_element_by_partial_link_text("Toaster manual").click()
         if not self.is_text_present("Toaster Manual"):
-            self.log.error("please check [Toaster manual] link on page")
-            self.failIf(True)
+            self.assertFalse(True, msg=("please check [Toaster manual] link on page"))
 
diff --git a/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg b/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg
index 6405f9a..685a9ee 100644
--- a/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg
+++ b/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg
@@ -18,4 +18,8 @@
 test_cases = [901, 902, 903]
 logging_level = 'DEBUG'
 
-
+[toaster_test_darwin]
+toaster_url = 'http://127.0.0.1:8000'
+test_browser = 'firefox'
+test_cases = [901, 902, 903, 904, 906, 910, 911, 912, 913, 914, 915, 916, 923, 924, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 955, 956]
+logging_level = 'INFO'
diff --git a/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py b/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py
new file mode 100644
index 0000000..6030605
--- /dev/null
+++ b/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py
@@ -0,0 +1,375 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'CustomImageRecipe'
+        db.create_table(u'orm_customimagerecipe', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
+            ('base_recipe', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Recipe'])),
+            ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Project'])),
+        ))
+        db.send_create_signal(u'orm', ['CustomImageRecipe'])
+
+        # Adding M2M table for field packages on 'CustomImageRecipe'
+        m2m_table_name = db.shorten_name(u'orm_customimagerecipe_packages')
+        db.create_table(m2m_table_name, (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('customimagerecipe', models.ForeignKey(orm[u'orm.customimagerecipe'], null=False)),
+            ('package', models.ForeignKey(orm[u'orm.package'], null=False))
+        ))
+        db.create_unique(m2m_table_name, ['customimagerecipe_id', 'package_id'])
+
+        # Adding unique constraint on 'CustomImageRecipe', fields ['name', 'project']
+        db.create_unique(u'orm_customimagerecipe', ['name', 'project_id'])
+
+
+        # Changing field 'Package.build'
+        db.alter_column(u'orm_package', 'build_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Build'], null=True))
+
+    def backwards(self, orm):
+        # Removing unique constraint on 'CustomImageRecipe', fields ['name', 'project']
+        db.delete_unique(u'orm_customimagerecipe', ['name', 'project_id'])
+
+        # Deleting model 'CustomImageRecipe'
+        db.delete_table(u'orm_customimagerecipe')
+
+        # Removing M2M table for field packages on 'CustomImageRecipe'
+        db.delete_table(db.shorten_name(u'orm_customimagerecipe_packages'))
+
+
+        # Changing field 'Package.build'
+        db.alter_column(u'orm_package', 'build_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['orm.Build']))
+
+    models = {
+        u'orm.bitbakeversion': {
+            'Meta': {'object_name': 'BitbakeVersion'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        },
+        u'orm.branch': {
+            'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.build': {
+            'Meta': {'object_name': 'Build'},
+            'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'completed_on': ('django.db.models.fields.DateTimeField', [], {}),
+            'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
+            'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'started_on': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        u'orm.buildartifact': {
+            'Meta': {'object_name': 'BuildArtifact'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
+            'file_size': ('django.db.models.fields.IntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'orm.customimagerecipe': {
+            'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'CustomImageRecipe'},
+            'base_recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['orm.Package']", 'symmetrical': 'False'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"})
+        },
+        u'orm.helptext': {
+            'Meta': {'object_name': 'HelpText'},
+            'area': ('django.db.models.fields.IntegerField', [], {}),
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'helptext_build'", 'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'text': ('django.db.models.fields.TextField', [], {})
+        },
+        u'orm.layer': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'},
+            'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}),
+            'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'})
+        },
+        u'orm.layer_version': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}),
+            'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}),
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}),
+            'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.layersource': {
+            'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'},
+            'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}),
+            'sourcetype': ('django.db.models.fields.IntegerField', [], {})
+        },
+        u'orm.layerversiondependency': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'LayerVersionDependency'},
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependees'", 'to': u"orm['orm.Layer_Version']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': u"orm['orm.Layer_Version']"}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.logmessage': {
+            'Meta': {'object_name': 'LogMessage'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'lineno': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}),
+            'pathname': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'task': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Task']", 'null': 'True', 'blank': 'True'})
+        },
+        u'orm.machine': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Machine'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.package': {
+            'Meta': {'object_name': 'Package'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']", 'null': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'installed_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            'installed_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'license': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']", 'null': 'True'}),
+            'revision': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
+            'section': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        u'orm.package_dependency': {
+            'Meta': {'object_name': 'Package_Dependency'},
+            'dep_type': ('django.db.models.fields.IntegerField', [], {}),
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_target'", 'to': u"orm['orm.Package']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_source'", 'to': u"orm['orm.Package']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']", 'null': 'True'})
+        },
+        u'orm.package_file': {
+            'Meta': {'object_name': 'Package_File'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildfilelist_package'", 'to': u"orm['orm.Package']"}),
+            'path': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        },
+        u'orm.project': {
+            'Meta': {'object_name': 'Project'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
+        },
+        u'orm.projectlayer': {
+            'Meta': {'unique_together': "(('project', 'layercommit'),)", 'object_name': 'ProjectLayer'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layercommit': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}),
+            'optional': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"})
+        },
+        u'orm.projecttarget': {
+            'Meta': {'object_name': 'ProjectTarget'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'})
+        },
+        u'orm.projectvariable': {
+            'Meta': {'object_name': 'ProjectVariable'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'value': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        u'orm.recipe': {
+            'Meta': {'unique_together': "(('layer_version', 'file_path', 'pathflags'),)", 'object_name': 'Recipe'},
+            'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'file_path': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}),
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recipe_layer_version'", 'to': u"orm['orm.Layer_Version']"}),
+            'license': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'pathflags': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        u'orm.recipe_dependency': {
+            'Meta': {'object_name': 'Recipe_Dependency'},
+            'dep_type': ('django.db.models.fields.IntegerField', [], {}),
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_depends'", 'to': u"orm['orm.Recipe']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_recipe'", 'to': u"orm['orm.Recipe']"})
+        },
+        u'orm.release': {
+            'Meta': {'object_name': 'Release'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}),
+            'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        },
+        u'orm.releasedefaultlayer': {
+            'Meta': {'object_name': 'ReleaseDefaultLayer'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"})
+        },
+        u'orm.releaselayersourcepriority': {
+            'Meta': {'unique_together': "(('release', 'layer_source'),)", 'object_name': 'ReleaseLayerSourcePriority'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.LayerSource']"}),
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"})
+        },
+        u'orm.target': {
+            'Meta': {'object_name': 'Target'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'license_manifest_path': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'})
+        },
+        u'orm.target_file': {
+            'Meta': {'object_name': 'Target_File'},
+            'directory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'directory_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}),
+            'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'inodetype': ('django.db.models.fields.IntegerField', [], {}),
+            'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
+            'permission': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+            'size': ('django.db.models.fields.IntegerField', [], {}),
+            'sym_target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'symlink_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.target_image_file': {
+            'Meta': {'object_name': 'Target_Image_File'},
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '254'}),
+            'file_size': ('django.db.models.fields.IntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.target_installed_package': {
+            'Meta': {'object_name': 'Target_Installed_Package'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildtargetlist_package'", 'to': u"orm['orm.Package']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.task': {
+            'Meta': {'ordering': "('order', 'recipe')", 'unique_together': "(('build', 'recipe', 'task_name'),)", 'object_name': 'Task'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_build'", 'to': u"orm['orm.Build']"}),
+            'cpu_usage': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}),
+            'disk_io': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'elapsed_time': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'line_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'logfile': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'outcome': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+            'path_to_sstate_obj': ('django.db.models.fields.FilePathField', [], {'max_length': '500', 'blank': 'True'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'to': u"orm['orm.Recipe']"}),
+            'script_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'source_url': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'sstate_checksum': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'sstate_result': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'task_executed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'task_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'work_directory': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'})
+        },
+        u'orm.task_dependency': {
+            'Meta': {'object_name': 'Task_Dependency'},
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_depends'", 'to': u"orm['orm.Task']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'task': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_task'", 'to': u"orm['orm.Task']"})
+        },
+        u'orm.toastersetting': {
+            'Meta': {'object_name': 'ToasterSetting'},
+            'helptext': ('django.db.models.fields.TextField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '63'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'orm.variable': {
+            'Meta': {'object_name': 'Variable'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_build'", 'to': u"orm['orm.Build']"}),
+            'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'human_readable_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'variable_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'variable_value': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        u'orm.variablehistory': {
+            'Meta': {'object_name': 'VariableHistory'},
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'line_number': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'operation': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'variable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vhistory'", 'to': u"orm['orm.Variable']"})
+        }
+    }
+
+    complete_apps = ['orm']
\ No newline at end of file
diff --git a/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py b/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py
new file mode 100644
index 0000000..a2f8661
--- /dev/null
+++ b/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py
@@ -0,0 +1,345 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Changing field 'LogMessage.message'
+        db.alter_column(u'orm_logmessage', 'message', self.gf('django.db.models.fields.TextField')(null=True))
+
+    def backwards(self, orm):
+
+        # Changing field 'LogMessage.message'
+        db.alter_column(u'orm_logmessage', 'message', self.gf('django.db.models.fields.CharField')(default='', max_length=240))
+
+    models = {
+        u'orm.bitbakeversion': {
+            'Meta': {'object_name': 'BitbakeVersion'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        },
+        u'orm.branch': {
+            'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.build': {
+            'Meta': {'object_name': 'Build'},
+            'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'completed_on': ('django.db.models.fields.DateTimeField', [], {}),
+            'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
+            'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'started_on': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        u'orm.buildartifact': {
+            'Meta': {'object_name': 'BuildArtifact'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
+            'file_size': ('django.db.models.fields.IntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'orm.customimagerecipe': {
+            'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'CustomImageRecipe'},
+            'base_recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['orm.Package']", 'symmetrical': 'False'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"})
+        },
+        u'orm.helptext': {
+            'Meta': {'object_name': 'HelpText'},
+            'area': ('django.db.models.fields.IntegerField', [], {}),
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'helptext_build'", 'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'text': ('django.db.models.fields.TextField', [], {})
+        },
+        u'orm.layer': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'},
+            'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}),
+            'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}),
+            'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'})
+        },
+        u'orm.layer_version': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'},
+            'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}),
+            'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}),
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}),
+            'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.layersource': {
+            'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'},
+            'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}),
+            'sourcetype': ('django.db.models.fields.IntegerField', [], {})
+        },
+        u'orm.layerversiondependency': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'LayerVersionDependency'},
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependees'", 'to': u"orm['orm.Layer_Version']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': u"orm['orm.Layer_Version']"}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.logmessage': {
+            'Meta': {'object_name': 'LogMessage'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'lineno': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'pathname': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'task': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Task']", 'null': 'True', 'blank': 'True'})
+        },
+        u'orm.machine': {
+            'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Machine'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'})
+        },
+        u'orm.package': {
+            'Meta': {'object_name': 'Package'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']", 'null': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'installed_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            'installed_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'license': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']", 'null': 'True'}),
+            'revision': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
+            'section': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        u'orm.package_dependency': {
+            'Meta': {'object_name': 'Package_Dependency'},
+            'dep_type': ('django.db.models.fields.IntegerField', [], {}),
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_target'", 'to': u"orm['orm.Package']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_source'", 'to': u"orm['orm.Package']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']", 'null': 'True'})
+        },
+        u'orm.package_file': {
+            'Meta': {'object_name': 'Package_File'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildfilelist_package'", 'to': u"orm['orm.Package']"}),
+            'path': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        },
+        u'orm.project': {
+            'Meta': {'object_name': 'Project'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}),
+            'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
+        },
+        u'orm.projectlayer': {
+            'Meta': {'unique_together': "(('project', 'layercommit'),)", 'object_name': 'ProjectLayer'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layercommit': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}),
+            'optional': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"})
+        },
+        u'orm.projecttarget': {
+            'Meta': {'object_name': 'ProjectTarget'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'})
+        },
+        u'orm.projectvariable': {
+            'Meta': {'object_name': 'ProjectVariable'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}),
+            'value': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        u'orm.recipe': {
+            'Meta': {'unique_together': "(('layer_version', 'file_path', 'pathflags'),)", 'object_name': 'Recipe'},
+            'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'file_path': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}),
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}),
+            'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recipe_layer_version'", 'to': u"orm['orm.Layer_Version']"}),
+            'license': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'pathflags': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+            'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}),
+            'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        u'orm.recipe_dependency': {
+            'Meta': {'object_name': 'Recipe_Dependency'},
+            'dep_type': ('django.db.models.fields.IntegerField', [], {}),
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_depends'", 'to': u"orm['orm.Recipe']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_recipe'", 'to': u"orm['orm.Recipe']"})
+        },
+        u'orm.release': {
+            'Meta': {'object_name': 'Release'},
+            'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}),
+            'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'})
+        },
+        u'orm.releasedefaultlayer': {
+            'Meta': {'object_name': 'ReleaseDefaultLayer'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"})
+        },
+        u'orm.releaselayersourcepriority': {
+            'Meta': {'unique_together': "(('release', 'layer_source'),)", 'object_name': 'ReleaseLayerSourcePriority'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.LayerSource']"}),
+            'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"})
+        },
+        u'orm.target': {
+            'Meta': {'object_name': 'Target'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'license_manifest_path': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}),
+            'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'})
+        },
+        u'orm.target_file': {
+            'Meta': {'object_name': 'Target_File'},
+            'directory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'directory_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}),
+            'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'inodetype': ('django.db.models.fields.IntegerField', [], {}),
+            'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
+            'permission': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+            'size': ('django.db.models.fields.IntegerField', [], {}),
+            'sym_target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'symlink_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.target_image_file': {
+            'Meta': {'object_name': 'Target_Image_File'},
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '254'}),
+            'file_size': ('django.db.models.fields.IntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.target_installed_package': {
+            'Meta': {'object_name': 'Target_Installed_Package'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildtargetlist_package'", 'to': u"orm['orm.Package']"}),
+            'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"})
+        },
+        u'orm.task': {
+            'Meta': {'ordering': "('order', 'recipe')", 'unique_together': "(('build', 'recipe', 'task_name'),)", 'object_name': 'Task'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_build'", 'to': u"orm['orm.Build']"}),
+            'cpu_usage': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}),
+            'disk_io': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'elapsed_time': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'line_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'logfile': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'outcome': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
+            'path_to_sstate_obj': ('django.db.models.fields.FilePathField', [], {'max_length': '500', 'blank': 'True'}),
+            'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'to': u"orm['orm.Recipe']"}),
+            'script_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'source_url': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}),
+            'sstate_checksum': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'sstate_result': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'task_executed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'task_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'work_directory': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'})
+        },
+        u'orm.task_dependency': {
+            'Meta': {'object_name': 'Task_Dependency'},
+            'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_depends'", 'to': u"orm['orm.Task']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'task': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_task'", 'to': u"orm['orm.Task']"})
+        },
+        u'orm.toastersetting': {
+            'Meta': {'object_name': 'ToasterSetting'},
+            'helptext': ('django.db.models.fields.TextField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '63'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'orm.variable': {
+            'Meta': {'object_name': 'Variable'},
+            'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_build'", 'to': u"orm['orm.Build']"}),
+            'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'human_readable_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'variable_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'variable_value': ('django.db.models.fields.TextField', [], {'blank': 'True'})
+        },
+        u'orm.variablehistory': {
+            'Meta': {'object_name': 'VariableHistory'},
+            'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'line_number': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'operation': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'variable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vhistory'", 'to': u"orm['orm.Variable']"})
+        }
+    }
+
+    complete_apps = ['orm']
\ No newline at end of file
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index e4d2e87..3832905 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -191,10 +191,11 @@
 
     # returns a queryset of compatible layers for a project
     def compatible_layerversions(self, release = None, layer_name = None):
+        logger.warning("This function is deprecated")
         if release == None:
             release = self.release
         # layers on the same branch or layers specifically set for this project
-        queryset = Layer_Version.objects.filter((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self) | Q(build__project = self))
+        queryset = Layer_Version.objects.filter(((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self)) & Q(build__isnull=True))
 
         if layer_name is not None:
             # we select only a layer name
@@ -205,45 +206,55 @@
 
         return queryset
 
-    def projectlayer_equivalent_set(self):
-        return self.compatible_layerversions().filter(layer__name__in = [x.layercommit.layer.name for x in self.projectlayer_set.all()]).select_related("up_branch")
+    def get_all_compatible_layer_versions(self):
+        """ Returns Queryset of all Layer_Versions which are compatible with
+        this project"""
+        queryset = Layer_Version.objects.filter(
+            (Q(up_branch__name=self.release.branch_name) & Q(build=None))
+            | Q(project=self))
+
+        return queryset
+
+    def get_project_layer_versions(self, pk=False):
+        """ Returns the Layer_Versions currently added to this project """
+        layer_versions = self.projectlayer_set.all().values('layercommit')
+
+        if pk is False:
+            return layer_versions
+        else:
+            return layer_versions.values_list('layercommit__pk', flat=True)
+
 
     def get_available_machines(self):
         """ Returns QuerySet of all Machines which are provided by the
         Layers currently added to the Project """
-        queryset = Machine.objects.filter(layer_version__in=self.projectlayer_equivalent_set)
+        queryset = Machine.objects.filter(
+            layer_version__in=self.get_project_layer_versions())
+
         return queryset
 
     def get_all_compatible_machines(self):
         """ Returns QuerySet of all the compatible machines available to the
         project including ones from Layers not currently added """
-        compatible_layers = self.compatible_layerversions()
+        queryset = Machine.objects.filter(
+            layer_version__in=self.get_all_compatible_layer_versions())
 
-        queryset = Machine.objects.filter(layer_version__in=compatible_layers)
         return queryset
 
     def get_available_recipes(self):
-        """ Returns QuerySet of all Recipes which are provided by the Layers
-        currently added to the Project """
-        project_layers = self.projectlayer_equivalent_set()
-        queryset = Recipe.objects.filter(layer_version__in = project_layers)
-
-        # Copied from get_all_compatible_recipes
-        search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id')))
-        queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source')
-        # End copy
+        """ Returns QuerySet of all the recipes that are provided by layers
+        added to this project """
+        queryset = Recipe.objects.filter(
+            layer_version__in=self.get_project_layer_versions())
 
         return queryset
 
     def get_all_compatible_recipes(self):
         """ Returns QuerySet of all the compatible Recipes available to the
         project including ones from Layers not currently added """
-        compatible_layerversions = self.compatible_layerversions()
-        queryset = Recipe.objects.filter(layer_version__in = compatible_layerversions)
+        queryset = Recipe.objects.filter(
+            layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
 
-        search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id')))
-
-        queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source')
         return queryset
 
 
@@ -260,7 +271,7 @@
             for l in self.projectlayer_set.all().order_by("pk"):
                 commit = l.layercommit.get_vcs_reference()
                 print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit)
-                BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath)
+                BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath, layer_version=l.layercommit)
 
             br.state = BuildRequest.REQ_QUEUED
             now = timezone.now()
@@ -270,7 +281,7 @@
                                 )
             for t in self.projecttarget_set.all():
                 BRTarget.objects.create(req = br, target = t.target, task = t.task)
-                Target.objects.create(build = br.build, target = t.target)
+                Target.objects.create(build = br.build, target = t.target, task = t.task)
 
             for v in self.projectvariable_set.all():
                 BRVariable.objects.create(req = br, name = v.name, value = v.value)
@@ -333,13 +344,14 @@
         tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
         return( tgts );
 
-    @property
-    def toaster_exceptions(self):
-        return self.logmessage_set.filter(level=LogMessage.EXCEPTION)
+    def get_outcome_text(self):
+        return Build.BUILD_OUTCOME[int(self.outcome)][1]
 
     @property
     def errors(self):
-        return (self.logmessage_set.filter(level=LogMessage.ERROR)|self.logmessage_set.filter(level=LogMessage.EXCEPTION))
+        return (self.logmessage_set.filter(level=LogMessage.ERROR) |
+                self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
+                self.logmessage_set.filter(level=LogMessage.CRITICAL))
 
     @property
     def warnings(self):
@@ -350,10 +362,23 @@
         return (self.completed_on - self.started_on).total_seconds()
 
     def get_current_status(self):
+        """
+        get the status string from the build request if the build
+        has one, or the text for the build outcome if it doesn't
+        """
+
         from bldcontrol.models import BuildRequest
-        if self.outcome == Build.IN_PROGRESS and self.buildrequest.state != BuildRequest.REQ_INPROGRESS:
+
+        build_request = None
+        if hasattr(self, 'buildrequest'):
+            build_request = self.buildrequest
+
+        if (build_request
+                and build_request.state != BuildRequest.REQ_INPROGRESS
+                and self.outcome == Build.IN_PROGRESS):
             return self.buildrequest.get_state_display()
-        return self.get_outcome_display()
+        else:
+            return self.get_outcome_text()
 
     def __str__(self):
         return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
@@ -551,7 +576,7 @@
 
 class Package(models.Model):
     search_allowed_fields = ['name', 'version', 'revision', 'recipe__name', 'recipe__version', 'recipe__license', 'recipe__layer_version__layer__name', 'recipe__layer_version__branch', 'recipe__layer_version__commit', 'recipe__layer_version__local_path', 'installed_name']
-    build = models.ForeignKey('Build')
+    build = models.ForeignKey('Build', null=True)
     recipe = models.ForeignKey('Recipe', null=True)
     name = models.CharField(max_length=100)
     installed_name = models.CharField(max_length=100, default='')
@@ -828,6 +853,7 @@
         import urllib2, urlparse, json
         import os
         proxy_settings = os.environ.get("http_proxy", None)
+        oe_core_layer = 'openembedded-core'
 
         def _get_json_response(apiurl = self.apiurl):
             _parsedurl = urlparse.urlparse(apiurl)
@@ -872,6 +898,25 @@
         if not connection.features.autocommits_when_autocommit_is_off:
             transaction.set_autocommit(False)
         for li in layers_info:
+            # Special case for the openembedded-core layer
+            if li['name'] == oe_core_layer:
+                try:
+                    # If we have an existing openembedded-core for example
+                    # from the toasterconf.json augment the info using the
+                    # layerindex rather than duplicate it
+                    oe_core_l =  Layer.objects.get(name=oe_core_layer)
+                    # Take ownership of the layer as now coming from the
+                    # layerindex
+                    oe_core_l.layer_source = self
+                    oe_core_l.up_id = li['id']
+                    oe_core_l.summary = li['summary']
+                    oe_core_l.description = li['description']
+                    oe_core_l.save()
+                    continue
+
+                except Layer.DoesNotExist:
+                    pass
+
             l, created = Layer.objects.get_or_create(layer_source = self, name = li['name'])
             l.up_id = li['id']
             l.up_date = li['updated']
@@ -882,6 +927,7 @@
             l.summary = li['summary']
             l.description = li['description']
             l.save()
+
         if not connection.features.autocommits_when_autocommit_is_off:
             transaction.set_autocommit(True)
 
@@ -974,9 +1020,12 @@
                 ro.file_path = ri['filepath'] + "/" + ri['filename']
                 if 'inherits' in ri:
                     ro.is_image = 'image' in ri['inherits'].split()
+                else: # workaround for old style layer index
+                    ro.is_image = "-image-" in ri['pn']
                 ro.save()
             except IntegrityError as e:
                 logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename']))
+                ro.delete()
         if not connection.features.autocommits_when_autocommit_is_off:
             transaction.set_autocommit(True)
 
@@ -1132,17 +1181,36 @@
         return project.compatible_layerversions(layer_name = self.layer.name)
 
     def get_vcs_reference(self):
-        if self.commit is not None and len(self.commit) > 0:
-            return self.commit
         if self.branch is not None and len(self.branch) > 0:
             return self.branch
         if self.up_branch is not None:
             return self.up_branch.name
+        if self.commit is not None and len(self.commit) > 0:
+            return self.commit
         return ("Cannot determine the vcs_reference for layer version %s" % vars(self))
 
     def get_detailspage_url(self, project_id):
         return reverse('layerdetails', args=(project_id, self.pk))
 
+    def get_alldeps(self, project_id):
+        """Get full list of unique layer dependencies."""
+        def gen_layerdeps(lver, project):
+            for ldep in lver.dependencies.all():
+                yield ldep.depends_on
+                # get next level of deps recursively calling gen_layerdeps
+                for subdep in gen_layerdeps(ldep.depends_on, project):
+                    yield subdep
+
+        project = Project.objects.get(pk=project_id)
+        result = []
+        projectlvers = [player.layercommit for player in project.projectlayer_set.all()]
+        for dep in gen_layerdeps(self, project):
+            # filter out duplicates and layers already belonging to the project
+            if dep not in result + projectlvers:
+                result.append(dep)
+
+        return sorted(result, key=lambda x: x.layer.name)
+
     def __unicode__(self):
         return "%d %s (VCS %s, Project %s)" % (self.pk, str(self.layer), self.get_vcs_reference(), self.build.project if self.build is not None else "No project")
 
@@ -1170,6 +1238,15 @@
     class Meta:
         unique_together = (("project", "layercommit"),)
 
+class CustomImageRecipe(models.Model):
+    name = models.CharField(max_length=100)
+    base_recipe = models.ForeignKey(Recipe)
+    packages = models.ManyToManyField(Package)
+    project = models.ForeignKey(Project)
+
+    class Meta:
+        unique_together = ("name", "project")
+
 class ProjectVariable(models.Model):
     project = models.ForeignKey(Project)
     name = models.CharField(max_length=100)
@@ -1206,16 +1283,20 @@
     INFO = 0
     WARNING = 1
     ERROR = 2
+    CRITICAL = 3
 
-    LOG_LEVEL = ( (INFO, "info"),
-            (WARNING, "warn"),
-            (ERROR, "error"),
-            (EXCEPTION, "toaster exception"))
+    LOG_LEVEL = (
+        (INFO, "info"),
+        (WARNING, "warn"),
+        (ERROR, "error"),
+        (CRITICAL, "critical"),
+        (EXCEPTION, "toaster exception")
+    )
 
     build = models.ForeignKey(Build)
     task  = models.ForeignKey(Task, blank = True, null=True)
     level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
-    message=models.CharField(max_length=240)
+    message = models.TextField(blank=True, null=True)
     pathname = models.FilePathField(max_length=255, blank=True)
     lineno = models.IntegerField(null=True)
 
diff --git a/bitbake/lib/toaster/orm/tests.py b/bitbake/lib/toaster/orm/tests.py
index 783aea8..719266e 100644
--- a/bitbake/lib/toaster/orm/tests.py
+++ b/bitbake/lib/toaster/orm/tests.py
@@ -23,12 +23,11 @@
 
 from django.test import TestCase, TransactionTestCase
 from orm.models import LocalLayerSource, LayerIndexLayerSource, ImportedLayerSource, LayerSource
-from orm.models import Branch
+from orm.models import Branch, LayerVersionDependency
 
-from orm.models import Project, Build, Layer, Layer_Version, Branch, ProjectLayer
+from orm.models import Project, Layer, Layer_Version, Branch, ProjectLayer
 from orm.models import Release, ReleaseLayerSourcePriority, BitbakeVersion
 
-from django.utils import timezone
 from django.db import IntegrityError
 
 import os
@@ -153,35 +152,29 @@
         equivqs = self.lver.get_equivalents_wpriority(self.project)
         self.assertEqual(list(equivqs), [lver2, self.lver])
 
-    def test_build_layerversion(self):
+    def test_compatible_layer_versions(self):
         """
-        Any layer version coming from the build should show up
-        before any layer version coming from upstream
-        """
-        build = Build.objects.create(project=self.project,
-                                     started_on=timezone.now(),
-                                     completed_on=timezone.now())
-        lvb = Layer_Version.objects.create(layer=self.layer, build=build,
-                                           commit="deadbeef")
-
-        # a build layerversion must be in the equivalence
-        # list for the original layerversion
-        equivqs = self.lver.get_equivalents_wpriority(self.project)
-        self.assertTrue(len(equivqs) == 2)
-        self.assertTrue(equivqs[0] == self.lver)
-        self.assertTrue(equivqs[1] == lvb)
-
-        # getting the build layerversion equivalent list must
-        # return the same list as the original layer
-        bequivqs = lvb.get_equivalents_wpriority(self.project)
-
-        self.assertEqual(list(equivqs), list(bequivqs))
-
-    def test_compatible_layerversions(self):
-        """
-        When we have a 2 layer versions, compatible_layerversions()
+        When we have a 2 layer versions, get_all_compatible_layerversions()
         should return a queryset with both.
         """
-        compat_lv = self.project.compatible_layerversions()
+        compat_lv = self.project.get_all_compatible_layer_versions()
         self.assertEqual(list(compat_lv), [self.lver, self.lver2])
 
+    def test_layerversion_get_alldeps(self):
+        """Test Layer_Version.get_alldeps API."""
+        lvers = {}
+        for i in range(10):
+            name = "layer%d" % i
+            lvers[name] = Layer_Version.objects.create(layer=Layer.objects.create(name=name),
+                                                       project=self.project)
+            if i:
+                LayerVersionDependency.objects.create(layer_version=lvers["layer%d" % (i - 1)],
+                                                      depends_on=lvers[name])
+                # Check dinamically added deps
+                self.assertEqual(lvers['layer0'].get_alldeps(self.project.id),
+                                 [lvers['layer%d' % n] for n in range(1, i+1)])
+
+        # Check chain of deps created in previous loop
+        for i in range(10):
+            self.assertEqual(lvers['layer%d' % i].get_alldeps(self.project.id),
+                             [lvers['layer%d' % n] for n in range(i+1, 10)])
diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css b/bitbake/lib/toaster/toastergui/static/css/default.css
index cce3e31..bc8a97b 100644
--- a/bitbake/lib/toaster/toastergui/static/css/default.css
+++ b/bitbake/lib/toaster/toastergui/static/css/default.css
@@ -15,6 +15,8 @@
 /* Styles for the help information */
 .get-help { color: #CCCCCC; }
 .get-help:hover, .icon-plus-sign:hover { color: #999999; cursor: pointer; }
+.get-help-green { color: #468847; }
+.get-help-green:hover { color: #347132; cursor: pointer; }
 .get-help-blue { color: #3A87AD; }
 .get-help-blue:hover { color: #005580; cursor: pointer; }
 .get-help-yellow { color: #C09853; }
@@ -161,9 +163,16 @@
 .project-name .label { font-weight: normal; margin-bottom: 5px; margin-left: -15px; padding: 5px; }
 .project-name .label > a { color: #fff; font-weight: normal; }
 
+/* styles for showing help icons next to command-line builds */
+.build-result .get-help-green, .build-result .get-help-red, .build-result .get-help-blue { margin-right: 35px; margin-top: 8px; font-size: 16px; }
+
 /* Remove bottom margin for forms inside modal dialogs */
 #dependencies-modal-form { margin-bottom: 0px; }
 
+/* Custom column widths */
+.narrow-col { width: 8%; }
+.medium-col { width: 12%; }
+
 /* Configuration styles */
 .icon-trash { color: #B94A48; font-size: 16px; padding-left: 5px; }
 .icon-trash:hover { color: #943A38; text-decoration: none; cursor: pointer; }
diff --git a/bitbake/lib/toaster/toastergui/static/js/base.js b/bitbake/lib/toaster/toastergui/static/js/base.js
index 895e61b..ed22a4e 100644
--- a/bitbake/lib/toaster/toastergui/static/js/base.js
+++ b/bitbake/lib/toaster/toastergui/static/js/base.js
@@ -6,6 +6,7 @@
   var newBuildTargetInput;
   var newBuildTargetBuildBtn;
   var projectNameForm = $("#project-name-change-form");
+  var projectNameContainer = $("#project-name-container");
   var projectName = $("#project-name");
   var projectNameFormToggle = $("#project-change-form-toggle");
   var projectNameChangeCancel = $("#project-name-change-cancel");
@@ -23,24 +24,21 @@
   /* Project name change functionality */
   projectNameFormToggle.click(function(e){
     e.preventDefault();
-
-    $(this).add(projectName).hide();
+    projectNameContainer.hide();
     projectNameForm.fadeIn();
   });
 
   projectNameChangeCancel.click(function(e){
     e.preventDefault();
-
     projectNameForm.hide();
-    projectName.add(projectNameFormToggle).fadeIn();
+    projectNameContainer.fadeIn();
   });
 
   $("#project-name-change-btn").click(function(e){
     var newProjectName = $("#project-name-change-input").val();
 
-    libtoaster.editCurrentProject({ projectName: newProjectName },function (){
-
-      projectName.text(newProjectName);
+    libtoaster.editCurrentProject({ projectName: newProjectName }, function (){
+      projectName.html(newProjectName);
       libtoaster.ctx.projectName = newProjectName;
       projectNameChangeCancel.click();
     });
@@ -123,14 +121,14 @@
   });
 
   function _checkProjectBuildable() {
-    if (selectedProject.projectId === undefined) {
+    if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) {
       return;
     }
 
     libtoaster.getProjectInfo(selectedProject.projectPageUrl,
       function (data) {
         if (data.machine === null || data.machine.name === undefined || data.layers.length === 0) {
-          /* we can't build anything with out a machine and some layers */
+          /* we can't build anything without a machine and some layers */
           $("#new-build-button #targets-form").hide();
           $("#new-build-button .alert").show();
         } else {
@@ -149,7 +147,7 @@
     /* If we don't have a current project then present the set project
      * form.
      */
-    if (selectedProject.projectId === undefined) {
+    if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) {
       $('#change-project-form').show();
       $('#project .icon-pencil').hide();
     }
diff --git a/bitbake/lib/toaster/toastergui/static/js/customrecipe.js b/bitbake/lib/toaster/toastergui/static/js/customrecipe.js
new file mode 100644
index 0000000..4f6b304
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/static/js/customrecipe.js
@@ -0,0 +1,50 @@
+"use strict";
+
+function customRecipePageInit(ctx) {
+
+  var urlParams = libtoaster.parseUrlParams();
+
+  (function notificationRequest(){
+    if (urlParams.hasOwnProperty('notify') && urlParams.notify === 'new'){
+      $("#image-created-notification").show();
+    }
+  })();
+
+  $("#recipeselection").on('table-done', function(e, total, tableParams){
+    /* Table is done so now setup the click handler for the package buttons */
+    $(".add-rm-package-btn").click(function(e){
+      e.preventDefault();
+      addRemovePackage($(this), tableParams);
+    });
+  });
+
+  function addRemovePackage(pkgBtn, tableParams){
+    var pkgBtnData = pkgBtn.data();
+    var method;
+    var buttonToShow;
+
+    if (pkgBtnData.directive == 'add') {
+      method = 'PUT';
+      buttonToShow = '#package-rm-btn-' + pkgBtnData.package;
+    } else if (pkgBtnData.directive == 'remove') {
+      method = 'DELETE';
+      buttonToShow = '#package-add-btn-' + pkgBtnData.package;
+    } else {
+      throw("Unknown package directive: should be add or remove");
+    }
+
+    $.ajax({
+        type: method,
+        url: pkgBtnData.packageUrl,
+        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+        success: function(data){
+          /* Invalidate the Add | Rm package table's current cache */
+          tableParams.nocache = true;
+          $.get(ctx.tableApiUrl, tableParams);
+          /* Swap the buttons around */
+          pkgBtn.hide();
+          $(buttonToShow).show();
+        }
+    });
+  }
+}
diff --git a/bitbake/lib/toaster/toastergui/static/js/importlayer.js b/bitbake/lib/toaster/toastergui/static/js/importlayer.js
index 2fadbc0..c68f366 100644
--- a/bitbake/lib/toaster/toastergui/static/js/importlayer.js
+++ b/bitbake/lib/toaster/toastergui/static/js/importlayer.js
@@ -195,8 +195,8 @@
     var dupLayerInfo = $("#duplicate-layer-info");
     dupLayerInfo.find(".dup-layer-name").text(layer.name);
     dupLayerInfo.find(".dup-layer-link").attr("href", layer.layerdetailurl);
-    dupLayerInfo.find("#dup-layer-vcs-url").text(layer.layer__vcs_url);
-    dupLayerInfo.find("#dup-layer-revision").text(layer.revision.commit);
+    dupLayerInfo.find("#dup-layer-vcs-url").text(layer.vcs_url);
+    dupLayerInfo.find("#dup-layer-revision").text(layer.vcs_reference);
 
     $(".fields-apart-from-layer-name").fadeOut(function(){
 
@@ -214,11 +214,10 @@
       $.getJSON(libtoaster.ctx.layersTypeAheadUrl,
         { include_added: "true" , search: name, format: "json" },
         function(layer) {
-          if (layer.rows.length > 0) {
-            for (var i in layer.rows){
-              if (layer.rows[i].name == name) {
-                console.log(layer.rows[i])
-                layerExistsError(layer.rows[i]);
+          if (layer.results.length > 0) {
+            for (var i in layer.results){
+              if (layer.results[i].name == name) {
+                layerExistsError(layer.results[i]);
               }
             }
           }
diff --git a/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js b/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js
index 42e7427..794b902 100644
--- a/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js
+++ b/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js
@@ -421,7 +421,7 @@
         columnElType: "td", // i.e. 'td', 'th' or 'td,th'
         expandable: false,
         expanderTemplate: "<a href='#'>&nbsp;</a>",
-        indent: 19,
+        indent: 10,
         indenterTemplate: "<span class='indenter'></span>",
         initialState: "collapsed",
         nodeIdAttr: "ttId", // maps to data-tt-id
diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
index a0509f9..7318b3f 100644
--- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
+++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
@@ -1,6 +1,6 @@
 "use strict";
 
-function layerBtnsInit(ctx) {
+function layerBtnsInit() {
 
   /* Remove any current bindings to avoid duplicated binds */
   $(".layerbtn").unbind('click');
@@ -68,10 +68,16 @@
       });
   });
 
-  /* Setup the initial state of the buttons */
 
-  for (var i in ctx.projectLayers){
-      $(".layer-exists-" + ctx.projectLayers[i]).show();
-      $(".layer-add-" + ctx.projectLayers[i]).hide();
-  }
+  $(".customise-btn").unbind('click');
+  $(".customise-btn").click(function(e){
+    e.preventDefault();
+    var imgCustomModal = $("#new-custom-image-modal");
+
+    if (imgCustomModal.length == 0)
+      throw("Modal new-custom-image not found");
+
+    imgCustomModal.data('recipe', $(this).data('recipe'));
+    imgCustomModal.modal('show');
+  });
 }
diff --git a/bitbake/lib/toaster/toastergui/static/js/layerdetails.js b/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
index 000e803..8c2ec4c 100644
--- a/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
+++ b/bitbake/lib/toaster/toastergui/static/js/layerdetails.js
@@ -7,6 +7,9 @@
   var layerDepsList = $("#layer-deps-list");
   var currentLayerDepSelection;
   var addRmLayerBtn = $("#add-remove-layer-btn");
+  var targetTab = $("#targets-tab");
+  var machineTab = $("#machines-tab");
+  var detailsTab = $("#details-tab");
 
   /* setup the dependencies typeahead */
   libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" }, function(item){
@@ -15,6 +18,21 @@
     layerDepBtn.removeAttr("disabled");
   });
 
+  $(window).on('hashchange', function(e){
+    switch(window.location.hash){
+      case '#machines':
+        machineTab.tab('show');
+        break;
+      case '#recipes':
+        targetTab.tab('show');
+        break;
+      default:
+        detailsTab.tab('show');
+        break;
+    }
+  });
+
+
   $(".breadcrumb li:first a").click(function(e){
     e.preventDefault();
     /* By default this link goes to the project configuration page. However
@@ -143,7 +161,7 @@
       addRmLayerBtn.removeClass("btn-danger");
   }
 
-  $("#details-tab").on('show', function(){
+  detailsTab.on('show', function(){
     if (!ctx.layerVersion.inCurrentPrj)
       defaultAddBtnText();
 
@@ -174,7 +192,7 @@
       $("#no-recipes-yet").hide();
     }
 
-    $("#targets-tab").removeClass("muted");
+    targetTab.removeClass("muted");
     if (window.location.hash === "#recipes"){
       /* re run the machinesTabShow to update the text */
       targetsTabShow();
@@ -189,7 +207,7 @@
     else
       $("#no-machines-yet").hide();
 
-    $("#machines-tab").removeClass("muted");
+    machineTab.removeClass("muted");
     if (window.location.hash === "#machines"){
       /* re run the machinesTabShow to update the text */
       machinesTabShow();
@@ -202,7 +220,7 @@
 
   });
 
-  $("#targets-tab").on('show', targetsTabShow);
+  targetTab.on('show', targetsTabShow);
 
   function machinesTabShow(){
     if (!ctx.layerVersion.inCurrentPrj) {
@@ -219,7 +237,7 @@
     window.location.hash = "machines";
   }
 
-  $("#machines-tab").on('show', machinesTabShow);
+  machineTab.on('show', machinesTabShow);
 
   $(".pagesize").change(function(){
     var search = libtoaster.parseUrlParams();
@@ -236,7 +254,7 @@
 
     if (added){
       /* enable and switch all the button states */
-      $(".build-target-btn").removeAttr("disabled");
+      $(".build-recipe-btn").removeAttr("disabled");
       $(".select-machine-btn").removeAttr("disabled");
       addRmLayerBtn.addClass("btn-danger");
       addRmLayerBtn.data('directive', "remove");
@@ -245,7 +263,7 @@
 
     } else {
       /* disable and switch all the button states */
-      $(".build-target-btn").attr("disabled","disabled");
+      $(".build-recipe-btn").attr("disabled","disabled");
       $(".select-machine-btn").attr("disabled", "disabled");
       addRmLayerBtn.removeClass("btn-danger");
       addRmLayerBtn.data('directive', "add");
diff --git a/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js b/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js
new file mode 100644
index 0000000..935b21e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js
@@ -0,0 +1,49 @@
+"use strict";
+
+function newCustomImagePageInit(ctx){
+
+  var newCustomImgBtn = $("#create-new-custom-image-btn");
+  var imgCustomModal = $("#new-custom-image-modal");
+
+  newCustomImgBtn.click(function(e){
+    e.preventDefault();
+
+    var name = imgCustomModal.find('input').val();
+    var baseRecipeId = imgCustomModal.data('recipe');
+
+    if (name.length > 0) {
+      createCustomRecipe(name, baseRecipeId);
+      imgCustomModal.modal('hide');
+    } else {
+      console.warn("TODO No name supplied");
+    }
+  });
+
+  function createCustomRecipe(name, baseRecipeId){
+    var data = {
+      'name' : name,
+      'project' : libtoaster.ctx.projectId,
+      'base' : baseRecipeId,
+    };
+
+    $.ajax({
+        type: "POST",
+        url: ctx.xhrCustomRecipeUrl,
+        data: data,
+        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
+        success: function (ret) {
+          if (ret.error !== "ok") {
+            console.warn(ret.error);
+          } else {
+            window.location.replace(ret.url + '?notify=new');
+          }
+        },
+        error: function (ret) {
+          console.warn("Call failed");
+          console.warn(ret);
+        }
+    });
+  }
+
+
+}
diff --git a/bitbake/lib/toaster/toastergui/static/js/projectpage.js b/bitbake/lib/toaster/toastergui/static/js/projectpage.js
index d367047..e742ef2 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projectpage.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projectpage.js
@@ -23,7 +23,7 @@
   var cancelReleaseChange = $("#cancel-release-change");
 
   var currentLayerAddSelection;
-  var currentMachineAddSelection = {};
+  var currentMachineAddSelection = "";
 
   var urlParams = libtoaster.parseUrlParams();
 
@@ -38,7 +38,7 @@
      */
     if (urlParams.hasOwnProperty('setMachine') &&
         urlParams.setMachine !== prjInfo.machine.name){
-        currentMachineAddSelection.name = urlParams.setMachine;
+        machineChangeInput.val(urlParams.setMachine);
         machineChangeBtn.click();
     } else {
       updateMachineName(prjInfo.machine.name);
@@ -103,6 +103,12 @@
     layerAddBtn.removeAttr("disabled");
   });
 
+  layerAddInput.keyup(function() {
+    if ($(this).val().length == 0) {
+      layerAddBtn.attr("disabled", "disabled")
+    }
+  });
+
   layerAddBtn.click(function(e){
     e.preventDefault();
     var layerObj = currentLayerAddSelection;
@@ -146,10 +152,7 @@
 
       link.attr("href", layerObj.layerdetailurl);
       link.text(layerObj.name);
-      /* YOCTO #8024
-        link.tooltip({title: layerObj.giturl + " | "+ layerObj.branch.name, placement: "right"});
-        branch name not accessible sometimes it is revision instead
-      */
+      link.tooltip({title: layerObj.vcs_url + " | "+ layerObj.vcs_reference, placement: "right"});
 
       var trashItem = projectLayer.children("span");
       trashItem.click(function (e) {
@@ -251,29 +254,33 @@
   }
 
   libtoaster.makeTypeahead(machineChangeInput, libtoaster.ctx.machinesTypeAheadUrl, { }, function(item){
-    currentMachineAddSelection = item;
+    currentMachineAddSelection = item.name;
     machineChangeBtn.removeAttr("disabled");
   });
 
   machineChangeBtn.click(function(e){
     e.preventDefault();
-    if (currentMachineAddSelection.name === undefined)
+    /* We accept any value regardless of typeahead selection or not */
+    if (machineChangeInput.val().length === 0)
       return;
 
-    libtoaster.editCurrentProject({ machineName : currentMachineAddSelection.name },
+    currentMachineAddSelection = machineChangeInput.val();
+
+    libtoaster.editCurrentProject(
+      { machineName : currentMachineAddSelection },
       function(){
         /* Success machine changed */
-        updateMachineName(currentMachineAddSelection.name);
+        updateMachineName(currentMachineAddSelection);
         machineChangeCancel.click();
 
         /* Show the alert message */
         var message = $('<span class="lead">You have changed the machine to: <strong><span id="notify-machine-name"></span></strong></span>');
-        message.find("#notify-machine-name").text(currentMachineAddSelection.name);
+        message.find("#notify-machine-name").text(currentMachineAddSelection);
         libtoaster.showChangeNotification(message);
     },
       function(){
         /* Failed machine changed */
-        console.log("failed to change machine");
+        console.warn("Failed to change machine");
     });
   });
 
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index f18034d..40b5022 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -33,14 +33,6 @@
 
   loadData(tableParams);
 
-  window.onpopstate = function(event){
-    if (event.state){
-      tableParams = event.state.tableParams;
-      /* We skip loadData and just update the table */
-      updateTable(event.state.tableData);
-    }
-  };
-
   function loadData(tableParams){
     $.ajax({
         type: "GET",
@@ -49,10 +41,8 @@
         headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
         success: function(tableData) {
           updateTable(tableData);
-          window.history.pushState({
-              tableData: tableData,
-              tableParams: tableParams
-          }, null, libtoaster.dumpsUrlParams(tableParams));
+          window.history.replaceState(null, null,
+            libtoaster.dumpsUrlParams(tableParams));
         }
     });
   }
@@ -140,7 +130,7 @@
       tableBody.append(row);
 
       /* If we have layerbtns then initialise them */
-      layerBtnsInit(ctx);
+      layerBtnsInit();
 
       /* If we have popovers initialise them now */
       $('td > a.btn').popover({
diff --git a/bitbake/lib/toaster/toastergui/static/js/tests/test.js b/bitbake/lib/toaster/toastergui/static/js/tests/test.js
index d610113..aac0ba6 100644
--- a/bitbake/lib/toaster/toastergui/static/js/tests/test.js
+++ b/bitbake/lib/toaster/toastergui/static/js/tests/test.js
@@ -13,8 +13,6 @@
     "name":"meta-example"
   };
 
-  var correctResponse = "You have added <strong>3</strong> layers to your project: <a id=\"layer-affected-name\" href=\"/toastergui/project/1/layer/22\">meta-example</a> and its dependencies <a href=\"/toastergui/project/1/layer/9\" data-original-title=\"\" title=\"\">meta-example-two</a>, <a href=\"/toastergui/project/1/layer/9\" data-original-title=\"\" title=\"\">meta-example-three</a>";
-
   var layerDepsList = [
     {
     "layerdetailurl":"/toastergui/project/1/layer/9",
@@ -68,9 +66,9 @@
 });
 
 var layer = {
-  "id": 91,
-  "name":  "meta-crystalforest",
-  "layerdetailurl": "/toastergui/project/4/layer/91"
+  "id": 1,
+  "name":  "meta-testing",
+  "layerdetailurl": "/toastergui/project/1/layer/1"
 };
 
 QUnit.test("Add layer", function(assert){
@@ -84,11 +82,19 @@
     }
   }, 200);
 
-  libtoaster.addRmLayer(layer, true, function(deps){
-    assert.equal(deps.length, 1);
-    done();
-  });
+  /* Compare the number of layers before and after the add in the project */
+  libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){
+    var origNumLayers = prjInfo.layers.length;
 
+    libtoaster.addRmLayer(layer, true, function(deps){
+      libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl,
+        function(prjInfo){
+        assert.ok(prjInfo.layers.length > origNumLayers,
+          "Layer not added to project");
+        done();
+      });
+    });
+  });
 });
 
 QUnit.test("Rm layer", function(assert){
@@ -152,11 +158,11 @@
 });
 
 QUnit.test("Layer btns init", function(assert){
-  assert.throws(layerBtnsInit({ projectLayers : [] }));
+  assert.throws(layerBtnsInit());
 });
 
 QUnit.test("Table init", function(assert){
-  assert.throws(tableInit({ url : tableUrl }));
+  assert.throws(tableInit({ url : ctx.tableUrl }));
 });
 
 $(document).ajaxError(function(event, jqxhr, settings, errMsg){
@@ -167,9 +173,3 @@
     assert.notOk(jqxhr.responseText);
   });
 });
-
-
-
-
-
-
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 92e3b5c..9c9cda4 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -21,6 +21,7 @@
 
 from toastergui.widgets import ToasterTable
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
+from orm.models import CustomImageRecipe, Package
 from django.db.models import Q, Max
 from django.conf.urls import url
 from django.core.urlresolvers import reverse
@@ -50,14 +51,13 @@
     def __init__(self, *args, **kwargs):
         super(LayersTable, self).__init__(*args, **kwargs)
         self.default_orderby = "layer__name"
+        self.title = "Compatible layers"
 
     def get_context_data(self, **kwargs):
         context = super(LayersTable, self).get_context_data(**kwargs)
 
         project = Project.objects.get(pk=kwargs['pid'])
-
         context['project'] = project
-        context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project))
 
         return context
 
@@ -91,7 +91,10 @@
 
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
-        compatible_layers = prj.compatible_layerversions()
+        compatible_layers = prj.get_all_compatible_layer_versions()
+
+        self.static_context_extra['current_layers'] = \
+                prj.get_project_layer_versions(pk=True)
 
         self.queryset = compatible_layers.order_by(self.default_orderby)
 
@@ -208,6 +211,7 @@
     def __init__(self, *args, **kwargs):
         super(MachinesTable, self).__init__(*args, **kwargs)
         self.empty_state = "No machines maybe you need to do a build?"
+        self.title = "Compatible machines"
         self.default_orderby = "name"
 
     def get_context_data(self, **kwargs):
@@ -218,7 +222,7 @@
 
     def setup_filters(self, *args, **kwargs):
         project = Project.objects.get(pk=kwargs['pid'])
-        self.project_layers = project.projectlayer_equivalent_set()
+        self.project_layers = project.get_project_layer_versions()
 
         self.add_filter(title="Filter by project machines",
                         name="in_current_project",
@@ -308,13 +312,20 @@
 
 
 class RecipesTable(ToasterTable, ProjectFiltersMixin):
-    """Table of Recipes in Toaster"""
+    """Table of All Recipes in Toaster"""
 
     def __init__(self, *args, **kwargs):
         super(RecipesTable, self).__init__(*args, **kwargs)
         self.empty_state = "Toaster has no recipe information. To generate recipe information you can configure a layer source then run a build."
         self.default_orderby = "name"
 
+    build_col = { 'title' : "Build",
+            'help_text' : "Add or delete recipes to and from your project",
+            'hideable' : False,
+            'filter_name' : "in_current_project",
+            'static_data_name' : "add-del-layers",
+            'static_data_template' : '{% include "recipe_btn.html" %}'}
+
     def get_context_data(self, **kwargs):
         project = Project.objects.get(pk=kwargs['pid'])
         context = super(RecipesTable, self).get_context_data(**kwargs)
@@ -326,9 +337,6 @@
         return context
 
     def setup_filters(self, *args, **kwargs):
-        project = Project.objects.get(pk=kwargs['pid'])
-        self.project_layers = project.projectlayer_equivalent_set()
-
         self.add_filter(title="Filter by project recipes",
                         name="in_current_project",
                         filter_actions=[
@@ -336,24 +344,23 @@
                             self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project)
                         ])
 
-
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
 
+        # Project layers used by the filters
+        self.project_layers = prj.get_project_layer_versions(pk=True)
+
+        # Project layers used to switch the button states
+        self.static_context_extra['current_layers'] = self.project_layers
+
         self.queryset = prj.get_all_compatible_recipes()
         self.queryset = self.queryset.order_by(self.default_orderby)
 
 
     def setup_columns(self, *args, **kwargs):
 
-        self.add_column(title="Recipe",
-                        help_text="Information about a single piece of software, including where to download the source, configuration options, how to compile the source files and how to package the compiled output",
-                        hideable=False,
-                        orderable=True,
-                        field_name="name")
-
-        self.add_column(title="Recipe Version",
-                        hidden=True,
+        self.add_column(title="Version",
+                        hidden=False,
                         field_name="version")
 
         self.add_column(title="Description",
@@ -374,6 +381,7 @@
 
         self.add_column(title="Section",
                         help_text="The section in which recipes should be categorized",
+                        hidden=True,
                         orderable=True,
                         field_name="section")
 
@@ -390,24 +398,14 @@
 
         self.add_column(title="License",
                         help_text="The list of source licenses for the recipe. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source",
+                        hidden=True,
                         orderable=True,
                         field_name="license")
 
         self.add_column(title="Revision",
+                        hidden=True,
                         field_name="layer_version__get_vcs_reference")
 
-        self.add_column(title="Build",
-                        help_text="Add or delete recipes to and from your project",
-                        hideable=False,
-                        filter_name="in_current_project",
-                        static_data_name="add-del-layers",
-                        static_data_template='{% include "recipe_btn.html" %}')
-
-        project = Project.objects.get(pk=kwargs['pid'])
-        self.add_column(title="Project compatible Layer ID",
-                        displayable = False,
-                        field_name = "projectcompatible_layer",
-                        computation = lambda x: (x.layer_version.get_equivalents_wpriority(project)[0]))
 
 class LayerRecipesTable(RecipesTable):
     """ Smaller version of the Recipes table for use in layer details """
@@ -422,8 +420,8 @@
 
 
     def setup_queryset(self, *args, **kwargs):
-        RecipesTable.setup_queryset(self, *args, **kwargs)
-        self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid']))
+        self.queryset = \
+                Recipe.objects.filter(layer_version__pk=int(kwargs['layerid']))
 
         self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count()
 
@@ -434,20 +432,197 @@
                         orderable=True,
                         field_name="name")
 
+        self.add_column(title="Version",
+                        field_name="version")
+
         self.add_column(title="Description",
                         field_name="get_description_or_summary")
 
-
         build_recipe_template ='<button class="btn btn-block build-recipe-btn" data-recipe-name="{{data.name}}" {%if extra.in_prj == 0 %}disabled="disabled"{%endif%}>Build recipe</button>'
 
         self.add_column(title="Build recipe",
                         static_data_name="add-del-layers",
                         static_data_template=build_recipe_template)
 
-class ProjectLayersRecipesTable(RecipesTable):
-    """ Table that lists only recipes available for layers added to the project """
+class CustomImagesTable(ToasterTable):
+    """ Table to display your custom images """
+    def __init__(self, *args, **kwargs):
+        super(CustomImagesTable, self).__init__(*args, **kwargs)
+        self.title = "Custom images"
+
+    def get_context_data(self, **kwargs):
+        context = super(CustomImagesTable, self).get_context_data(**kwargs)
+        project = Project.objects.get(pk=kwargs['pid'])
+        context['project'] = project
+        context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
+        return context
 
     def setup_queryset(self, *args, **kwargs):
-        super(ProjectLayersRecipesTable, self).setup_queryset(*args, **kwargs)
         prj = Project.objects.get(pk = kwargs['pid'])
-        self.queryset = self.queryset.filter(layer_version__in = prj.projectlayer_equivalent_set())
+        self.queryset = CustomImageRecipe.objects.filter(project=prj)
+        self.queryset = self.queryset.order_by('name')
+
+    def setup_columns(self, *args, **kwargs):
+
+        name_link_template = '''
+        <a href="{% url 'customrecipe' extra.pid data.id %}">
+          {{data.name}}
+        </a>
+        '''
+
+        self.add_column(title="Custom image",
+                        hideable=False,
+                        static_data_name="name",
+                        static_data_template=name_link_template)
+
+        self.add_column(title="Recipe file",
+                        static_data_name='recipe_file',
+                        static_data_template='')
+
+        approx_packages_template = '<a href="#imagedetails">{{data.packages.all|length}}</a>'
+        self.add_column(title="Approx packages",
+                        static_data_name='approx_packages',
+                        static_data_template=approx_packages_template)
+
+
+        build_btn_template = '''<button data-recipe-name="{{data.name}}"
+        class="btn btn-block build-recipe-btn" style="margin-top: 5px;" >
+        Build</button>'''
+
+        self.add_column(title="Build",
+                        hideable=False,
+                        static_data_name='build_custom_img',
+                        static_data_template=build_btn_template)
+
+class ImageRecipesTable(RecipesTable):
+    """ A subset of the recipes table which displayed just image recipes """
+
+    def __init__(self, *args, **kwargs):
+        super(ImageRecipesTable, self).__init__(*args, **kwargs)
+        self.title = "Compatible image recipes"
+
+    def setup_queryset(self, *args, **kwargs):
+        super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
+
+        self.queryset = self.queryset.filter(is_image=True)
+
+
+    def setup_columns(self, *args, **kwargs):
+        self.add_column(title="Image recipe",
+                        help_text="When you build an image recipe, you get an "
+                                  "image: a root file system you can"
+                                  "deploy to a machine",
+                        hideable=False,
+                        orderable=True,
+                        field_name="name")
+
+        super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
+
+        self.add_column(**RecipesTable.build_col)
+
+
+class NewCustomImagesTable(ImageRecipesTable):
+    """ Table which displays Images recipes which can be customised """
+    def __init__(self, *args, **kwargs):
+        super(NewCustomImagesTable, self).__init__(*args, **kwargs)
+        self.title = "Select the image recipe you want to customise"
+
+    def setup_queryset(self, *args, **kwargs):
+        super(ImageRecipesTable, self).setup_queryset(*args, **kwargs)
+
+        self.queryset = self.queryset.filter(is_image=True)
+
+    def setup_columns(self, *args, **kwargs):
+        self.add_column(title="Image recipe",
+                        help_text="When you build an image recipe, you get an "
+                                  "image: a root file system you can"
+                                  "deploy to a machine",
+                        hideable=False,
+                        orderable=True,
+                        field_name="recipe__name")
+
+        super(ImageRecipesTable, self).setup_columns(*args, **kwargs)
+
+        self.add_column(title="Customise",
+                        hideable=False,
+                        filter_name="in_current_project",
+                        static_data_name="customise-or-add-recipe",
+                        static_data_template='{% include "customise_btn.html" %}')
+
+
+class SoftwareRecipesTable(RecipesTable):
+    """ Displays just the software recipes """
+    def __init__(self, *args, **kwargs):
+        super(SoftwareRecipesTable, self).__init__(*args, **kwargs)
+        self.title = "Compatible software recipes"
+
+    def setup_queryset(self, *args, **kwargs):
+        super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs)
+
+        self.queryset = self.queryset.filter(is_image=False)
+
+
+    def setup_columns(self, *args, **kwargs):
+        self.add_column(title="Software recipe",
+                        help_text="Information about a single piece of "
+                        "software, including where to download the source, "
+                        "configuration options, how to compile the source "
+                        "files and how to package the compiled output",
+                        hideable=False,
+                        orderable=True,
+                        field_name="name")
+
+        super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs)
+
+        self.add_column(**RecipesTable.build_col)
+
+
+class SelectPackagesTable(ToasterTable):
+    """ Table to display the packages to add and remove from an image """
+
+    def __init__(self, *args, **kwargs):
+        super(SelectPackagesTable, self).__init__(*args, **kwargs)
+        self.title = "Add | Remove packages"
+
+    def setup_queryset(self, *args, **kwargs):
+        cust_recipe = CustomImageRecipe.objects.get(pk=kwargs['recipeid'])
+        prj = Project.objects.get(pk = kwargs['pid'])
+
+        current_packages = cust_recipe.packages.all()
+
+        # Get all the packages that are in the custom image
+        # Get all the packages built by builds in the current project
+        # but not those ones that are already in the custom image
+        self.queryset = Package.objects.filter(
+                            Q(pk__in=current_packages) |
+                            (Q(build__project=prj) &
+                            ~Q(name__in=current_packages.values_list('name'))))
+
+        self.queryset = self.queryset.order_by('name')
+
+        self.static_context_extra['recipe_id'] = kwargs['recipeid']
+        self.static_context_extra['current_packages'] = \
+                cust_recipe.packages.values_list('pk', flat=True)
+
+    def setup_columns(self, *args, **kwargs):
+        self.add_column(title="Package",
+                        hideable=False,
+                        orderable=True,
+                        field_name="name")
+
+        self.add_column(title="Package Version",
+                        field_name="version")
+
+        self.add_column(title="Approx Size",
+                        orderable=True,
+                        static_data_name="size",
+                        static_data_template="{% load projecttags %} \
+                        {{data.size|filtered_filesizeformat}}")
+        self.add_column(title="summary",
+                        field_name="summary")
+
+        self.add_column(title="Add | Remove",
+                        help_text="Use the add and remove buttons to modify "
+                        "the package content of you custom image",
+                        static_data_name="add_rm_pkg_btn",
+                        static_data_template='{% include "pkg_add_rm_btn.html" %}')
diff --git a/bitbake/lib/toaster/toastergui/templates/base.html b/bitbake/lib/toaster/toastergui/templates/base.html
index 640bc47..11ac2a0 100644
--- a/bitbake/lib/toaster/toastergui/templates/base.html
+++ b/bitbake/lib/toaster/toastergui/templates/base.html
@@ -1,9 +1,12 @@
 <!DOCTYPE html>
 {% load static %}
 {% load projecttags %}
+{% load project_url_tag %}
 <html lang="en">
     <head>
-        <title>{% if objectname %} {{objectname|title}} - {% endif %}Toaster</title>
+        <title>
+          {% block title %} Toaster {% endblock %}
+        </title>
 <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
 <link rel="stylesheet" href="{% static 'css/bootstrap-responsive.min.css' %}" type='text/css'/>
 <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
@@ -35,8 +38,9 @@
         projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}},
         {% if project.id %}
         projectId : {{project.id}},
-        projectPageUrl : {% url 'project' project.id as purl%}{{purl|json}},
+        projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}},
         projectName : {{project.name|json}},
+        projectIsDefault: {% if project.is_default %}true{% else %}false{% endif %},
         recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}},
         layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}},
         machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}},
@@ -47,7 +51,7 @@
         projectId : undefined,
         projectPageUrl : undefined,
         projectName : undefined,
-        projectId : undefined,
+        projectIsDefault: false,
         {% endif %}
       };
     </script>
@@ -89,9 +93,9 @@
             <i class="icon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i>
             {% endif %}
           </span>
-          {% if request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %}
+          {% if BUILD_MODE and request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %}
           <ul class="nav">
-            <li  {% if request.resolver_match.url_name == 'all-builds' %}
+            <li {% if request.resolver_match.url_name == 'all-builds' %}
                 class="active"
                 {% endif %}>
               <a href="{% url 'all-builds' %}">
@@ -118,55 +122,65 @@
             </li>
           </ul>
           <span class="pull-right divider-vertical"></span>
-          <div class="btn-group pull-right">
-            <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a>
-          </div>
-          <!-- New build popover -->
-          <div class="btn-group pull-right" id="new-build-button" style="display:none">
-            <button class="btn dropdown-toggle" data-toggle="dropdown">
-              New build
-              <i class="icon-caret-down"></i>
-            </button>
-            <ul class="dropdown-menu new-build multi-select">
-              <li>
-                <h3>New build</h3>
-                <h6>Project:</h6>
-                <span id="project">
-                  {% if project.id %}
-                  <a class="lead" href="{% url 'project' project.id %}">{{project.name}}</a>
-                  {% else %}
-                  <a class="lead" href="#"></a>
-                  {% endif %}
-                  <i class="icon-pencil"></i>
-                </span>
-                <form id="change-project-form" style="display:none;">
-                  <div class="input-append">
-                    <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/>
-                    <button id="save-project-button" class="btn" type="button">Save</button>
-                    <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a>
-                  </div>
-                  <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p>
-                </form>
-              </li>
-              <li>
-                <div class="alert" style="display:none;">
-                  <p>This project configuration is incomplete, so you cannot run builds.</p>
-                  <p><a href="{% if project.id %}{% url 'project' project.id %}{% endif %}">View project configuration</a></p>
-                </div>
-              </li>
-              <li id="targets-form">
-                <h6>Recipe(s):</h6>
-                <form>
-                  <input type="text" class="input-xlarge build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/>
-                  <div class="row-fluid">
-                    <button class="btn btn-primary build-button" disabled>Build</button>
-                  </div>
-                </form>
-              </li>
-            </ul>
-          </div>
 
+          <!-- new project button; only show in build mode -->
+          {% if BUILD_MODE %}
+            <div class="btn-group pull-right">
+              <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a>
+            </div>
+          {% endif %}
 
+          <!--
+          New build popover; only shown if there is at least one user-created project
+          and we're in build mode
+          -->
+          {% if BUILD_MODE and non_cli_projects.count > 0 %}
+            <div class="btn-group pull-right" id="new-build-button" style="display:none">
+              <button class="btn dropdown-toggle" data-toggle="dropdown">
+                New build
+                <i class="icon-caret-down"></i>
+              </button>
+              <ul class="dropdown-menu new-build multi-select">
+                <li>
+                  <h3>New build</h3>
+                  <h6>
+                    Project:
+                    <span id="project">
+                      {% if project.id and not project.is_default %}
+                        <a class="lead" href="{% project_url project %}">{{project.name}}</a>
+                      {% else %}
+                        <a class="lead" href="#"></a>
+                      {% endif %}
+                      <i class="icon-pencil"></i>
+                    </span>
+                  </h6>
+                  <form id="change-project-form" style="display:none;">
+                    <div class="input-append">
+                      <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/>
+                      <button id="save-project-button" class="btn" type="button">Save</button>
+                      <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a>
+                    </div>
+                    <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p>
+                  </form>
+                </li>
+                <li>
+                  <div class="alert" style="display:none;">
+                    <p>This project configuration is incomplete, so you cannot run builds.</p>
+                    <p><a href="{% if project.id %}{% url 'project' project.id %}{% endif %}">View project configuration</a></p>
+                  </div>
+                </li>
+                <li id="targets-form">
+                  <h6>Recipe(s):</h6>
+                  <form>
+                    <input type="text" class="input-xlarge build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/>
+                    <div class="row-fluid">
+                      <button class="btn btn-primary build-button" disabled>Build</button>
+                    </div>
+                  </form>
+                </li>
+              </ul>
+            </div>
+          {% endif %}
     </div>
  </div>
 </div>
diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
index 668e0bf..1f45be4 100644
--- a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
+++ b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
@@ -1,6 +1,9 @@
 {% extends "base.html" %}
 {% load projecttags %}
 {% load humanize %}
+
+{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
+
 {% block pagecontent %}
 
 {% include "projecttopbar.html" %}
@@ -23,8 +26,11 @@
     <ul class="nav nav-list well">
       <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li>
       <li class="nav-header">Compatible metadata</li>
-<!--  <li><a href="all-image-recipes.html">Image recipes</a></li> -->
-      <li><a href="{% url 'projecttargets' project.id %}">Recipes</a></li>
+      {% if CUSTOM_IMAGE %}
+      <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li>
+      {% endif %}
+      <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li>
+      <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li>
       <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li>
       <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li>
       <li class="nav-header">Extra configuration</li>
diff --git a/bitbake/lib/toaster/toastergui/templates/bpackage.html b/bitbake/lib/toaster/toastergui/templates/bpackage.html
index d775fec..81973cb 100644
--- a/bitbake/lib/toaster/toastergui/templates/bpackage.html
+++ b/bitbake/lib/toaster/toastergui/templates/bpackage.html
@@ -2,6 +2,7 @@
 
 {% load projecttags %}
 
+{% block title %} Packages built - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li>Packages</li>
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/builddashboard.html b/bitbake/lib/toaster/toastergui/templates/builddashboard.html
index bab8e38..323bbbb 100644
--- a/bitbake/lib/toaster/toastergui/templates/builddashboard.html
+++ b/bitbake/lib/toaster/toastergui/templates/builddashboard.html
@@ -2,8 +2,14 @@
 {% load humanize %}
 {% load projecttags %}
 
+{% block title %} {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block parentbreadcrumb %}
-{{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
+{% if build.get_sorted_target_list.count > 0 %}
+  {{build.get_sorted_target_list.0.target}}
+  &nbsp;
+{% endif %}
+
+{%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}})
 {% endblock %}
 
 {% block buildinfomain %}
@@ -37,19 +43,13 @@
     <span > <i class="icon-warning-sign yellow"></i><strong><a href="#warnings" class="warning show-warnings"> {{build.warnings.count}} warning{{build.warnings.count|pluralize}}</a></strong></span>
 {% endif %}
             <span class="pull-right">Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a>
-            <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%else%}btn-danger{%endif%} pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a>
+            {% if build.cooker_log_path %}
+                <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%else%}btn-danger{%endif%} pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a>
+            {% endif %}
             </span>
 
 {%endif%}
     </div>
-    {% if build.toaster_exceptions.count > 0 %}
-    <div class="row">
-        <small class="pull-right">
-        <i class="icon-question-sign get-help get-help-blue" title="" data-original-title="Toaster exceptions do not affect your build: only the operation of Toaster"></i>
-        <a class="show-exceptions" href="#exceptions">Toaster threw {{build.toaster_exceptions.count}} exception{{build.toaster_exceptions.count|pluralize}}</a>
-    </small>
-    </div>
-    {% endif %}
   </div>
 </div>
 
@@ -67,11 +67,10 @@
     <div class="accordion-body collapse in" id="collapse-errors">
       <div class="accordion-inner">
         <div class="span10">
-          {% for error in logmessages %}{% if error.level == 2 %}
-            <div class="alert alert-error">
+          {% for error in build.errors %}
+            <div class="alert alert-error" data-error="{{ error.id }}">
               <pre>{{error.message}}</pre>
             </div>
-          {% endif %}
           {% endfor %}
         </div>
       </div>
@@ -268,33 +267,6 @@
 </div>
 {% endif %}
 
-
-{% if build.toaster_exceptions.count > 0 %}
-<div class="accordion span10 pull-right" id="exceptions">
-  <div class="accordion-group">
-    <div class="accordion-heading">
-      <a class="accordion-toggle exception toggle-exceptions">
-        <h2 id="exception-toggle">
-          <i class="icon-warning-sign"></i>
-          {{build.toaster_exceptions.count}} Toaster exception{{build.toaster_exceptions.count|pluralize}}
-        </h2>
-      </a>
-    </div>
-    <div class="accordion-body collapse" id="collapse-exceptions">
-      <div class="accordion-inner">
-        <div class="span10">
-          {% for exception in build.toaster_exceptions %}
-            <div class="alert alert-exception">
-              <pre>{{exception.message}}</pre>
-            </div>
-          {% endfor %}
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-{% endif %}
-
 <script type="text/javascript">
     $(document).ready(function() {
         //show warnings section when requested from the previous page
diff --git a/bitbake/lib/toaster/toastergui/templates/builds.html b/bitbake/lib/toaster/toastergui/templates/builds.html
index c0d0c64..a27a121 100644
--- a/bitbake/lib/toaster/toastergui/templates/builds.html
+++ b/bitbake/lib/toaster/toastergui/templates/builds.html
@@ -2,8 +2,10 @@
 
 {% load static %}
 {% load projecttags %}
+{% load project_url_tag %}
 {% load humanize %}
 
+{% block title %} All builds - Toaster {% endblock %}
 {% block extraheadcontent %}
 <link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
 <link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
@@ -28,8 +30,6 @@
 
   {% include "mrb_section.html" %}
 
-
-  {% if 1 %}
   <div class="page-header top-air">
      <h1>
       {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
@@ -56,17 +56,25 @@
         </form>
       </div>
     </div>
-
-
   {% else %}
   {% include "basetable_top.html" %}
         <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work -->
         {% for build in objects %}
-        <tr class="data">
+        <tr class="data" data-table-build-result="{{ build.id }}">
             <td class="outcome">
-        <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> &nbsp;
-        </td>
-            <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td>
+                <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> &nbsp;
+            </td>
+            <td class="target">
+                {% for t in build.target_set.all %}
+                    <a href="{% url "builddashboard" build.id %}">
+                        {% if t.task %}
+                            {{t.target}}:{{t.task}}
+                        {% else %}
+                            {{t.target}}
+                        {% endif %}
+                    </a> <br />
+                {% endfor %}
+            </td>
             <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
             <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td>
             <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td>
@@ -93,8 +101,11 @@
               <a href="{%url "builddashboard" build.id%}#images">{{fstypes|get_dict_value:build.id}}</a>
               {% endif %}
             </td>
-        <td>
-                <a href="{% url 'project' build.project.id %}">{{build.project.name}}</a>
+            <td class="project-name">
+                <a href="{% project_url build.project %}">{{build.project.name}}</a>
+                {% if build.project.is_default %}
+                    <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
+                {% endif %}
             </td>
         </tr>
 
@@ -103,7 +114,6 @@
 
   {% include "basetable_bottom.html" %}
   {% endif %} {# objects.paginator.count #}
-{% endif %} {# empty #}
 </div><!-- end row-fluid-->
 
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/configuration.html b/bitbake/lib/toaster/toastergui/templates/configuration.html
index 3e48991..85d6a62 100644
--- a/bitbake/lib/toaster/toastergui/templates/configuration.html
+++ b/bitbake/lib/toaster/toastergui/templates/configuration.html
@@ -1,6 +1,7 @@
 {% extends "basebuildpage.html" %}
 {% load projecttags %}
 
+{% block title %} Configuration summary - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li>Configuration</li>
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/configvars.html b/bitbake/lib/toaster/toastergui/templates/configvars.html
index 8a572ae..e40c225 100644
--- a/bitbake/lib/toaster/toastergui/templates/configvars.html
+++ b/bitbake/lib/toaster/toastergui/templates/configvars.html
@@ -1,6 +1,7 @@
 {% extends "basebuildpage.html" %}
 {% load projecttags %}
 
+{% block title %} BitBake variables - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li>Configuration</li>
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/customise_btn.html b/bitbake/lib/toaster/toastergui/templates/customise_btn.html
new file mode 100644
index 0000000..54d05f9
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/customise_btn.html
@@ -0,0 +1,9 @@
+<button class="btn btn-block layer-exists-{{data.layer_version.id}} customise-btn" style="display:none;" data-recipe="{{data.id}}">
+  Customise
+</button>
+
+<button class="btn btn-block layer-add-{{data.layer_version.id}} layerbtn" data-layer='{ "id": {{data.layer_version.id}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{% url 'layerdetails' extra.pid data.layer_version.id %}"}' data-directive="add">
+  <i class="icon-plus"></i>
+  Add layer
+</button>
+
diff --git a/bitbake/lib/toaster/toastergui/templates/customrecipe.html b/bitbake/lib/toaster/toastergui/templates/customrecipe.html
new file mode 100644
index 0000000..823bbd8
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/customrecipe.html
@@ -0,0 +1,142 @@
+{% extends "base.html" %}
+{% load projecttags %}
+{% load humanize %}
+{% load static %}
+{% block pagecontent %}
+
+{% include "projecttopbar.html" %}
+
+<script src="{% static 'js/customrecipe.js' %}"></script>
+<script>
+  $(document).ready(function (){
+    var ctx = {
+      tableApiUrl: "{% url 'recipeselectpackages' project.id recipe.pk %}?format=json"
+    };
+
+    try {
+      customRecipePageInit(ctx);
+    } catch (e) {
+      document.write("Sorry, An error has occurred loading this page");
+      console.warn(e);
+    }
+  });
+</script>
+
+<div class="row-fluid span11">
+  <div class="alert alert-success lead" id="image-created-notification" style="margin-top: 15px; display: none">
+    <button type="button" data-dismiss="alert" class="close">x</button>
+    Your custom image <strong>{{recipe.name}}</strong> has been created. You can now add or remove packages as needed.
+  </div>
+  <div class="page-header air">
+    <h1>
+      {{recipe.name}}
+      <small>({{recipe.base_recipe.name}})</small>
+    </h1>
+  </div>
+</div>
+
+<div class="row-fluid span11">
+  <div class="span8">
+    <div class="button-place btn-group" style="width: 100%">
+      <a class="btn btn-large span6" href="#" id="build-custom-image" style="width: 50%">
+        Build {{recipe.name}}
+      </a>
+      <button class="btn btn-large span6" data-toggle="modal" data-target="#download-file" id="download" style="width: 50%">
+      Download recipe file
+    </button>
+  </div>
+  <div id="no-package-results" class="air" style="display:none;">
+    <div class="alert">
+      <h3>No packages found</h3>
+      <p>You might consider <a href="all-software-recipes.html">searching the list of recipes</a> instead. If you find a recipe that matches the name of the package you want:</p>
+      <ol>
+        <li>Add the layer providing the recipe to your project</li>
+        <li>Build the recipe</li>
+        <li>Once the build completes, come back to this page and search for the package</li>
+      </ol>
+      <form class="input-append no-results">
+        <input type="text" class="input-xlarge" value="search query">
+          <a href="#" class="add-on btn">
+            <i class="icon-remove"></i>
+          </a>
+          <button class="btn">Search</button>
+          <button class="btn btn-link" id="show-all">Show all packages</button>
+        </form>
+      </div>
+    </div>
+    <div id="packages-table">
+      {% url 'recipeselectpackages' project.id recipe.id as xhr_table_url %}
+      {% with 'recipeselection' as table_name %}
+      {% with 'Add | Remove packages' as  title %}
+
+      <h2>{{title}} (<span class="table-count-{{table_name}}"></span>) </h2>
+
+      {% include "toastertable.html" %}
+      {% endwith %}
+      {% endwith %}
+    </div>
+  </div>
+    <div class="span4 well">
+      <h2 style="margin-bottom:20px;">About {{recipe.name}}</h2>
+
+      <dl>
+        <dt>
+          Approx. packages included
+          <i class="icon-question-sign get-help" title="" data-original-title="The number of packages included is based on information from previous builds and from parsing layers, so we can never be sure it is 100% accurate"></i>
+        </dt>
+        <dd class="no-packages">{{recipe.packages.count}}</dd>
+        <!-- <dt>
+          Approx. package size
+          <i class="icon-question-sign get-help" title="" data-original-title="Package size is based on information from previous builds, so we can never be sure it is 100% accurate"></i>
+        </dt>
+        <dd>244.3 MB</dd>
+        <dt>Last build</dt>
+        <dd>
+          <i class="icon-ok-sign success"></i>
+          <a href="build-dashboard.html">11/06/15 15:22</a>
+        </dd>
+        <dt>Recipe file</dt>
+        <dd>
+          <code>custom-image-name.bb</code>
+          <a href="#download-file" data-toggle="modal"><i class="icon-download-alt" title="" data-original-title="Download recipe file"></i></a>
+          </dd> -->
+        <dt>Layer</dt>
+        <!-- TODO recipe details page -->
+        <dd><a href="{% url 'layerdetails' project.id recipe.base_recipe.layer_version.pk %}">{{recipe.base_recipe.layer_version.layer.name}}</a></dd>
+        <!--<dt>
+          Summary
+        </dt>
+        <dd>
+          <span class="muted">Not set</span>
+          <i class="icon-pencil" data-original-title="" title=""></i>
+        </dd>
+        <dt>
+          Description
+        </dt>
+        <dd>
+          <span class="muted">Not set</span>
+          <i class="icon-pencil" data-original-title="" title=""></i>
+        </dd>
+        <dt>Version</dt>
+        <dd>
+          1.0
+          <i class="icon-pencil" data-original-title="" title=""></i>
+        </dd>
+        <dt>Section</dt>
+        <dd>
+          base
+          <i class="icon-pencil" data-original-title="" title=""></i>
+          <i class="icon-trash" data-original-title="" title=""></i>
+        </dd>
+        <dt>License</dt>
+        <dd>
+          MIT
+          <i class="icon-question-sign get-help" title="" data-original-title="All custom images have their license set to MIT. This is because the license applies only to the recipe (.bb) file, and not to the image itself. To see which licenses apply to the image you must check the license manifest generated with each build"></i>
+          </dd> -->
+      </dl>
+      <i class="icon-trash no-tooltip"></i>
+      <a href="#" class="error" id="delete">Delete custom image</a>
+    </div>
+</div>
+
+  {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/dirinfo.html b/bitbake/lib/toaster/toastergui/templates/dirinfo.html
index a5bc481..ecb46bf 100644
--- a/bitbake/lib/toaster/toastergui/templates/dirinfo.html
+++ b/bitbake/lib/toaster/toastergui/templates/dirinfo.html
@@ -1,4 +1,5 @@
 {% extends "basebuildpage.html" %}
+{% block title %} Directory structure - {{ target.target }} {{ build.machine }} - {{ build.project.name }} - Toaster {% endblock %}
 {% block extraheadcontent %}
 {% load static %}
 <link rel="stylesheet" href="{% static 'css/jquery.treetable.css' %}" type="text/css">
@@ -103,12 +104,16 @@
             name += '</td>';
         }
         else {
-            name = '<td>';
             if (o.link_to == null) {
-                name += '<i class="icon-file"></i>';
+                namespan = 2;
+                if (o.package == null) {
+                  namespan = 3;
+                }
+                var colspan = 'colspan="' + namespan + '"';
+                name = '<td ' + colspan + '><i class="icon-file"></i>';
             }
             else {
-                name += '<i class="icon-hand-right"></i>';
+                name = '<td><i class="icon-hand-right"></i>';
             }
             name += '&nbsp;' + o.name;
             name += '</td>';
@@ -207,10 +212,10 @@
                     <th>Directory / File</th>
                     <th>Symbolic link to</th>
                     <th>Source package</th>
-                    <th>Size</th>
-                    <th>Permissions</th>
-                    <th>Owner</th>
-                    <th>Group</th>
+                    <th class="narrow-col">Size</th>
+                    <th class="medium-col">Permissions</th>
+                    <th class="narrow-col">Owner</th>
+                    <th class="narrow-col">Group</th>
                 </tr>
             </thead>
             <tbody>
diff --git a/bitbake/lib/toaster/toastergui/templates/importlayer.html b/bitbake/lib/toaster/toastergui/templates/importlayer.html
index ce3d724..033f0ae 100644
--- a/bitbake/lib/toaster/toastergui/templates/importlayer.html
+++ b/bitbake/lib/toaster/toastergui/templates/importlayer.html
@@ -2,6 +2,7 @@
 {% load projecttags %}
 {% load humanize %}
 {% load static %}
+{% block title %} Import layer - {{project.name}} - Toaster {% endblock %}
 {% block pagecontent %}
 
 {% include "projecttopbar.html" %}
diff --git a/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html b/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
index 5b8fd84..8d65f33 100644
--- a/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
+++ b/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html
@@ -22,9 +22,11 @@
 <script src="{% static 'js/table.js' %}"></script>
 
 <script>
-  var tableUrl = '{% url 'projectlayers' project.pk %}';
+  var ctx = {
+    tableUrl : '{% url 'projectlayers' project.pk %}',
+    projectId : {{project.pk}},
+  }
 </script>
-
 <script src="{% static 'js/tests/test.js' %}"></script>
 
 <div id="qunit"></div>
@@ -34,6 +36,12 @@
 <input type="text" id="projects" placeholder="projects"></input>
 <input type="text" id="machines" placeholder="machines"></input>
 
+<!-- import layer dependency input typeahead -->
+<input type="text" id="layer-dependency" style="display:none"></input>
+<!-- project page input typeaheads -->
+<input type="text" id="layer-add-input" style="display:none"></input>
+<input type="text" id="machine-change-input" style="display:none"></input>
+<!-- import layer dependency input typeahead on layer details edit layer -->
+<input type="text" id="layer-dep-input" style="display:none"></input>
+
 {% endblock %}
-
-
diff --git a/bitbake/lib/toaster/toastergui/templates/landing.html b/bitbake/lib/toaster/toastergui/templates/landing.html
index 45e9532..cafaa1a 100644
--- a/bitbake/lib/toaster/toastergui/templates/landing.html
+++ b/bitbake/lib/toaster/toastergui/templates/landing.html
@@ -4,55 +4,69 @@
 {% load projecttags %}
 {% load humanize %}
 
+{% block title %} Welcome to Toaster {% endblock %}
 {% block pagecontent %}
 
-  <div class="container-fluid">
-   <div class="row-fluid">
-    <!-- Empty - no data in database -->
-    <div class="hero-unit span12 well-transparent">
-     <div class="row-fluid">
-      <div class="span6">
-       <h1>
-        This is Toaster
-       </h1>
-       <p>A web interface to <a href="http://www.openembedded.org">OpenEmbedded</a> and <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake">BitBake</a>, the <a href="http://www.yoctoproject.org">Yocto Project</a> build system.</p>
+  {% if BUILD_MODE %}
+    <!-- build mode -->
+    <div class="container-fluid">
+      <div class="row-fluid">
+        <div class="hero-unit span12 well-transparent">
+          <div class="row-fluid">
 
+            <div class="span6">
+              <h1>This is Toaster</h1>
 
-		{% if lvs_nos %}
-		    <p class="hero-actions">
-			    <a class="btn btn-primary btn-large" href="{% url 'newproject' %}">
-				    To start building, create your first Toaster project
-			    </a>
-		    </p>
-		{% else %}
-		    <div class="alert alert-info lead air">
-			    Toaster has no layer information. Without layer information, you cannot run builds. To generate layer information you can:
-			    <ul>
-			        <li>
-			            <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html#layer-source">Configure a layer source</a>
-			        </li>
-			        <li>
-			            <a href="{% url 'newproject' %}">Create a project</a>, then import layers
-			        </li>
-			    </ul>
-		    </div>
-	        {% endif %}
+              <p>A web interface to <a href="http://www.openembedded.org">OpenEmbedded</a> and <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake">BitBake</a>, the <a href="http://www.yoctoproject.org">Yocto Project</a> build system.</p>
 
-        <ul class="unstyled">
-            <li>
-                <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html">Read the Toaster manual</a>
-            </li>
-            <li>
-                <a href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster">Contribute to Toaster</a>
-            </li>
-        </ul>
+		          {% if lvs_nos %}
+		            <p class="hero-actions">
+		              <a class="btn btn-primary btn-large" href="{% url 'newproject' %}">
+			              To start building, create your first Toaster project
+		              </a>
+		            </p>
+		          {% else %}
+                <div class="alert alert-info lead air">
+                  Toaster has no layer information. Without layer information, you cannot run builds. To generate layer information you can:
+                  <ul>
+                    <li>
+                      <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html#layer-source">Configure a layer source</a>
+                    </li>
+                    <li>
+			                <a href="{% url 'newproject' %}">Create a project</a>, then import layers
+                    </li>
+                  </ul>
+                </div>
+              {% endif %}
 
+              <ul class="unstyled">
+                <li>
+                  <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html">
+                    Read the Toaster manual
+                  </a>
+                </li>
+
+                <li>
+                  <a href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster">
+                    Contribute to Toaster
+                  </a>
+                </li>
+              </ul>
+            </div>
+
+            <div class="span6">
+              <img alt="Yocto Project" class="thumbnail" src="{% static 'img/toaster_bw.png' %}"/>
+            </div>
+
+          </div>
+        </div>
       </div>
-      <div class="span6">
-          <img alt="Yocto Project" class="thumbnail" src="{% static 'img/toaster_bw.png' %}"/>
-      </div>
-     </div>
     </div>
-   </div>
+  {% else %}
+    <!-- analysis mode -->
+    <div class="alert alert-info lead top-air">
+      Toaster has not recorded any builds yet. Run a build from the command line to see it here.
+    </div>
+  {% endif %}
 
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html b/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html
index 5bc435d..9b37f55 100644
--- a/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html
+++ b/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html
@@ -4,6 +4,8 @@
 {% load projecttags %}
 {% load humanize %}
 
+{% block title %} Welcome to Toaster {% endblock %}
+
 {% block pagecontent %}
 
   <div class="container-fluid">
diff --git a/bitbake/lib/toaster/toastergui/templates/layer_btn.html b/bitbake/lib/toaster/toastergui/templates/layer_btn.html
index a2e9393..314eec7 100644
--- a/bitbake/lib/toaster/toastergui/templates/layer_btn.html
+++ b/bitbake/lib/toaster/toastergui/templates/layer_btn.html
@@ -1,8 +1,16 @@
-<button class="btn btn-danger btn-block layer-exists-{{data.pk}} layerbtn" style="display:none;" data-layer='{ "id": {{data.pk}}, "name":  "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="remove" >
+<button class="btn btn-danger btn-block layer-exists-{{data.pk}} layerbtn"  data-layer='{ "id": {{data.pk}}, "name":  "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="remove"
+    {% if data.pk not in extra.current_layers %}
+    style="display:none;"
+    {% endif %}
+  >
   <i class="icon-trash"></i>
   Delete layer
 </button>
-<button class="btn btn-block layer-add-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name":  "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="add">
+<button class="btn btn-block layer-add-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name":  "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="add"
+    {% if data.pk in extra.current_layers %}
+    style="display:none;"
+    {% endif %}
+  >
   <i class="icon-plus"></i>
   Add layer
 </button>
diff --git a/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
index 7dd3db2..7fe365d 100644
--- a/bitbake/lib/toaster/toastergui/templates/layerdetails.html
+++ b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
@@ -3,6 +3,7 @@
 {% load humanize %}
 {% load static %}
 
+{% block title %} {{layerversion.layer.name}} - {{project.name}} - Toaster {% endblock %}
 {% block pagecontent %}
 
 <div class="section">
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index 396fb8e..bd8f991 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -1,43 +1,68 @@
 {% load static %}
 {% load projecttags %}
+{% load project_url_tag %}
 {% load humanize %}
 
+{%if mru and mru.count > 0%}
 
-{%if mru.count > 0%}
+  {%if mrb_type == 'project' %}
+      <h2>
+      Latest project builds
 
-  <div class="page-header">
+      {% if project.is_default %}
+          <i class="icon-question-sign get-help heading-help" title="" data-original-title="Builds in this project cannot be started from Toaster: they are started from the command line"></i>
+      {% endif %}
+      </h2>
+  {% else %}
+    <div class="page-header">
       <h1>
-          Latest builds
-       </h1>
-  </div>
+      Latest builds
+      </h1>
+    </div>
+  {% endif %}
   <div id="latest-builds">
   {% for build in mru %}
-    <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%} project-name ">
-       <span class="label {%if build.outcome == build.SUCCEEDED%}label-success{%elif build.outcome == build.FAILED%}label-important{%else%}label-info{%endif%}">
-         <a href={% url 'project' build.project.pk %}>
-           {{build.project.name}}
-         </a>
-       </span>
-
+  <div data-latest-build-result="{{ build.id }}" class="alert build-result {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}{% if mrb_type != 'project' %} project-name{% endif %}">
+      {% if mrb_type != 'project' %}
+        <span class="label {%if build.outcome == build.SUCCEEDED%}label-success{%elif build.outcome == build.FAILED%}label-important{%else%}label-info{%endif%}">
+            <a href={% project_url build.project %}>
+                {{build.project.name}}
+            </a>
+        </span>
+      {% endif %}
         <div class="row-fluid">
-            <div class="span3 lead">
+          <div class="span3 lead">
     {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
-                <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}">
+              <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}">
     {% endif %}
             {% if build.target_set.all.count > 0 %}
                 <span data-toggle="tooltip"
-                  {%if build.target_set.all.count > 1%}
-                    title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"
-                  {%endif%}
+                  {% if build.target_set.all.count > 1 %}
+                    title="Targets:
+                    {% for target in build.target_set.all %}
+                        {% if target.task %}
+                            {{target.target}}:{{target.task}}
+                        {% else %}
+                            {{target.target}}
+                        {% endif %}
+                    {% endfor %}"
+                  {% endif %}
                 >
-
-                  {{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%}
+                {% if build.target_set.all.0.task %}
+                    {{build.target_set.all.0.target}}:{{build.target_set.all.0.task}}
+                {% else %}
+                    {{build.target_set.all.0.target}}
+                {% endif %}
+                {% if build.target_set.all.count > 1 %}
+                    (+ {{build.target_set.all.count|add:"-1"}})
+                {% endif %}
                 </span>
              {% endif %}
     {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
                 </a>
     {% endif %}
             </div>
+    {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
             <div class="span2 lead">
                 {% if build.completed_on|format_build_date  %}
                     {{ build.completed_on|date:'d/m/y H:i' }}
@@ -45,6 +70,7 @@
                     {{ build.completed_on|date:'H:i' }}
                 {% endif %}
             </div>
+    {% endif %}
     {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
             <div class="span2 lead">
       {% if  build.errors.count %}
@@ -58,28 +84,41 @@
             </div>
             <div class="lead ">
               <span class="lead">
-                Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a>
+                  Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a>
               </span>
-              <button class="btn
+              {% if build.project.is_default %}
+                  <i class="pull-right icon-question-sign get-help
                   {% if build.outcome == build.SUCCEEDED %}
-                      btn-success
+                      get-help-green
                   {% elif build.outcome == build.FAILED %}
-                      btn-danger
+                      get-help-red
                   {% else %}
-                      btn-info
-                  {%endif%}
-                  pull-right"
-                  onclick='scheduleBuild({% url 'projectbuilds' build.project.id as bpi %}{{bpi|json}},
-                    {{build.project.name|json}},
-                    {% url 'project' build.project.id as bpurl %}{{bpurl|json}},
-                    {{build.target_set.all|get_tasks|json}})'>
+                      get-help-blue
+                  {% endif %}
+                  " title="Builds in this project cannot be started from Toaster: they are started from the command line">
+                  </i>
+              {% else %}
+                  <button class="btn
+                      {% if build.outcome == build.SUCCEEDED %}
+                          btn-success
+                      {% elif build.outcome == build.FAILED %}
+                          btn-danger
+                      {% else %}
+                          btn-info
+                      {%endif%}
+                      pull-right"
+                      onclick='scheduleBuild({% url 'projectbuilds' build.project.id as bpi %}{{bpi|json}},
+                        {{build.project.name|json}},
+                        {% url 'project' build.project.id as purl %}{{purl|json}},
+                        {{build.target_set.all|get_tasks|json}})'>
 
-                    Run again
-              </button>
+                        Run again
+                  </button>
+              {% endif %}
             </div>
     {%endif%}
     {%if build.outcome == build.IN_PROGRESS %}
-            <div class="span4">
+            <div class="span4 offset1">
                 <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
                     <div style="width: {{build.completeper}}%;" class="bar"></div>
                 </div>
diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
new file mode 100644
index 0000000..4487b3e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
@@ -0,0 +1,54 @@
+{% extends "base.html" %}
+{% load projecttags %}
+{% load humanize %}
+{% load static %}
+{% block pagecontent %}
+
+<script src="{% static 'js/newcustomimage.js' %}"></script>
+<script>
+  $(document).ready(function (){
+    var ctx = {
+      xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
+    };
+
+    try {
+      newCustomImagePageInit(ctx);
+    } catch (e) {
+      document.write("Sorry, An error has occurred loading this page");
+      console.warn(e);
+    }
+  });
+</script>
+
+</script>
+<div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+    <h3>Name your custom image</h3>
+  </div>
+  <div class="modal-body">
+    <div class="row-fluid">
+      <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p>
+      </span></div>
+    <div class="control-group controls">
+      <input type="text" class="huge span5" placeholder="Type the name, something like 'core-image-myimage'">
+        <span class="help-block" style="display:none">Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)</span>
+        <span class="help-block" style="display: none">An image with this name already exists. Image names must be unique: try a different one.</span>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <a href="#" id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="">Create custom image</a>
+    </div>
+</div>
+
+{% include "projecttopbar.html" %}
+
+
+{% url table_name project.id as xhr_table_url %}
+{% include "toastertable.html" %}
+
+
+
+{% endblock %}
+
+
diff --git a/bitbake/lib/toaster/toastergui/templates/newproject.html b/bitbake/lib/toaster/toastergui/templates/newproject.html
index 997390b..e83b2be 100644
--- a/bitbake/lib/toaster/toastergui/templates/newproject.html
+++ b/bitbake/lib/toaster/toastergui/templates/newproject.html
@@ -1,6 +1,9 @@
 {% extends "base.html" %}
 {% load projecttags %}
 {% load humanize %}
+
+{% block title %} Create a new project - Toaster {% endblock %}
+
 {% block pagecontent %}
 <div class="row-fluid">
     <div class="page-header">
diff --git a/bitbake/lib/toaster/toastergui/templates/package_detail_base.html b/bitbake/lib/toaster/toastergui/templates/package_detail_base.html
index a24bc8e..9fa28a8 100644
--- a/bitbake/lib/toaster/toastergui/templates/package_detail_base.html
+++ b/bitbake/lib/toaster/toastergui/templates/package_detail_base.html
@@ -1,6 +1,13 @@
 {% extends "basebuilddetailpage.html" %}
 {% load projecttags %}
 
+{% block title %}
+  {% if target %}
+    {{package.fullpackagespec}} - {{ target.target }} {{ build.machine }} - {{ build.project.name }} - Toaster
+  {% else %}
+    {{package.fullpackagespec}} - {{ build.target_set.all|dictsort:"target"|join:", " }} {{ build.machine }} - {{ build.project.name }} - Toaster
+  {% endif %}
+{% endblock %}
 {% block extraheadcontent %}
     <!-- functions to format package 'installed_package' alias -->
     <script>
@@ -38,9 +45,9 @@
 {% block pagedetailinfomain %}
     <div class="row span11">
         <div class="page-header">
-            {% block title %}
+            {% block mainheading %}
             <h1>{{package.fullpackagespec}}</h1>
-            {% endblock title %}
+            {% endblock %}
         </div> <!-- page-header -->
     </div> <!-- row span11 page-header -->
 
diff --git a/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html b/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html
index 642ca69..8a0508e 100644
--- a/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html
+++ b/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html
@@ -1,13 +1,13 @@
 {% extends "package_detail_base.html" %}
 {% load projecttags %}
 
-{% block title %}
+{% block mainheading %}
     <h1>
         {{package.fullpackagespec}}
         <script> fmtAliasHelp("{{package.name}}", "{{package.alias}}", false) </script>
         <small>({{target.target}})</small>
     </h1>
-{% endblock title %}
+{% endblock %}
 
 {% block tabcontent %}
     {% with packageFileCount=package.buildfilelist_package.count %}
diff --git a/bitbake/lib/toaster/toastergui/templates/package_included_detail.html b/bitbake/lib/toaster/toastergui/templates/package_included_detail.html
index d2aa26e..568e2f2 100644
--- a/bitbake/lib/toaster/toastergui/templates/package_included_detail.html
+++ b/bitbake/lib/toaster/toastergui/templates/package_included_detail.html
@@ -1,7 +1,7 @@
 {% extends "package_detail_base.html" %}
 {% load projecttags %}
 
-{% block title %}
+{% block mainheading %}
         <h1>
             {{package.fullpackagespec}}
             <script>
@@ -9,7 +9,7 @@
             </script>
             <small>({{target.target}})</small>
         </h1>
-{% endblock title %}
+{% endblock %}
 
 {% block tabcontent %}
 {% with packageFileCount=package.buildfilelist_package.count %}
diff --git a/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html b/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html
index 5cc8b47..fb310c7 100644
--- a/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html
+++ b/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html
@@ -1,13 +1,13 @@
 {% extends "package_detail_base.html" %}
 {% load projecttags %}
 
-{% block title %}
+{% block mainheading %}
         <h1>
             {{package.fullpackagespec}}
             <script> fmtAliasHelp("{{package.name}}", "{{package.alias}}", false) </script>
             <small>({{target.target}})</small>
         </h1>
-{% endblock title %}
+{% endblock %}
 
 {% block tabcontent %}
     {% with packageFileCount=package.buildfilelist_package.count %}
diff --git a/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html b/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html
new file mode 100644
index 0000000..b766aea
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html
@@ -0,0 +1,16 @@
+<button class="btn btn-block btn-danger add-rm-package-btn" id="package-rm-btn-{{data.pk}}" data-directive="remove" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style="
+  {% if data.pk not in extra.current_packages %}
+    display:none
+  {% endif %}
+  ">
+  <i class="icon-trash no-tooltip"></i>
+  Remove package
+</a>
+<button class="btn btn-block add-rm-package-btn" data-directive="add" id="package-add-btn-{{data.pk}}" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style="
+  {% if data.pk in extra.current_packages %}
+    display:none
+  {% endif %}
+    ">
+<i class="icon-plus"></i>
+ Add package
+</button>
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html
index e8354fd..4e83981 100644
--- a/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/bitbake/lib/toaster/toastergui/templates/project.html
@@ -4,6 +4,7 @@
 {% load humanize %}
 {% load static %}
 
+{% block title %} Configuration - {{project.name}} - Toaster {% endblock %}
 {% block projectinfomain %}
 
 <script src="{% static 'js/layerDepsModal.js' %}"></script>
@@ -67,7 +68,7 @@
 
       <div class="alert alert-info" style="display:none" id="no-most-built">
         <span class="lead">You haven't built any recipes yet</span>
-        <p style="margin-top: 10px;"><a href="{% url 'projecttargets' project.id %}">Choose a recipe to build</a></p>
+        <p style="margin-top: 10px;"><a href="{% url 'projectsoftwarerecipes' project.id %}">Choose a recipe to build</a></p>
       </div>
 
       <ul class="unstyled configuration-list" id="freq-build-list">
diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds.html b/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
index 27cfcd7..bb38284 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds.html
@@ -2,7 +2,7 @@
 {% load projecttags %}
 {% load humanize %}
 
-
+{% block title %} Builds - {{project.name}} - Toaster {% endblock %}
 {% block extraheadcontent %}
 <link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
 <link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
@@ -21,13 +21,17 @@
     });
 </script>
 
+ {% with mrb_type='project' %}
+     {% include "mrb_section.html" %}
+ {% endwith %}
+
       <h2>
         {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
-            {{objects.paginator.count}} build{{objects.paginator.count|pluralize}} found
+            {{objects.paginator.count}} project build{{objects.paginator.count|pluralize}} found
         {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
-            No builds found
+            No project builds found
         {%else%}
-            Project builds
+            All project builds
         {%endif%}
         <i class="icon-question-sign get-help heading-help" title="This page lists all the builds for the current project"></i>
       </h2>
@@ -66,7 +70,17 @@
                     {% endif %}
             </td>
 
-            <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td>
+            <td class="target">
+                {% for t in build.target_set.all %}
+                    <a href="{% url "builddashboard" build.id %}">
+                        {% if t.task %}
+                            {{t.target}}:{{t.task}}
+                        {% else %}
+                            {{t.target}}
+                        {% endif %}
+                    </a> <br />
+                {% endfor %}
+            </td>
             <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
             <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td>
             <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td>
diff --git a/bitbake/lib/toaster/toastergui/templates/projectconf.html b/bitbake/lib/toaster/toastergui/templates/projectconf.html
index 4c5a188..30fd03e 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectconf.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectconf.html
@@ -2,7 +2,7 @@
 {% load projecttags %}
 {% load humanize %}
 
-
+{% block title %} BitBake variables - {{project.name}} - Toaster {% endblock %}
 {% block projectinfomain %}
 
 <h2>Bitbake variables</h2>
@@ -43,6 +43,7 @@
                     <input id="filter-image_fstypes" type="text" placeholder="Search image types" class="span4">
                     <div id="all-image_fstypes" class="scrolling">
                     </div>
+                    <span class="help-block" id="fstypes-error-message">You must select at least one image type</span>
                     <button id="apply-change-image_fstypes" type="button" class="btn">Save</button>
                     <button id="cancel-change-image_fstypes" type="button" class="btn btn-link">Cancel</button>
                 </form>
@@ -312,9 +313,11 @@
             });
             if ( 0 == any_checked ) {
                 $("#apply-change-image_fstypes").attr("disabled","disabled");
+                $('#fstypes-error-message').show();
             }
             else {
                 $("#apply-change-image_fstypes").removeAttr("disabled");
+                $('#fstypes-error-message').hide();
             }
         }
 
@@ -546,10 +549,14 @@
                 // Add the un-checked boxes second
                 for (var i = 0, length = fstypes_list.length; i < length; i++) {
                     if (0  > fstypes.indexOf(" "+fstypes_list[i].value+" ")) {
-                            html += '<label class="checkbox"><input type="checkbox" class="fs-checkbox-fstypes" value="'+fstypes_list[i].value+'">'+fstypes_list[i].value+'</label>\n';
+                        html += '<label class="checkbox"><input type="checkbox" class="fs-checkbox-fstypes" value="'+fstypes_list[i].value+'">'+fstypes_list[i].value+'</label>\n';
                     }
                 }
+                // Add the 'no search matches' line last
+                html += '<label id="no-match-fstypes">No image types found</label>\n';
+                // Display the list
                 document.getElementById("all-image_fstypes").innerHTML = html;
+                $('#no-match-fstypes').hide();
 
                 // Watch elements to disable Save when none are checked
                 $(".fs-checkbox-fstypes").each(function(){
@@ -558,8 +565,9 @@
                     });
                 });
 
-                // clear the previous filter values
+                // clear the previous filter values and warning messages
                 $("input#filter-image_fstypes").val("");
+                $('#fstypes-error-message').hide();
             });
 
             $('#cancel-change-image_fstypes').click(function(){
@@ -569,17 +577,24 @@
             });
 
             $('#filter-image_fstypes').on('input', function(){
-                   var valThis = $(this).val().toLowerCase();
+                var valThis = $(this).val().toLowerCase();
+                var matchCount=0;
                 $('#all-image_fstypes label').each(function(){
                     var text = $(this).text().toLowerCase();
                     var match = text.indexOf(valThis);
                     if (match >= 0) {
                         $(this).show();
+                        matchCount += 1;
                     }
                     else {
                         $(this).hide();
                     }
                 });
+                if (matchCount === 0) {
+                   $('#no-match-fstypes').show();
+                } else {
+                   $('#no-match-fstypes').hide();
+                }
             });
 
             $('#apply-change-image_fstypes').click(function(){
diff --git a/bitbake/lib/toaster/toastergui/templates/projects.html b/bitbake/lib/toaster/toastergui/templates/projects.html
index c2d77b5..678a796 100644
--- a/bitbake/lib/toaster/toastergui/templates/projects.html
+++ b/bitbake/lib/toaster/toastergui/templates/projects.html
@@ -2,8 +2,11 @@
 
 {% load static %}
 {% load projecttags %}
+{% load project_url_tag %}
 {% load humanize %}
 
+{% block title %} All projects - Toaster {% endblock %}
+
 {% block pagecontent %}
 
 
@@ -36,17 +39,29 @@
   {% else %} {# We have builds to display #}
   {% include "basetable_top.html" %}
   {% for o in objects %}
-    <tr class="data">
-      <td><a href="{% url 'project' o.id %}">{{o.name}}</a></td>
-      <td class="updated"><a href="{% url 'project' o.id %}">{{o.updated|date:"d/m/y H:i"}}</a></td>
-      <td>
+    <tr class="data" data-project="{{ o.id }}">
+      <td data-project-field="name">
+          <a href="{% project_url o %}">{{o.name}}</a>
+      </td>
+      <td class="updated"><a href="{% project_url o %}">{{o.updated|date:"d/m/y H:i"}}</a></td>
+      <td data-project-field="release">
         {% if o.release %}
             <a href="{% url 'project' o.id %}#project-details">{{o.release.name}}</a>
+        {% elif o.is_default %}
+            <span class="muted">Not applicable</span>
+            <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a release set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
         {% else %}
             No release available
         {% endif %}
       </td>
-      <td><a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a></td>
+      <td data-project-field="machine">
+        {% if o.is_default %}
+            <span class="muted">Not applicable</span>
+            <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a machine set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
+        {% else %}
+            <a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a>
+        {% endif %}
+      </td>
       {% if o.get_number_of_builds == 0 %}
       <td class="muted">{{o.get_number_of_builds}}</td>
       <td class="loutcome"></td>
diff --git a/bitbake/lib/toaster/toastergui/templates/projecttopbar.html b/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
index ca2741d..ee86b54 100644
--- a/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
+++ b/bitbake/lib/toaster/toastergui/templates/projecttopbar.html
@@ -1,12 +1,18 @@
 <div class="alert alert-success lead" id="project-created-notification" style="margin-top:15px; display:none">
   <button type="button" class="close" data-dismiss="alert">×</button>
-  Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projecttargets' project.id %}">choose image recipes</a> to build.
+  Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projectsoftwarerecipes' project.id %}">choose image recipes</a> to build.
 </div>
 
 <!-- project name -->
 <div class="page-header">
-  <h1><span id="project-name">{{project.name}}</span>
+  <h1 id="project-name-container">
+    <span id="project-name">{{project.name}}</span>
+
     <i class="icon-pencil" data-original-title="" id="project-change-form-toggle" title=""></i>
+
+    {% if project.is_default %}
+        <i class="icon-question-sign get-help heading-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running"></i>
+    {% endif %}
   </h1>
   <form id="project-name-change-form" style="margin-bottom: 0px; display: none;">
     <div class="input-append">
@@ -17,31 +23,40 @@
   </form>
 </div>
 
-<div id="project-topbar">
-  <ul class="nav nav-pills">
-    <li>
-      <a href="{% url 'projectbuilds' project.id %}">
-        Builds (<span class="total-builds">0</span>)
-      </a>
-    </li>
-    <li id="topbar-configuration-tab">
-      <a href="{% url 'project' project.id %}">
-        Configuration
-      </a>
-    </li>
-    <li>
-      <a href="{% url 'importlayer' project.id %}">
-        Import layer
-      </a>
-    </li>
-    <li class="pull-right">
-      <form class="form-inline" style="margin-bottom:0px;">
-        <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a semicolon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i>
-        <div class="input-append">
-          <input id="build-input" type="text" class="input-xlarge input-lg build-target-input" placeholder="Type the recipe you want to build" autocomplete="off" disabled>
-          <button id="build-button" class="btn btn-primary btn-large build-button" data-project-id="{{project.id}}" disabled>Build</button>
-        </div>
-      </form>
-    </li>
-  </ul>
-</div>
+{% if not project.is_default %}
+  <div id="project-topbar">
+    <ul class="nav nav-pills">
+      <li>
+        <a href="{% url 'projectbuilds' project.id %}">
+          Builds (<span class="total-builds">0</span>)
+        </a>
+      </li>
+      <li id="topbar-configuration-tab">
+        <a href="{% url 'project' project.id %}">
+          Configuration
+        </a>
+      </li>
+      <li>
+        <a href="{% url 'importlayer' project.id %}">
+          Import layer
+        </a>
+      </li>
+      {% if CUSTOM_IMAGE %}
+      <li>
+        <a href="{% url 'newcustomimage' project.id %}">
+          New custom image
+        </a>
+      </li>
+      {% endif %}
+      <li class="pull-right">
+        <form class="form-inline" style="margin-bottom:0px;">
+          <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a semicolon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i>
+          <div class="input-append">
+            <input id="build-input" type="text" class="input-xlarge input-lg build-target-input" placeholder="Type the recipe you want to build" autocomplete="off" disabled>
+            <button id="build-button" class="btn btn-primary btn-large build-button" data-project-id="{{project.id}}" disabled>Build</button>
+          </div>
+        </form>
+      </li>
+    </ul>
+  </div>
+{% endif %}
diff --git a/bitbake/lib/toaster/toastergui/templates/recipe.html b/bitbake/lib/toaster/toastergui/templates/recipe.html
index b5e4192..c6ae2f3 100644
--- a/bitbake/lib/toaster/toastergui/templates/recipe.html
+++ b/bitbake/lib/toaster/toastergui/templates/recipe.html
@@ -2,6 +2,7 @@
 
 {% load projecttags %}
 
+{% block title %} {{object.name}}_{{object.version}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
 <li>{{object.name}}_{{object.version}} </li>
diff --git a/bitbake/lib/toaster/toastergui/templates/recipe_btn.html b/bitbake/lib/toaster/toastergui/templates/recipe_btn.html
index 77c1b23..baab06e 100644
--- a/bitbake/lib/toaster/toastergui/templates/recipe_btn.html
+++ b/bitbake/lib/toaster/toastergui/templates/recipe_btn.html
@@ -1,7 +1,15 @@
-<button data-recipe-name="{{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.pk}} build-recipe-btn" style="display:none; margin-top: 5px;" >
+<button data-recipe-name="{{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.pk}} build-recipe-btn" style="margin-top: 5px;
+  {% if data.layer_version.pk not in extra.current_layers %}
+    display:none;
+  {% endif %}"
+ >
   Build recipe
 </button>
-<button class="btn btn-block layerbtn layer-add-{{data.layer_version.pk}}" data-layer='{ "id": {{data.layer_version.pk}}, "name":  "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add">
+<button class="btn btn-block layerbtn layer-add-{{data.layer_version.pk}}" data-layer='{ "id": {{data.layer_version.pk}}, "name":  "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add"
+    {% if data.layer_version.pk in extra.current_layers %}
+     style="display:none;"
+    {% endif %}
+>
   <i class="icon-plus"></i>
   Add layer
   <i title="" class="icon-question-sign get-help" data-original-title="To build this target, you must first add the {{data.layer_version.layer.name}} layer to your project"></i>
diff --git a/bitbake/lib/toaster/toastergui/templates/recipes.html b/bitbake/lib/toaster/toastergui/templates/recipes.html
index 5cdac43..d144893 100644
--- a/bitbake/lib/toaster/toastergui/templates/recipes.html
+++ b/bitbake/lib/toaster/toastergui/templates/recipes.html
@@ -2,6 +2,7 @@
 
 {% load projecttags %}
 
+{% block title %} Recipes - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li>Recipes</li>
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/target.html b/bitbake/lib/toaster/toastergui/templates/target.html
index 65e6c4a..4c33eaa 100644
--- a/bitbake/lib/toaster/toastergui/templates/target.html
+++ b/bitbake/lib/toaster/toastergui/templates/target.html
@@ -1,4 +1,5 @@
 {% extends "basebuildpage.html" %}
+{% block title %} Packages included - {{ target.target }} {{ target.build.machine }} - {{ target.build.project.name }} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li>{{target.target}}</li>
 {% endblock localbreadcrumb%}
diff --git a/bitbake/lib/toaster/toastergui/templates/task.html b/bitbake/lib/toaster/toastergui/templates/task.html
index 635098a..ef628d9 100644
--- a/bitbake/lib/toaster/toastergui/templates/task.html
+++ b/bitbake/lib/toaster/toastergui/templates/task.html
@@ -3,6 +3,7 @@
 {% load projecttags %}
 {% load humanize %}
 
+{% block title %} {{task.recipe.name}}_{{task.recipe.version}} {{task.task_name}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %}
 {% block localbreadcrumb %}
 <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
 <li>{{task.recipe.name}}_{{task.recipe.version}} {{task.task_name}}</li>
diff --git a/bitbake/lib/toaster/toastergui/templates/tasks.html b/bitbake/lib/toaster/toastergui/templates/tasks.html
index b18b5c7..353410f 100644
--- a/bitbake/lib/toaster/toastergui/templates/tasks.html
+++ b/bitbake/lib/toaster/toastergui/templates/tasks.html
@@ -1,33 +1,34 @@
 {% extends "basebuildpage.html" %}
 {% load projecttags %}
 
+{% block title %} {{mainheading}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster{% endblock %}
 {% block localbreadcrumb %}
-<li>{{title}}</li>
+<li>{{mainheading}}</li>
 {% endblock %}
 
 {% block nav-tasks %}
-  {% if 'Tasks' == title %}
+  {% if 'Tasks' == mainheading %}
     <li class="active"><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
   {% else %}
     <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li>
   {% endif %}
 {% endblock %}
 {% block nav-buildtime %}
-  {% if 'Time' == title %}
+  {% if 'Time' == mainheading %}
     <li class="active"><a href="{% url 'buildtime' build.pk %}">Time</a></li>
   {% else %}
     <li><a href="{% url 'buildtime' build.pk %}">Time</a></li>
   {% endif %}
 {% endblock %}
 {% block nav-cpuusage %}
-  {% if 'CPU usage' == title %}
+  {% if 'CPU usage' == mainheading %}
     <li class="active"><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li>
   {% else %}
     <li><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li>
   {% endif %}
 {% endblock %}
 {% block nav-diskio %}
-  {% if 'Disk I/O' == title %}
+  {% if 'Disk I/O' == mainheading %}
     <li class="active"><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
   {% else %}
     <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li>
@@ -39,7 +40,7 @@
 {% if not request.GET.filter and not request.GET.search and not objects.paginator.count %}
   <!-- Empty - no data in database -->
   <div class="page-header">
-  <h1>{{title}}</h1>
+  <h1>{{mainheading}}</h1>
   </div>
   <div class="alert alert-info lead">
   No data was recorded for this build.
@@ -54,7 +55,7 @@
   {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
       No tasks found
   {%else%}
-      {{title}}
+      {{mainheading}}
   {%endif%}
   </h1>
   </div>
diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable.html b/bitbake/lib/toaster/toastergui/templates/toastertable.html
index 9ef4c6f..98a715f 100644
--- a/bitbake/lib/toaster/toastergui/templates/toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/toastertable.html
@@ -12,7 +12,6 @@
       tableName : "{{table_name}}",
       url : "{{ xhr_table_url }}?format=json",
       title : "{{title}}",
-      projectLayers : {{projectlayers|json}},
     };
 
     try {
diff --git a/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html b/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
index b9f8fee..0301a6c 100644
--- a/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
+++ b/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html
@@ -3,6 +3,8 @@
 {% load humanize %}
 {% load static %}
 
+{% block title %} Build artifact no longer exists - Toaster {% endblock %}
+
 {% block pagecontent %}
 
 <div class="row-fluid air">
diff --git a/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py b/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py
new file mode 100644
index 0000000..04770ac
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py
@@ -0,0 +1,34 @@
+from django import template
+from django.core.urlresolvers import reverse
+
+register = template.Library()
+
+def project_url(parser, token):
+    """
+    Create a URL for a project's main page;
+    for non-default projects, this is the configuration page;
+    for the default project, this is the project builds page
+    """
+    try:
+        tag_name, project = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(
+            "%s tag requires exactly one argument" % tag_name
+        )
+    return ProjectUrlNode(project)
+
+class ProjectUrlNode(template.Node):
+    def __init__(self, project):
+        self.project = template.Variable(project)
+
+    def render(self, context):
+        try:
+            project = self.project.resolve(context)
+            if project.is_default:
+                return reverse('projectbuilds', args=(project.id,))
+            else:
+                return reverse('project', args=(project.id,))
+        except template.VariableDoesNotExist:
+            return ''
+
+register.tag('project_url', project_url)
diff --git a/bitbake/lib/toaster/toastergui/tests.py b/bitbake/lib/toaster/toastergui/tests.py
index 4d1549b..9e6c46a 100644
--- a/bitbake/lib/toaster/toastergui/tests.py
+++ b/bitbake/lib/toaster/toastergui/tests.py
@@ -22,15 +22,29 @@
 """Test cases for Toaster GUI and ReST."""
 
 from django.test import TestCase
+from django.test.client import RequestFactory
 from django.core.urlresolvers import reverse
 from django.utils import timezone
-from orm.models import Project, Release, BitbakeVersion, ProjectTarget
+
+from orm.models import Project, Release, BitbakeVersion, Package, LogMessage
 from orm.models import ReleaseLayerSourcePriority, LayerSource, Layer, Build
-from orm.models import Layer_Version, Recipe, Machine, ProjectLayer
+from orm.models import Layer_Version, Recipe, Machine, ProjectLayer, Target
+from orm.models import CustomImageRecipe, ProjectVariable
+from orm.models import Branch
+
+import toastermain
+
+from toastergui.tables import SoftwareRecipesTable
 import json
 from bs4 import BeautifulSoup
+import re
 
 PROJECT_NAME = "test project"
+CLI_BUILDS_PROJECT_NAME = 'Command line builds'
+
+# by default, tests are run in build mode; to run in analysis mode,
+# set this to False in individual test cases
+toastermain.settings.BUILD_MODE = True
 
 class ViewTests(TestCase):
     """Tests to verify view APIs."""
@@ -39,27 +53,58 @@
         bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
                                             branch="master", dirpath="")
         release = Release.objects.create(name="test release",
+                                         branch_name="master",
                                          bitbake_version=bbv)
         self.project = Project.objects.create_project(name=PROJECT_NAME,
                                                       release=release)
+        now = timezone.now()
+
+        build = Build.objects.create(project=self.project,
+                                     started_on=now,
+                                     completed_on=now)
+
         layersrc = LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED)
         self.priority = ReleaseLayerSourcePriority.objects.create(release=release,
                                                                   layer_source=layersrc)
         layer = Layer.objects.create(name="base-layer", layer_source=layersrc,
                                      vcs_url="/tmp/")
 
-        lver = Layer_Version.objects.create(layer=layer, project=self.project,
-                                            layer_source=layersrc, commit="master")
+        branch = Branch.objects.create(name="master", layer_source=layersrc)
 
-        Recipe.objects.create(layer_source=layersrc, name="base-recipe",
-                              version="1.2", summary="one recipe",
-                              description="recipe", layer_version=lver)
+        lver = Layer_Version.objects.create(layer=layer, project=self.project,
+                                            layer_source=layersrc, commit="master",
+                                            up_branch=branch)
+
+        self.recipe1 = Recipe.objects.create(layer_source=layersrc,
+                                             name="base-recipe",
+                                             version="1.2",
+                                             summary="one recipe",
+                                             description="recipe",
+                                             layer_version=lver)
 
         Machine.objects.create(layer_version=lver, name="wisk",
                                description="wisking machine")
 
         ProjectLayer.objects.create(project=self.project, layercommit=lver)
 
+
+        self.customr = CustomImageRecipe.objects.create(\
+                           name="custom recipe", project=self.project,
+                           base_recipe=self.recipe1)
+
+        self.package = Package.objects.create(name='pkg1', recipe=self.recipe1,
+                                              build=build)
+
+
+        # recipe with project for testing AvailableRecipe table
+        self.recipe2 = Recipe.objects.create(layer_source=layersrc,
+                                             name="fancy-recipe",
+                                             version="1.4",
+                                             summary="a fancy recipe",
+                                             description="fancy recipe",
+                                             layer_version=lver,
+                                             file_path='/home/foo')
+
         self.assertTrue(lver in self.project.compatible_layerversions())
 
     def test_get_base_call_returns_html(self):
@@ -181,6 +226,140 @@
         data = json.loads(response.content)
         self.assertNotEqual(data["error"], "ok")
 
+    def test_custom_ok(self):
+        """Test successful return from ReST API xhr_customrecipe"""
+        url = reverse('xhr_customrecipe')
+        params = {'name': 'custom', 'project': self.project.id,
+                  'base': self.recipe1.id}
+        response = self.client.post(url, params)
+        self.assertEqual(response.status_code, 200)
+        data = json.loads(response.content)
+        self.assertEqual(data['error'], 'ok')
+        self.assertTrue('url' in data)
+        # get recipe from the database
+        recipe = CustomImageRecipe.objects.get(project=self.project,
+                                               name=params['name'])
+        args = (self.project.id, recipe.id,)
+        self.assertEqual(reverse('customrecipe', args=args), data['url'])
+
+    def test_custom_incomplete_params(self):
+        """Test not passing all required parameters to xhr_customrecipe"""
+        url = reverse('xhr_customrecipe')
+        for params in [{}, {'name': 'custom'},
+                       {'name': 'custom', 'project': self.project.id}]:
+            response = self.client.post(url, params)
+            self.assertEqual(response.status_code, 200)
+            data = json.loads(response.content)
+            self.assertNotEqual(data["error"], "ok")
+
+    def test_xhr_custom_wrong_project(self):
+        """Test passing wrong project id to xhr_customrecipe"""
+        url = reverse('xhr_customrecipe')
+        params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
+        response = self.client.post(url, params)
+        self.assertEqual(response.status_code, 200)
+        data = json.loads(response.content)
+        self.assertNotEqual(data["error"], "ok")
+
+    def test_xhr_custom_wrong_base(self):
+        """Test passing wrong base recipe id to xhr_customrecipe"""
+        url = reverse('xhr_customrecipe')
+        params = {'name': 'custom', 'project': self.project.id, "base": 0}
+        response = self.client.post(url, params)
+        self.assertEqual(response.status_code, 200)
+        data = json.loads(response.content)
+        self.assertNotEqual(data["error"], "ok")
+
+    def test_xhr_custom_details(self):
+        """Test getting custom recipe details"""
+        name = "custom recipe"
+        url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        expected = {"error": "ok",
+                    "info": {'id': self.customr.id,
+                             'name': name,
+                             'base_recipe_id': self.recipe1.id,
+                             'project_id': self.project.id,
+                            }
+                   }
+        self.assertEqual(json.loads(response.content), expected)
+
+    def test_xhr_custom_del(self):
+        """Test deleting custom recipe"""
+        name = "to be deleted"
+        recipe = CustomImageRecipe.objects.create(\
+                     name=name, project=self.project,
+                     base_recipe=self.recipe1)
+        url = reverse('xhr_customrecipe_id', args=(recipe.id,))
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(json.loads(response.content), {"error": "ok"})
+        # try to delete not-existent recipe
+        url = reverse('xhr_customrecipe_id', args=(recipe.id,))
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertNotEqual(json.loads(response.content)["error"], "ok")
+
+    def test_xhr_custom_packages(self):
+        """Test adding and deleting package to a custom recipe"""
+        url = reverse('xhr_customrecipe_packages',
+                      args=(self.customr.id, self.package.id))
+        # add self.package1 to recipe
+        response = self.client.put(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(json.loads(response.content), {"error": "ok"})
+        self.assertEqual(self.customr.packages.all()[0].id, self.package.id)
+        # delete it
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(json.loads(response.content), {"error": "ok"})
+        self.assertFalse(self.customr.packages.all())
+        # delete it again to test error condition
+        response = self.client.delete(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertNotEqual(json.loads(response.content)["error"], "ok")
+
+    def test_xhr_custom_packages_err(self):
+        """Test error conditions of xhr_customrecipe_packages"""
+        # test calls with wrong recipe id and wrong package id
+        for args in [(0, self.package.id), (self.customr.id, 0)]:
+            url = reverse('xhr_customrecipe_packages', args=args)
+            # test put and delete methods
+            for method in (self.client.put, self.client.delete):
+                response = method(url)
+                self.assertEqual(response.status_code, 200)
+                self.assertNotEqual(json.loads(response.content),
+                                    {"error": "ok"})
+
+    def test_software_recipes_table(self):
+        """Test structure returned for Software RecipesTable"""
+        table = SoftwareRecipesTable()
+        request = RequestFactory().get('/foo/', {'format': 'json'})
+        response = table.get(request, pid=self.project.id)
+        data = json.loads(response.content)
+
+        rows = data['rows']
+        row1 = next(x for x in rows if x['name'] == self.recipe1.name)
+        row2 = next(x for x in rows if x['name'] == self.recipe2.name)
+
+        self.assertEqual(response.status_code, 200, 'should be 200 OK status')
+        self.assertEqual(len(rows), 2, 'should be 2 recipes')
+
+        # check other columns have been populated correctly
+        self.assertEqual(row1['name'], self.recipe1.name)
+        self.assertEqual(row1['version'], self.recipe1.version)
+        self.assertEqual(row1['get_description_or_summary'],
+                         self.recipe1.description)
+        self.assertEqual(row1['layer_version__layer__name'],
+                         self.recipe1.layer_version.layer.name)
+        self.assertEqual(row2['name'], self.recipe2.name)
+        self.assertEqual(row2['version'], self.recipe2.version)
+        self.assertEqual(row2['get_description_or_summary'],
+                         self.recipe2.description)
+        self.assertEqual(row2['layer_version__layer__name'],
+                         self.recipe2.layer_version.layer.name)
+
 class LandingPageTests(TestCase):
     """ Tests for redirects on the landing page """
     # disable bogus pylint message error:
@@ -255,18 +434,48 @@
         self.assertTrue('/builds' in response.url,
                         'should redirect to builds')
 
-class ProjectsPageTests(TestCase):
-    """ Tests for projects page """
+class AllProjectsPageTests(TestCase):
+    """ Tests for projects page /projects/ """
 
-    PROJECT_NAME = 'cli builds'
+    MACHINE_NAME = 'delorean'
 
     def setUp(self):
         """ Add default project manually """
-        project = Project.objects.create_project(self.PROJECT_NAME, None)
+        project = Project.objects.create_project(CLI_BUILDS_PROJECT_NAME, None)
         self.default_project = project
         self.default_project.is_default = True
         self.default_project.save()
 
+        # this project is only set for some of the tests
+        self.project = None
+
+        self.release = None
+
+    def _add_build_to_default_project(self):
+        """ Add a build to the default project (not used in all tests) """
+        now = timezone.now()
+        build = Build.objects.create(project=self.default_project,
+                                     started_on=now,
+                                     completed_on=now)
+        build.save()
+
+    def _add_non_default_project(self):
+        """ Add another project """
+        bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
+                                            branch="master", dirpath="")
+        self.release = Release.objects.create(name="test release",
+                                              branch_name="master",
+                                              bitbake_version=bbv)
+        self.project = Project.objects.create_project(PROJECT_NAME, self.release)
+        self.project.is_default = False
+        self.project.save()
+
+        # fake the MACHINE variable
+        project_var = ProjectVariable.objects.create(project=self.project,
+                                                     name='MACHINE',
+                                                     value=self.MACHINE_NAME)
+        project_var.save()
+
     def test_default_project_hidden(self):
         """ The default project should be hidden if it has no builds """
         params = {"count": 10, "orderby": "updated:-", "page": 1}
@@ -274,26 +483,116 @@
 
         self.assertTrue(not('tr class="data"' in response.content),
                         'should be no project rows in the page')
-        self.assertTrue(not(self.PROJECT_NAME in response.content),
+        self.assertTrue(not(CLI_BUILDS_PROJECT_NAME in response.content),
                         'default project "cli builds" should not be in page')
 
     def test_default_project_has_build(self):
         """ The default project should be shown if it has builds """
-        now = timezone.now()
-        build = Build.objects.create(project=self.default_project,
-                                     started_on=now,
-                                     completed_on=now)
-        build.save()
+        self._add_build_to_default_project()
 
         params = {"count": 10, "orderby": "updated:-", "page": 1}
         response = self.client.get(reverse('all-projects'), params)
 
         self.assertTrue('tr class="data"' in response.content,
                         'should be a project row in the page')
-        self.assertTrue(self.PROJECT_NAME in response.content,
+        self.assertTrue(CLI_BUILDS_PROJECT_NAME in response.content,
                         'default project "cli builds" should be in page')
 
-class ProjectBuildsDisplayTest(TestCase):
+    def test_default_project_release(self):
+        """
+        The release for the default project should display as
+        'Not applicable'
+        """
+        # need a build, otherwise project doesn't display at all
+        self._add_build_to_default_project()
+
+        # another project to test, which should show release
+        self._add_non_default_project()
+
+        response = self.client.get(reverse('all-projects'), follow=True)
+        soup = BeautifulSoup(response.content)
+
+        # check the release cell for the default project
+        attrs = {'data-project': str(self.default_project.id)}
+        rows = soup.find_all('tr', attrs=attrs)
+        self.assertEqual(len(rows), 1, 'should be one row for default project')
+        cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
+        self.assertEqual(len(cells), 1, 'should be one release cell')
+        text = cells[0].select('span.muted')[0].text
+        self.assertEqual(text, 'Not applicable',
+                         'release should be not applicable for default project')
+
+        # check the link in the release cell for the other project
+        attrs = {'data-project': str(self.project.id)}
+        rows = soup.find_all('tr', attrs=attrs)
+        cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
+        text = cells[0].select('a')[0].text
+        self.assertEqual(text, self.release.name,
+                         'release name should be shown for non-default project')
+
+    def test_default_project_machine(self):
+        """
+        The machine for the default project should display as
+        'Not applicable'
+        """
+        # need a build, otherwise project doesn't display at all
+        self._add_build_to_default_project()
+
+        # another project to test, which should show machine
+        self._add_non_default_project()
+
+        response = self.client.get(reverse('all-projects'), follow=True)
+        soup = BeautifulSoup(response.content)
+
+        # check the machine cell for the default project
+        attrs = {'data-project': str(self.default_project.id)}
+        rows = soup.find_all('tr', attrs=attrs)
+        self.assertEqual(len(rows), 1, 'should be one row for default project')
+        cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
+        self.assertEqual(len(cells), 1, 'should be one machine cell')
+        text = cells[0].select('span.muted')[0].text
+        self.assertEqual(text, 'Not applicable',
+                         'machine should be not applicable for default project')
+
+        # check the link in the machine cell for the other project
+        attrs = {'data-project': str(self.project.id)}
+        rows = soup.find_all('tr', attrs=attrs)
+        cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
+        text = cells[0].select('a')[0].text
+        self.assertEqual(text, self.MACHINE_NAME,
+                         'machine name should be shown for non-default project')
+
+    def test_project_page_links(self):
+        """
+        Test that links for the default project point to the builds
+        page /projects/X/builds for that project, and that links for
+        other projects point to their configuration pages /projects/X/
+        """
+
+        # need a build, otherwise project doesn't display at all
+        self._add_build_to_default_project()
+
+        # another project to test, which should show machine
+        self._add_non_default_project()
+
+        response = self.client.get(reverse('all-projects'), follow=True)
+        soup = BeautifulSoup(response.content)
+
+        # link for default project
+        row = soup.find('tr', attrs={'data-project': self.default_project.id})
+        cell = row.find('td', attrs={'data-project-field': 'name'})
+        expected_url = reverse('projectbuilds', args=(self.default_project.id,))
+        self.assertEqual(cell.find('a')['href'], expected_url,
+                         'link on default project name should point to builds')
+
+        # link for other project
+        row = soup.find('tr', attrs={'data-project': self.project.id})
+        cell = row.find('td', attrs={'data-project-field': 'name'})
+        expected_url = reverse('project', args=(self.project.id,))
+        self.assertEqual(cell.find('a')['href'], expected_url,
+                         'link on project name should point to configuration')
+
+class ProjectBuildsPageTests(TestCase):
     """ Test data at /project/X/builds is displayed correctly """
 
     def setUp(self):
@@ -303,8 +602,18 @@
                                          bitbake_version=bbv)
         self.project1 = Project.objects.create_project(name=PROJECT_NAME,
                                                        release=release)
+        self.project1.save()
+
         self.project2 = Project.objects.create_project(name=PROJECT_NAME,
                                                        release=release)
+        self.project2.save()
+
+        self.default_project = Project.objects.create_project(
+            name=CLI_BUILDS_PROJECT_NAME,
+            release=release
+        )
+        self.default_project.is_default = True
+        self.default_project.save()
 
         # parameters for builds to associate with the projects
         now = timezone.now()
@@ -338,6 +647,7 @@
         }
 
     def _get_rows_for_project(self, project_id):
+        """ Helper to retrieve HTML rows for a project """
         url = reverse("projectbuilds", args=(project_id,))
         response = self.client.get(url, follow=True)
         soup = BeautifulSoup(response.content)
@@ -345,35 +655,273 @@
 
     def test_show_builds_for_project(self):
         """ Builds for a project should be displayed """
-        build1a = Build.objects.create(**self.project1_build_success)
-        build1b = Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
         build_rows = self._get_rows_for_project(self.project1.id)
         self.assertEqual(len(build_rows), 2)
 
-    def test_show_builds_for_project_only(self):
+    def test_show_builds_project_only(self):
         """ Builds for other projects should be excluded """
-        build1a = Build.objects.create(**self.project1_build_success)
-        build1b = Build.objects.create(**self.project1_build_success)
-        build1c = Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
 
         # shouldn't see these two
-        build2a = Build.objects.create(**self.project2_build_success)
-        build2b = Build.objects.create(**self.project2_build_in_progress)
+        Build.objects.create(**self.project2_build_success)
+        Build.objects.create(**self.project2_build_in_progress)
 
         build_rows = self._get_rows_for_project(self.project1.id)
         self.assertEqual(len(build_rows), 3)
 
-    def test_show_builds_exclude_in_progress(self):
+    def test_builds_exclude_in_progress(self):
         """ "in progress" builds should not be shown """
-        build1a = Build.objects.create(**self.project1_build_success)
-        build1b = Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
+        Build.objects.create(**self.project1_build_success)
 
         # shouldn't see this one
-        build1c = Build.objects.create(**self.project1_build_in_progress)
+        Build.objects.create(**self.project1_build_in_progress)
 
         # shouldn't see these two either, as they belong to a different project
-        build2a = Build.objects.create(**self.project2_build_success)
-        build2b = Build.objects.create(**self.project2_build_in_progress)
+        Build.objects.create(**self.project2_build_success)
+        Build.objects.create(**self.project2_build_in_progress)
 
         build_rows = self._get_rows_for_project(self.project1.id)
-        self.assertEqual(len(build_rows), 2)
\ No newline at end of file
+        self.assertEqual(len(build_rows), 2)
+
+    def test_tasks_in_projectbuilds(self):
+        """ Task should be shown as suffix on build name """
+        build = Build.objects.create(**self.project1_build_success)
+        Target.objects.create(build=build, target='bash', task='clean')
+        url = reverse("projectbuilds", args=(self.project1.id,))
+        response = self.client.get(url, follow=True)
+        result = re.findall('^ +bash:clean$', response.content, re.MULTILINE)
+        self.assertEqual(len(result), 2)
+
+    def test_cli_builds_hides_tabs(self):
+        """
+        Display for command line builds should hide tabs;
+        note that the latest builds section is already tested in
+        AllBuildsPageTests, as the template is the same
+        """
+        url = reverse("projectbuilds", args=(self.default_project.id,))
+        response = self.client.get(url, follow=True)
+        soup = BeautifulSoup(response.content)
+        tabs = soup.select('#project-topbar')
+        self.assertEqual(len(tabs), 0,
+                         'should be no top bar shown for command line builds')
+
+    def test_non_cli_builds_has_tabs(self):
+        """
+        Non-command-line builds projects should show the tabs
+        """
+        url = reverse("projectbuilds", args=(self.project1.id,))
+        response = self.client.get(url, follow=True)
+        soup = BeautifulSoup(response.content)
+        tabs = soup.select('#project-topbar')
+        self.assertEqual(len(tabs), 1,
+                         'should be a top bar shown for non-command-line builds')
+
+class AllBuildsPageTests(TestCase):
+    """ Tests for all builds page /builds/ """
+
+    def setUp(self):
+        bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
+                                            branch="master", dirpath="")
+        release = Release.objects.create(name="release1",
+                                         bitbake_version=bbv)
+        self.project1 = Project.objects.create_project(name=PROJECT_NAME,
+                                                       release=release)
+        self.default_project = Project.objects.create_project(
+            name=CLI_BUILDS_PROJECT_NAME,
+            release=release
+        )
+        self.default_project.is_default = True
+        self.default_project.save()
+
+        # parameters for builds to associate with the projects
+        now = timezone.now()
+
+        self.project1_build_success = {
+            "project": self.project1,
+            "started_on": now,
+            "completed_on": now,
+            "outcome": Build.SUCCEEDED
+        }
+
+        self.default_project_build_success = {
+            "project": self.default_project,
+            "started_on": now,
+            "completed_on": now,
+            "outcome": Build.SUCCEEDED
+        }
+
+    def test_show_tasks_in_allbuilds(self):
+        """ Task should be shown as suffix on build name """
+        build = Build.objects.create(**self.project1_build_success)
+        Target.objects.create(build=build, target='bash', task='clean')
+        url = reverse('all-builds')
+        response = self.client.get(url, follow=True)
+        result = re.findall('bash:clean', response.content, re.MULTILINE)
+        self.assertEqual(len(result), 3)
+
+    def test_no_run_again_for_cli_build(self):
+        """ "Run again" button should not be shown for command-line builds """
+        build = Build.objects.create(**self.default_project_build_success)
+        url = reverse('all-builds')
+        response = self.client.get(url, follow=True)
+        soup = BeautifulSoup(response.content)
+
+        attrs = {'data-latest-build-result': build.id}
+        result = soup.find('div', attrs=attrs)
+
+        # shouldn't see a run again button for command-line builds
+        run_again_button = result.select('button')
+        self.assertEqual(len(run_again_button), 0)
+
+        # should see a help icon for command-line builds
+        help_icon = result.select('i.get-help-green')
+        self.assertEqual(len(help_icon), 1)
+
+    def test_tooltips_on_project_name(self):
+        """
+        A tooltip should be present next to the command line
+        builds project name in the all builds page, but not for
+        other projects
+        """
+        build1 = Build.objects.create(**self.project1_build_success)
+        default_build = Build.objects.create(**self.default_project_build_success)
+
+        url = reverse('all-builds')
+        response = self.client.get(url, follow=True)
+        soup = BeautifulSoup(response.content)
+
+        # no help icon on non-default project name
+        result = soup.find('tr', attrs={'data-table-build-result': build1.id})
+        name = result.select('td.project-name')[0]
+        icons = name.select('i.get-help')
+        self.assertEqual(len(icons), 0,
+                         'should not be a help icon for non-cli builds name')
+
+        # help icon on default project name
+        result = soup.find('tr', attrs={'data-table-build-result': default_build.id})
+        name = result.select('td.project-name')[0]
+        icons = name.select('i.get-help')
+        self.assertEqual(len(icons), 1,
+                         'should be a help icon for cli builds name')
+
+class ProjectPageTests(TestCase):
+    """ Test project data at /project/X/ is displayed correctly """
+    CLI_BUILDS_PROJECT_NAME = 'Command line builds'
+
+    def test_command_line_builds_in_progress(self):
+        """
+        In progress builds should not cause an error to be thrown
+        when navigating to "command line builds" project page;
+        see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
+        """
+
+        # add the "command line builds" default project; this mirrors what
+        # we do in migration 0026_set_default_project.py
+        default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
+        default_project.is_default = True
+        default_project.save()
+
+        # add an "in progress" build for the default project
+        now = timezone.now()
+        build = Build.objects.create(project=default_project,
+                                     started_on=now,
+                                     completed_on=now,
+                                     outcome=Build.IN_PROGRESS)
+
+        # navigate to the project page for the default project
+        url = reverse("project", args=(default_project.id,))
+        response = self.client.get(url, follow=True)
+
+        self.assertEqual(response.status_code, 200)
+
+class BuildDashboardTests(TestCase):
+    """ Tests for the build dashboard /build/X """
+
+    def setUp(self):
+        bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
+                                            branch="master", dirpath="")
+        release = Release.objects.create(name="release1",
+                                         bitbake_version=bbv)
+        project = Project.objects.create_project(name=PROJECT_NAME,
+                                                 release=release)
+
+        now = timezone.now()
+
+        self.build1 = Build.objects.create(project=project,
+                                           started_on=now,
+                                           completed_on=now)
+
+        # exception
+        msg1 = 'an exception was thrown'
+        self.exception_message = LogMessage.objects.create(
+            build=self.build1,
+            level=LogMessage.EXCEPTION,
+            message=msg1
+        )
+
+        # critical
+        msg2 = 'a critical error occurred'
+        self.critical_message = LogMessage.objects.create(
+            build=self.build1,
+            level=LogMessage.CRITICAL,
+            message=msg2
+        )
+
+    def _get_build_dashboard_errors(self):
+        """
+        Get a list of HTML fragments representing the errors on the
+        build dashboard
+        """
+        url = reverse('builddashboard', args=(self.build1.id,))
+        response = self.client.get(url)
+        soup = BeautifulSoup(response.content)
+        return soup.select('#errors div.alert-error')
+
+    def _check_for_log_message(self, log_message):
+        """
+        Check whether the LogMessage instance <log_message> is
+        represented as an HTML error in the build dashboard page
+        """
+        errors = self._get_build_dashboard_errors()
+        self.assertEqual(len(errors), 2)
+
+        expected_text = log_message.message
+        expected_id = str(log_message.id)
+
+        found = False
+        for error in errors:
+            error_text = error.find('pre').text
+            text_matches = (error_text == expected_text)
+
+            error_id = error['data-error']
+            id_matches = (error_id == expected_id)
+
+            if text_matches and id_matches:
+                found = True
+                break
+
+        template_vars = (expected_text, error_text,
+                         expected_id, error_id)
+        assertion_error_msg = 'exception not found as error: ' \
+            'expected text "%s" and got "%s"; ' \
+            'expected ID %s and got %s' % template_vars
+        self.assertTrue(found, assertion_error_msg)
+
+    def test_exceptions_show_as_errors(self):
+        """
+        LogMessages with level EXCEPTION should display in the errors
+        section of the page
+        """
+        self._check_for_log_message(self.exception_message)
+
+    def test_criticals_show_as_errors(self):
+        """
+        LogMessages with level CRITICAL should display in the errors
+        section of the page
+        """
+        self._check_for_log_message(self.critical_message)
diff --git a/bitbake/lib/toaster/toastergui/typeaheads.py b/bitbake/lib/toaster/toastergui/typeaheads.py
index d5bec58..dd4b7f5 100644
--- a/bitbake/lib/toaster/toastergui/typeaheads.py
+++ b/bitbake/lib/toaster/toastergui/typeaheads.py
@@ -27,7 +27,7 @@
       super(LayersTypeAhead, self).__init__()
 
     def apply_search(self, search_term, prj, request):
-        layers = prj.compatible_layerversions()
+        layers = prj.get_all_compatible_layer_versions()
         layers = layers.order_by('layer__name')
 
         # Unlike the other typeaheads we also don't want to show suggestions
@@ -35,7 +35,8 @@
         # layerdeps to a new layer.
         if ("include_added" in request.GET and
                 request.GET['include_added'] != "true"):
-            layers = layers.exclude(pk__in=prj.projectlayer_equivalent_set)
+            layers = layers.exclude(
+                pk__in=prj.get_project_layer_versions(pk=True))
 
         primary_results = layers.filter(layer__name__istartswith=search_term)
         secondary_results = layers.filter(layer__name__icontains=search_term).exclude(pk__in=primary_results)
@@ -120,12 +121,12 @@
         return results
 
 class ProjectsTypeAhead(ToasterTypeAhead):
-    """ Typeahead for all the projects """
+    """ Typeahead for all the projects, except for command line builds """
     def __init__(self):
         super(ProjectsTypeAhead, self).__init__()
 
     def apply_search(self, search_term, prj, request):
-        projects = Project.objects.all().order_by("name")
+        projects = Project.objects.exclude(is_default=True).order_by("name")
 
         primary_results = projects.filter(name__istartswith=search_term)
         secondary_results = projects.filter(name__icontains=search_term).exclude(pk__in=primary_results)
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 46e5761..a1adbb7 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -87,28 +87,29 @@
         # the table pages that have been converted to ToasterTable widget
         url(r'^project/(?P<pid>\d+)/machines/$',
             tables.MachinesTable.as_view(template_name="generic-toastertable-page.html"),
-            { 'table_name': tables.MachinesTable.__name__.lower(),
-              'title' : 'Compatible machines' },
             name="projectmachines"),
 
-        url(r'^project/(?P<pid>\d+)/recipes/$',
-            tables.RecipesTable.as_view(template_name="generic-toastertable-page.html"),
-            { 'table_name': tables.RecipesTable.__name__.lower(),
-              'title' : 'Compatible recipes' },
-            name="projecttargets"),
+        url(r'^project/(?P<pid>\d+)/softwarerecipes/$',
+            tables.SoftwareRecipesTable.as_view(template_name="generic-toastertable-page.html"),
+            name="projectsoftwarerecipes"),
 
-        url(r'^project/(?P<pid>\d+)/availablerecipes/$',
-            tables.ProjectLayersRecipesTable.as_view(template_name="generic-toastertable-page.html"),
-            { 'table_name': tables.ProjectLayersRecipesTable.__name__.lower(),
-              'title' : 'Recipes available for layers in the current project' },
-            name="projectavailabletargets"),
+        url(r'^project/(?P<pid>\d+)/images/$',
+            tables.ImageRecipesTable.as_view(template_name="generic-toastertable-page.html"), name="projectimagerecipes"),
+
+        url(r'^project/(?P<pid>\d+)/customimages/$',
+            tables.CustomImagesTable.as_view(template_name="generic-toastertable-page.html"), name="projectcustomimages"),
+
+        url(r'^project/(?P<pid>\d+)/newcustomimage/$',
+            tables.NewCustomImagesTable.as_view(template_name="newcustomimage.html"),
+            name="newcustomimage"),
+
 
         url(r'^project/(?P<pid>\d+)/layers/$',
             tables.LayersTable.as_view(template_name="generic-toastertable-page.html"),
-            { 'table_name': tables.LayersTable.__name__.lower(),
-              'title' : 'Compatible layers' },
             name="projectlayers"),
 
+
+
         url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$',
             'layerdetails', name='layerdetails'),
 
@@ -125,6 +126,16 @@
             name=tables.LayerMachinesTable.__name__.lower()),
 
 
+        url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipeid>\d+)/selectpackages/$',
+            tables.SelectPackagesTable.as_view(template_name="generic-toastertable-page.html"), name="recipeselectpackages"),
+
+
+        url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipe_id>\d+)$',
+            'customrecipe',
+            name="customrecipe"),
+
+
+
         # typeahead api end points
         url(r'^xhr_typeahead/(?P<pid>\d+)/layers$',
             typeaheads.LayersTypeAhead.as_view(), name='xhr_layerstypeahead'),
@@ -148,6 +159,14 @@
         # JS Unit tests
         url(r'^js-unit-tests/$', 'jsunittests', name='js-unit-tests'),
 
-        # default redirection
+        # image customisation functionality
+        url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/(?P<package_id>\d+|)$',
+            'xhr_customrecipe_packages', name='xhr_customrecipe_packages'),
+        url(r'^xhr_customrecipe/(?P<recipe_id>\d+)$', 'xhr_customrecipe_id',
+            name='xhr_customrecipe_id'),
+        url(r'^xhr_customrecipe/', 'xhr_customrecipe',
+            name='xhr_customrecipe'),
+
+          # default redirection
         url(r'^$', RedirectView.as_view( url= 'landing')),
 )
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 8689a12..0e255f1 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -26,12 +26,12 @@
 import operator,re
 
 from django.db.models import F, Q, Sum, Count, Max
-from django.db import IntegrityError
+from django.db import IntegrityError, Error
 from django.shortcuts import render, redirect
 from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe, LogMessage, Variable
 from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File, Package_Dependency
 from orm.models import Target_Installed_Package, Target_File, Target_Image_File, BuildArtifact
-from orm.models import BitbakeVersion
+from orm.models import BitbakeVersion, CustomImageRecipe
 from bldcontrol import bbcontroller
 from django.views.decorators.cache import cache_control
 from django.core.urlresolvers import reverse, resolve
@@ -45,32 +45,50 @@
 from toastergui.templatetags.projecttags import json as jsonfilter
 import json
 from os.path import dirname
+from functools import wraps
 import itertools
+import mimetypes
 
-import magic
 import logging
 
 logger = logging.getLogger("toaster")
 
 class MimeTypeFinder(object):
-    _magic = magic.Magic(flags = magic.MAGIC_MIME_TYPE)
+    # setting this to False enables additional non-standard mimetypes
+    # to be included in the guess
+    _strict = False
 
-    # returns the mimetype for a file path
+    # returns the mimetype for a file path as a string,
+    # or 'application/octet-stream' if the type couldn't be guessed
     @classmethod
     def get_mimetype(self, path):
-        return self._magic.id_filename(path)
+        guess = mimetypes.guess_type(path, self._strict)
+        guessed_type = guess[0]
+        if guessed_type == None:
+            guessed_type = 'application/octet-stream'
+        return guessed_type
 
 # all new sessions should come through the landing page;
 # determine in which mode we are running in, and redirect appropriately
 def landing(request):
+    # in build mode, we redirect to the command-line builds page
+    # if there are any builds for the default (cli builds) project
+    default_project = Project.objects.get_default_project()
+    default_project_builds = Build.objects.filter(project = default_project)
+
+    if (not toastermain.settings.BUILD_MODE) and default_project_builds.count() > 0:
+        args = (default_project.id,)
+        return redirect(reverse('projectbuilds', args = args), permanent = False)
+
     # we only redirect to projects page if there is a user-generated project
+    num_builds = Build.objects.all().count()
     user_projects = Project.objects.filter(is_default = False)
     has_user_project = user_projects.count() > 0
 
-    if Build.objects.count() == 0 and has_user_project:
+    if num_builds == 0 and has_user_project:
         return redirect(reverse('all-projects'), permanent = False)
 
-    if Build.objects.all().count() > 0:
+    if num_builds > 0:
         return redirect(reverse('all-builds'), permanent = False)
 
     context = {'lvs_nos' : Layer_Version.objects.all().count()}
@@ -84,9 +102,12 @@
     if prj is not None:
         queryset = queryset.filter(project = prj)
 
+    if not toastermain.settings.BUILD_MODE:
+        queryset = queryset.exclude(project__is_default=False)
+
     return list(itertools.chain(
-        queryset.filter(outcome=Build.IN_PROGRESS).order_by("-pk"),
-        queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-pk")[:3] ))
+        queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
+        queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] ))
 
 
 # a JSON-able dict of recent builds; for use in the Project page, xhr_ updates,  and other places, as needed
@@ -1215,7 +1236,7 @@
     context = { 'objectname': variant,
                 'object_search_display': object_search_display,
                 'filter_search_display': filter_search_display,
-                'title': title_variant,
+                'mainheading': title_variant,
                 'build': build,
                 'objects': task_objects,
                 'default_orderby' : orderby,
@@ -1862,11 +1883,21 @@
     return redirect(builds)
     # the context processor that supplies data used across all the pages
 
-
+# a context processor which runs on every request; this provides the
+# projects and non_cli_projects (i.e. projects created by the user)
+# variables referred to in templates, which used to determine the
+# visibility of UI elements like the "New build" button
 def managedcontextprocessor(request):
+    projects = Project.objects.all()
     ret = {
-        "projects": Project.objects.all(),
+        "projects": projects,
+        "non_cli_projects": projects.exclude(is_default=True),
         "DEBUG" : toastermain.settings.DEBUG,
+
+        # True if Toaster is in build mode, False otherwise
+        "BUILD_MODE": toastermain.settings.BUILD_MODE,
+
+        "CUSTOM_IMAGE" : toastermain.settings.CUSTOM_IMAGE,
         "TOASTER_BRANCH": toastermain.settings.TOASTER_BRANCH,
         "TOASTER_REVISION" : toastermain.settings.TOASTER_REVISION,
     }
@@ -1908,6 +1939,11 @@
 
         queryset = Build.objects.all()
 
+        # if in analysis mode, exclude builds for all projects except
+        # command line builds
+        if not toastermain.settings.BUILD_MODE:
+            queryset = queryset.exclude(project__is_default=False)
+
         redirect_page = resolve(request.path_info).url_name
 
         context, pagesize, orderby = _build_list_helper(request,
@@ -1982,7 +2018,7 @@
         build_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
 
         # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
-        build_mru = Build.objects.order_by("-started_on")[:3]
+        build_mru = _get_latest_builds()[:3]
 
         # calculate the exact begining of local today and yesterday, append context
         context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'})
@@ -2101,35 +2137,38 @@
                     },
                     {'name': 'Errors', 'clclass': 'errors_no',
                      'qhelp': "How many errors were encountered during the build (if any)",
-                     'orderfield': _get_toggle_order(request, "errors_no", True),
-                     'ordericon':_get_toggle_order_icon(request, "errors_no"),
-                     'orderkey' : 'errors_no',
-                     'filter' : {'class' : 'errors_no',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()),
-                                             ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()),
-                                             ]
-                                }
+                     # Comment out sorting and filter until YOCTO #8131 is fixed
+                     #'orderfield': _get_toggle_order(request, "errors_no", True),
+                     #'ordericon':_get_toggle_order_icon(request, "errors_no"),
+                     #'orderkey' : 'errors_no',
+                     #'filter' : {'class' : 'errors_no',
+                     #            'label': 'Show:',
+                     #            'options' : [
+                     #                        ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()),
+                     #                        ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()),
+                     #                        ]
+                     #           }
                     },
                     {'name': 'Warnings', 'clclass': 'warnings_no',
                      'qhelp': "How many warnings were encountered during the build (if any)",
-                     'orderfield': _get_toggle_order(request, "warnings_no", True),
-                     'ordericon':_get_toggle_order_icon(request, "warnings_no"),
-                     'orderkey' : 'warnings_no',
-                     'filter' : {'class' : 'warnings_no',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()),
-                                             ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()),
-                                             ]
-                                }
+                     # Comment out sorting and filter until YOCTO #8131 is fixed
+                     #'orderfield': _get_toggle_order(request, "warnings_no", True),
+                     #'ordericon':_get_toggle_order_icon(request, "warnings_no"),
+                     #'orderkey' : 'warnings_no',
+                     #'filter' : {'class' : 'warnings_no',
+                     #            'label': 'Show:',
+                     #            'options' : [
+                     #                        ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()),
+                     #                        ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()),
+                     #                        ]
+                     #           }
                     },
                     {'name': 'Time', 'clclass': 'time', 'hidden' : 1,
                      'qhelp': "How long it took the build to finish",
-                     'orderfield': _get_toggle_order(request, "timespent", True),
-                     'ordericon':_get_toggle_order_icon(request, "timespent"),
-                     'orderkey' : 'timespent',
+                     # Comment out sorting until YOCTO #8131 is fixed
+                     #'orderfield': _get_toggle_order(request, "timespent", True),
+                     #'ordericon':_get_toggle_order_icon(request, "timespent"),
+                     #'orderkey' : 'timespent',
                     },
                     {'name': 'Image files', 'clclass': 'output',
                      'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory",
@@ -2313,21 +2352,33 @@
 
         return context
 
+    def xhr_response(fun):
+        """
+        Decorator for REST methods.
+        calls jsonfilter on the returned dictionary and returns result
+        as HttpResponse object of content_type application/json
+        """
+        @wraps(fun)
+        def wrapper(*args, **kwds):
+            return HttpResponse(jsonfilter(fun(*args, **kwds)),
+                                content_type="application/json")
+        return wrapper
+
     def jsunittests(request):
-      """ Provides a page for the js unit tests """
-      bbv = BitbakeVersion.objects.filter(branch="master").first()
-      release = Release.objects.filter(bitbake_version=bbv).first()
+        """ Provides a page for the js unit tests """
+        bbv = BitbakeVersion.objects.filter(branch="master").first()
+        release = Release.objects.filter(bitbake_version=bbv).first()
 
-      name = "_js_unit_test_prj_"
+        name = "_js_unit_test_prj_"
 
-      # If there is an existing project by this name delete it. We don't want
-      # Lots of duplicates cluttering up the projects.
-      Project.objects.filter(name=name).delete()
+        # If there is an existing project by this name delete it. We don't want
+        # Lots of duplicates cluttering up the projects.
+        Project.objects.filter(name=name).delete()
 
-      new_project = Project.objects.create_project(name=name, release=release)
+        new_project = Project.objects.create_project(name=name, release=release)
 
-      context = { 'project' : new_project }
-      return render(request, "js-unit-tests.html", context)
+        context = { 'project' : new_project }
+        return render(request, "js-unit-tests.html", context)
 
     from django.views.decorators.csrf import csrf_exempt
     @csrf_exempt
@@ -2582,7 +2633,155 @@
 
         return HttpResponse(jsonfilter({"error": "ok",}), content_type = "application/json")
 
+    @xhr_response
+    def xhr_customrecipe(request):
+        """
+        Custom image recipe REST API
 
+        Entry point: /xhr_customrecipe/
+        Method: POST
+
+        Args:
+            name: name of custom recipe to create
+            project: target project id of orm.models.Project
+            base: base recipe id of orm.models.Recipe
+
+        Returns:
+            {"error": "ok",
+             "url": <url of the created recipe>}
+            or
+            {"error": <error message>}
+        """
+        # check if request has all required parameters
+        for param in ('name', 'project', 'base'):
+            if param not in request.POST:
+                return {"error": "Missing parameter '%s'" % param}
+
+        # get project and baserecipe objects
+        params = {}
+        for name, model in [("project", Project),
+                            ("base", Recipe)]:
+            value = request.POST[name]
+            try:
+                params[name] = model.objects.get(id=value)
+            except model.DoesNotExist:
+                return {"error": "Invalid %s id %s" % (name, value)}
+
+        # create custom recipe
+        try:
+            recipe = CustomImageRecipe.objects.create(
+                         name=request.POST["name"],
+                         base_recipe=params["base"],
+                         project=params["project"])
+        except Error as err:
+            return {"error": "Can't create custom recipe: %s" % err}
+
+        # Find the package list from the last build of this recipe/target
+        build = Build.objects.filter(target__target=params['base'].name,
+                    project=params['project']).last()
+
+        if build:
+            # Copy in every package
+            # We don't want these packages to be linked to anything because
+            # that underlying data may change e.g. delete a build
+            for package in build.package_set.all():
+                # Create the duplicate
+                package.pk = None
+                package.save()
+                # Disassociate the package from the build
+                package.build = None
+                package.save()
+                recipe.packages.add(package)
+        else:
+            logger.warn("No packages found for this base recipe")
+
+        return {"error": "ok",
+                "url": reverse('customrecipe', args=(params['project'].pk,
+                                                     recipe.id))}
+
+    @xhr_response
+    def xhr_customrecipe_id(request, recipe_id):
+        """
+        Set of ReST API processors working with recipe id.
+
+        Entry point: /xhr_customrecipe/<recipe_id>
+
+        Methods:
+            GET - Get details of custom image recipe
+            DELETE - Delete custom image recipe
+
+        Returns:
+            GET:
+            {"error": "ok",
+             "info": dictionary of field name -> value pairs
+                     of the CustomImageRecipe model}
+            DELETE:
+            {"error": "ok"}
+            or
+            {"error": <error message>}
+        """
+        objects = CustomImageRecipe.objects.filter(id=recipe_id)
+        if not objects:
+            return {"error": "Custom recipe with id=%s "
+                             "not found" % recipe_id}
+        if request.method == 'GET':
+            values = CustomImageRecipe.objects.filter(id=recipe_id).values()
+            if values:
+                return {"error": "ok", "info": values[0]}
+            else:
+                return {"error": "Custom recipe with id=%s "
+                                 "not found" % recipe_id}
+            return {"error": "ok", "info": objects.values()[0]}
+        elif request.method == 'DELETE':
+            objects.delete()
+            return {"error": "ok"}
+        else:
+            return {"error": "Method %s is not supported" % request.method}
+
+    @xhr_response
+    def xhr_customrecipe_packages(request, recipe_id, package_id):
+        """
+        ReST API to add/remove packages to/from custom recipe.
+
+        Entry point: /xhr_customrecipe/<recipe_id>/packages/
+
+        Methods:
+            PUT - Add package to the recipe
+            DELETE - Delete package from the recipe
+
+        Returns:
+            {"error": "ok"}
+            or
+            {"error": <error message>}
+        """
+        try:
+            recipe = CustomImageRecipe.objects.get(id=recipe_id)
+        except CustomImageRecipe.DoesNotExist:
+            return {"error": "Custom recipe with id=%s "
+                             "not found" % recipe_id}
+
+        if request.method == 'GET' and not package_id:
+            return {"error": "ok",
+                    "packages": list(recipe.packages.values_list('id'))}
+
+        try:
+            package = Package.objects.get(id=package_id)
+        except Package.DoesNotExist:
+            return {"error": "Package with id=%s "
+                             "not found" % package_id}
+
+        if request.method == 'PUT':
+            recipe.packages.add(package)
+            return {"error": "ok"}
+        elif request.method == 'DELETE':
+            if package in recipe.packages.all():
+                recipe.packages.remove(package)
+                return {"error": "ok"}
+            else:
+                return {"error": "Package '%s' is not in the recipe '%s'" % \
+                                 (package.name, recipe.name)}
+        else:
+            return {"error": "Method %s is not supported" % request.method}
 
     def importlayer(request, pid):
         template = "importlayer.html"
@@ -2596,12 +2795,16 @@
         project = Project.objects.get(pk=pid)
         layer_version = Layer_Version.objects.get(pk=layerid)
 
-        context = { 'project' : project,
-                   'layerversion' : layer_version,
-                   'layerdeps' : { "list": [
-                     [{"id": y.id, "name": y.layer.name} for y in x.depends_on.get_equivalents_wpriority(project)][0] for x in layer_version.dependencies.all()]},
-                   'projectlayers': map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project))
-                  }
+        context = {'project' : project,
+            'layerversion' : layer_version,
+            'layerdeps' : {"list": [{"id": dep.id,
+                "name": dep.layer.name,
+                "layerdetailurl": reverse('layerdetails', args=(pid, dep.pk)),
+                "vcs_url": dep.layer.vcs_url,
+                "vcs_reference": dep.get_vcs_reference()} \
+                for dep in layer_version.get_alldeps(project.id)]},
+            'projectlayers': map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project))
+        }
 
         return context
 
@@ -2628,6 +2831,15 @@
 
         return(vars_managed,sorted(vars_fstypes),vars_blacklist)
 
+    def customrecipe(request, pid, recipe_id):
+        project = Project.objects.get(pk=pid)
+        context = {'project' : project,
+                   'projectlayers': [],
+                   'recipe' : CustomImageRecipe.objects.get(pk=recipe_id)
+                  }
+
+        return render(request, "customrecipe.html", context)
+
     @_template_renderer("projectconf.html")
     def projectconf(request, pid):
 
@@ -2733,6 +2945,9 @@
         context['project'] = prj
         _set_parameters_values(pagesize, orderby, request)
 
+        # add the most recent builds for this project
+        context['mru'] = _get_latest_builds(prj)
+
         return context
 
 
@@ -2797,7 +3012,7 @@
             if file_name is None:
                 raise Exception("Could not handle artifact %s id %s" % (artifact_type, artifact_id))
             else:
-                content_type = b.buildrequest.environment.get_artifact_type(file_name)
+                content_type = MimeTypeFinder.get_mimetype(file_name)
                 fsock = b.buildrequest.environment.get_artifact(file_name)
                 file_name = os.path.basename(file_name) # we assume that the build environment system has the same path conventions as host
 
@@ -2833,6 +3048,10 @@
         queryset_all = queryset_all.filter(Q(is_default=False) |
                                            q_default_with_builds)
 
+        # if in BUILD_MODE, exclude everything but the command line builds project
+        if not toastermain.settings.BUILD_MODE:
+            queryset_all = queryset_all.exclude(is_default=False)
+
         # boilerplate code that takes a request for an object type and returns a queryset
         # for that object type. copypasta for all needed table searches
         (filter_string, search_term, ordering_string) = _search_tuple(request, Project)
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index eb2914d..6bb3889 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -20,6 +20,7 @@
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 from django.views.generic import View, TemplateView
+from django.views.decorators.cache import cache_control
 from django.shortcuts import HttpResponse
 from django.http import HttpResponseBadRequest
 from django.core import serializers
@@ -38,6 +39,9 @@
 import operator
 import re
 
+import logging
+logger = logging.getLogger("toaster")
+
 from toastergui.views import objtojson
 
 class ToasterTable(TemplateView):
@@ -45,7 +49,7 @@
         super(ToasterTable, self).__init__()
         if 'template_name' in kwargs:
             self.template_name = kwargs['template_name']
-        self.title = None
+        self.title = "Table"
         self.queryset = None
         self.columns = []
         self.filters = {}
@@ -61,6 +65,18 @@
                         orderable=True,
                         field_name="id")
 
+        # prevent HTTP caching of table data
+    @cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True)
+    def dispatch(self, *args, **kwargs):
+        return super(ToasterTable, self).dispatch(*args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        context = super(ToasterTable, self).get_context_data(**kwargs)
+        context['title'] = self.title
+        context['table_name'] =  type(self).__name__.lower()
+
+        return context
+
 
     def get(self, request, *args, **kwargs):
         if request.GET.get('format', None) == 'json':
@@ -219,7 +235,8 @@
         """Creates a query based on the model's search_allowed_fields"""
 
         if not hasattr(self.queryset.model, 'search_allowed_fields'):
-            raise Exception("Err Search fields aren't defined in the model")
+            raise Exception("Search fields aren't defined in the model %s"
+                           % self.queryset.model)
 
         search_queries = []
         for st in search_term.split(" "):
@@ -242,11 +259,14 @@
         search = request.GET.get("search", None)
         filters = request.GET.get("filter", None)
         orderby = request.GET.get("orderby", None)
+        nocache = request.GET.get("nocache", None)
 
         # Make a unique cache name
         cache_name = self.__class__.__name__
 
         for key, val in request.GET.iteritems():
+            if key == 'nocache':
+                continue
             cache_name = cache_name + str(key) + str(val)
 
         for key, val in kwargs.iteritems():
@@ -254,9 +274,14 @@
 
         # No special chars allowed in the cache name apart from dash
         cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name)
+
+        if nocache:
+            cache.delete(cache_name)
+
         data = cache.get(cache_name)
 
         if data:
+            logger.debug("Got cache data for table '%s'" % self.title)
             return data
 
         self.setup_columns(**kwargs)
@@ -330,33 +355,6 @@
         return data
 
 
-class ToasterTemplateView(TemplateView):
-    # renders a instance in a template, or returns the context as json
-    # the class-equivalent of the _template_renderer decorator for views
-
-    def __init__(self, *args, **kwargs):
-        super(ToasterTemplateView, self).__init__(*args, **kwargs)
-        self.context_entries = []
-
-    def get(self, *args, **kwargs):
-        if self.request.GET.get('format', None) == 'json':
-            from django.core.urlresolvers import reverse
-            from django.shortcuts import HttpResponse
-            from views import objtojson
-            from toastergui.templatetags.projecttags import json as jsonfilter
-
-            context = self.get_context_data(**kwargs)
-
-            for x in context.keys():
-                if x not in self.context_entries:
-                    del context[x]
-
-            context["error"] = "ok"
-
-            return HttpResponse(jsonfilter(context,  default=objtojson ),
-                            content_type = "application/json; charset=utf-8")
-
-        return super(ToasterTemplateView, self).get(*args, **kwargs)
 
 class ToasterTypeAhead(View):
     """ A typeahead mechanism to support the front end typeahead widgets """
diff --git a/bitbake/lib/toaster/toastermain/settings.py b/bitbake/lib/toaster/toastermain/settings.py
index b149a5e..b28ddb2 100644
--- a/bitbake/lib/toaster/toastermain/settings.py
+++ b/bitbake/lib/toaster/toastermain/settings.py
@@ -23,6 +23,11 @@
 
 import os, re
 
+# Temporary toggle for Image customisation
+CUSTOM_IMAGE = False
+if os.environ.get("CUSTOM_IMAGE", None) is not None:
+    CUSTOM_IMAGE = True
+
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
@@ -87,11 +92,9 @@
     else:
         raise Exception("FIXME: Please implement missing database url schema for url: %s" % dburl)
 
-
+BUILD_MODE = False
 if 'TOASTER_MANAGED' in os.environ and os.environ['TOASTER_MANAGED'] == "1":
-    MANAGED = True
-else:
-    MANAGED = False
+    BUILD_MODE = True
 
 # Allows current database settings to be exported as a DATABASE_URL environment variable value
 
diff --git a/bitbake/lib/toaster/toastermain/urls.py b/bitbake/lib/toaster/toastermain/urls.py
index 521588a..6c4a953 100644
--- a/bitbake/lib/toaster/toastermain/urls.py
+++ b/bitbake/lib/toaster/toastermain/urls.py
@@ -60,7 +60,7 @@
     #logger.info("Enabled django_toolbar extension")
 
 
-if toastermain.settings.MANAGED:
+if toastermain.settings.BUILD_MODE:
     urlpatterns = [
         # Uncomment the next line to enable the admin:
         url(r'^admin/', include(admin.site.urls)),