diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml
index 288c9cb..2693a24 100644
--- a/.github/workflows/alpine.yml
+++ b/.github/workflows/alpine.yml
@@ -33,7 +33,7 @@
     steps:
     # Checkout the repository
     - name: Checkout
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: Build
       run: |
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0fac3c3..41a7a5a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,6 +23,28 @@
       - github-actions
 
 jobs:
+  single_checks:
+    name: "Single sanity check"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install latest DMD
+        uses: dlang-community/setup-dlang@v1
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Run tests
+        run: |
+          # check for trailing whitespace
+          TRAILING_WS_COUNT=$(find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \; | wc -l)
+          if [ $TRAILING_WS_COUNT -ne 0 ]; then
+              echo "========================================"
+              find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \;
+              echo "========================================"
+              echo "The files above have trailing whitespace"
+              exit 1
+          fi
+          # check that the man page generation still works
+          dub --single -v scripts/man/gen_man.d
+
   main:
     name: Run
     strategy:
@@ -32,21 +54,23 @@
         # Latest stable version, update at will
         os: [ macOS-11, ubuntu-20.04, windows-2019 ]
         dc:
+          # Always test latest as that is what we use to compile on release
           - dmd-latest
-          - dmd-2.100.2
           - ldc-latest
+          # Provide some testing for upstream
           - dmd-master
-#          - ldc-master
-          # This is the bootstrap compiler used to compile the releases
-          - ldc-1.23.0
-          # Some intermediate compilers for good measure
-          - dmd-2.095.1
+          - ldc-master
+          # Test some intermediate versions
+          - ldc-1.26.0
           - dmd-2.098.1
+          - dmd-2.101.1
+          - dmd-2.104.2
         include:
-          - { do_test: true }
-          - { dc: dmd-2.095.1, do_test: false }
-          - { dc: dmd-2.098.1, do_test: false }
-          - { dc: ldc-1.23.0 , do_test: false }
+          - { do_test: false }
+          - { dc: dmd-latest, do_test: true }
+          - { dc: ldc-latest, do_test: true }
+          - { dc: dmd-master, do_test: true }
+          - { dc: ldc-master, do_test: true }
 
     runs-on: ${{ matrix.os }}
     steps:
@@ -71,19 +95,17 @@
 
     # Checkout the repository
     - name: Checkout
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: '[POSIX] Test'
       if: runner.os != 'Windows'
       env:
-        COVERAGE: false
-        # The value doesn't matter as long as it's > 2.087
-        FRONTEND: 2.095.0
+        COVERAGE: true
       run: |
         dub build --compiler=${{ env.DC }}
         if [[ ${{ matrix.do_test }} == 'true' ]]; then
           dub run   --compiler=${{ env.DC }} --single test/issue2051_running_unittests_from_dub_single_file_packages_fails.d
-          ./scripts/ci/travis.sh
+          ./scripts/ci/ci.sh
         fi
 
     - name: '[Windows] Test'
@@ -109,3 +131,6 @@
           test/run-unittest.sh
         fi
       shell: bash
+
+    - name: Codecov
+      uses: codecov/codecov-action@v4
diff --git a/.github/workflows/pr_info_untrusted.yml b/.github/workflows/pr_info_untrusted.yml
index c3416e5..56b8214 100644
--- a/.github/workflows/pr_info_untrusted.yml
+++ b/.github/workflows/pr_info_untrusted.yml
@@ -29,7 +29,7 @@
         compiler: ldc-latest
 
     - name: Checkout
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
       with:
         fetch-depth: 0
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..f94737c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,113 @@
+# When a release is published, build the assets and upload them
+name: Build release assets
+
+on:
+  release:
+    types:
+      - published
+
+jobs:
+  # First we define a job with a matrix that will build all relevant assets,
+  # and collect them in a temporary storage using `actions/upload-artifacts`
+  build:
+    name: 'Build artifacts for ${{ github.event.release.tag_name }}'
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ macOS-11, ubuntu-20.04, windows-2019 ]
+        arch: [ x86_64 ]
+        include:
+          - { os: windows-2019, arch: i686 }
+
+    runs-on: ${{ matrix.os }}
+    steps:
+      ## Dependencies
+      - name: '[OSX] Install dependencies'
+        if: runner.os == 'macOS'
+        run: |
+          brew install pkg-config coreutils
+          echo "PKG_CONFIG_PATH=/usr/local/opt/openssl@1.1/lib/pkgconfig/" >> $GITHUB_ENV
+      - name: '[Linux] Install dependencies'
+        if: runner.os == 'Linux'
+        run: |
+          sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
+
+      ## Boileterplate (compiler/repo)
+      - name: Install compiler
+        uses: dlang-community/setup-dlang@v1
+        with:
+          compiler: ldc-latest
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          ref: ${{ github.event.release.tag_name }}
+
+      ## Actually build the releases
+      - name: '[POSIX] Build release'
+        if: runner.os == 'Linux' || runner.os == 'macOS'
+        env:
+          GITVER: ${{ github.event.release.tag_name }}
+          DMD: "ldmd2"
+          ARCH_TRIPLE: ${{ matrix.arch }}-${{ runner.os == 'linux' &&  'pc-linux' || 'apple-darwin' }}
+        run: |
+          ldc2 -run ./build.d -release -mtriple=${ARCH_TRIPLE}
+          pushd bin
+          if [ ${{ runner.os }} == 'Linux' ]; then
+            tar -c -f 'dub-${{ github.event.release.tag_name }}-linux-${{ matrix.arch }}.tar.gz' -v -z --owner=0 --group=0 dub
+          else
+            gtar -c -f 'dub-${{ github.event.release.tag_name }}-osx-${{ matrix.arch }}.tar.gz' -v -z --owner=0 --group=0 dub
+          fi
+          popd
+      - name: '[Windows] Build release'
+        if: runner.os == 'Windows'
+        env:
+          GITVER: ${{ github.event.release.tag_name }}
+          DMD: "ldmd2"
+        run: |
+          ldc2 -run ./build.d -release -mtriple=${{ matrix.arch }}-pc-windows-msvc
+          pushd bin
+          7z a dub-${{ github.event.release.tag_name }}-windows-${{ matrix.arch }}.zip dub.exe
+          popd
+
+      - name: 'Upload temporary binaries'
+        uses: actions/upload-artifact@v4
+        with:
+          name: dub-release-${{ matrix.os }}-${{ matrix.arch }}
+          path: |
+            bin/dub-${{ github.event.release.tag_name }}-*
+          if-no-files-found: error
+          retention-days: 1
+
+  # Uploads collected builds to the release
+  release:
+    name: "Update release artifacts"
+    runs-on: ubuntu-latest
+    needs:
+      - build
+
+    steps:
+      - name: Download artifacts to release
+        uses: actions/download-artifact@v4
+        with:
+          path: ~/artifacts/
+
+      - name: List all artifacts included in the release
+        id: list-artifacts
+        shell: bash
+        run: |
+          set -euox pipefail
+          ls -aulR ~/artifacts
+          echo "artifacts_directory=$HOME/artifacts" >> $GITHUB_OUTPUT
+
+      - name: Update release artifacts
+        uses: ncipollo/release-action@v1
+        with:
+          token: "${{ secrets.GITHUB_TOKEN }}"
+          tag: ${{ github.event.release.tag_name }}
+          artifacts: ${{ steps.list-artifacts.outputs.artifacts_directory }}/*/*
+          # Keep the existing state of the release
+          allowUpdates: true
+          artifactErrorsFailBuild: true
+          omitNameDuringUpdate: true
+          omitBodyDuringUpdate: true
+          omitPrereleaseDuringUpdate: true
diff --git a/.gitignore b/.gitignore
index 47dcf7d..e815926 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
 /bin/dub-test-library
 /bin/libdub.a
 /bin/dub-*
+/bin/dub.*
 
 # Ignore files or directories created by the test suite.
 *.exe
diff --git a/build-files.txt b/build-files.txt
index 182d017..3a9ec14 100644
--- a/build-files.txt
+++ b/build-files.txt
@@ -6,7 +6,6 @@
 source/dub/compilers/gdc.d
 source/dub/compilers/ldc.d
 source/dub/compilers/utils.d
-source/dub/data/platform.d
 source/dub/data/settings.d
 source/dub/dependency.d
 source/dub/dependencyresolver.d
diff --git a/changelog/dub-fetch.dd b/changelog/dub-fetch.dd
new file mode 100644
index 0000000..a4d5556
--- /dev/null
+++ b/changelog/dub-fetch.dd
@@ -0,0 +1,21 @@
+The fetch command now supports multiple arguments, recursive fetch, and is project-aware
+
+Previously, `dub fetch` could only fetch a single package,
+and was working independently of the working directory.
+
+With this release, support for multiple packages have
+been added, such that the following is now possible:
+---
+$ dub fetch vibe-d@0.9.0 vibe-d@0.9.1 vibe-d@0.9.2
+---
+
+When called with no argument, `dub fetch` used to error out.
+However, it will now attempt to fetch dependencies for the
+current project, if any exists.
+
+Finally, when fetching a package, it might be useful to fetch
+all its dependencies. This is done automatically for projects,
+and can now be done for direct fetch as well:
+---
+$ dub fetch --recursive vibe-d@0.9.0 vibe-d@0.9.1
+---
diff --git a/dub.sdl b/dub.sdl
index 2ae4255..642a204 100644
--- a/dub.sdl
+++ b/dub.sdl
@@ -7,6 +7,13 @@
 
 targetPath "bin"
 
+dflags "-preview=in" platform="dmd"
+dflags "-preview=in" platform="ldc"
+//Disabled due to ICEs in gdc.
+//dflags "-fpreview=in" platform="gdc"
+// Deprecated module(s)
+excludedSourceFiles "source/dub/packagesupplier.d"
+
 configuration "application" {
 	targetType "executable"
 	mainSourceFile "source/app.d"
diff --git a/dub.selections.json b/dub.selections.json
index c675e35..3a78158 100644
--- a/dub.selections.json
+++ b/dub.selections.json
@@ -4,16 +4,18 @@
 		"botan": "1.12.19",
 		"botan-math": "1.0.3",
 		"diet-ng": "1.8.1",
-		"eventcore": "0.9.23",
+		"eventcore": "0.9.27",
 		"libasync": "0.8.6",
 		"libev": "5.0.0+4.04",
 		"libevent": "2.0.2+2.0.16",
-		"memutils": "1.0.9",
+		"memutils": "1.0.10",
 		"mir-linux-kernel": "1.0.1",
-		"openssl": "3.3.0",
+		"openssl": "3.3.3",
+		"openssl-static": "1.0.2+3.0.8",
 		"stdx-allocator": "2.77.5",
 		"taggedalgebraic": "0.11.22",
-		"vibe-core": "1.23.0",
-		"vibe-d": "0.9.5"
+		"vibe-container": "1.0.1",
+		"vibe-core": "2.7.1",
+		"vibe-d": "0.9.7"
 	}
 }
diff --git a/scripts/ci/ci.sh b/scripts/ci/ci.sh
new file mode 100755
index 0000000..494302b
--- /dev/null
+++ b/scripts/ci/ci.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+set -v -e -o pipefail
+
+vibe_ver=$(jq -r '.versions | .["vibe-d"]' < dub.selections.json)
+dub fetch vibe-d@$vibe_ver # get optional dependency
+dub test --compiler=${DC} -c library-nonet
+
+export DMD="$(command -v $DMD)"
+
+./build.d -preview=dip1000 -preview=in -w -g -debug
+
+if [ "$COVERAGE" = true ]; then
+    # library-nonet fails to build with coverage (Issue 13742)
+    dub test --compiler=${DC} -b unittest-cov
+    ./build.d -cov
+else
+    dub test --compiler=${DC} -b unittest-cov
+    ./build.d
+fi
+DUB=`pwd`/bin/dub DC=${DC} dub --single ./test/run-unittest.d
+DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh
diff --git a/scripts/ci/release-windows.sh b/scripts/ci/release-windows.sh
deleted file mode 100755
index 5b87df2..0000000
--- a/scripts/ci/release-windows.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-# Build the Windows binaries under Linux
-set -eux -o pipefail
-
-BIN_NAME=dub
-
-# Allow the script to be run from anywhere
-DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd )"
-cd $DIR
-
-# Setup cross compiler
-source scripts/ci/setup-ldc-windows.sh
-
-# Run LDC with cross-compilation
-archiveName="$BIN_NAME-$VERSION-$OS-$ARCH_SUFFIX.zip"
-echo "Building $archiveName"
-mkdir -p bin
-DMD=ldmd2 ldc2 -run ./build.d -release ${LDC_XDFLAGS}
-
-cd bin
-zip "$archiveName" "${BIN_NAME}.exe"
diff --git a/scripts/ci/release.sh b/scripts/ci/release.sh
deleted file mode 100755
index bdc45da..0000000
--- a/scripts/ci/release.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env bash
-
-set -eux -o pipefail
-
-# Get the directory root, which is two level ahead
-ROOT_DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd )"
-cd ${ROOT_DIR}
-
-VERSION=$(git describe --abbrev=0 --tags)
-ARCH="${ARCH:-64}"
-CUSTOM_FLAGS=()
-unameOut="$(uname -s)"
-case "$unameOut" in
-    Linux*)
-        OS=linux
-        CUSTOM_FLAGS+=("-L--export-dynamic")
-        ;;
-    Darwin*)
-        OS=osx
-        CUSTOM_FLAGS+=("-L-dead_strip")
-        ;;
-    *) echo "Unknown OS: $unameOut"; exit 1
-esac
-
-if [[ $(basename "$DMD") =~ ldmd.* ]] ; then
-    CUSTOM_FLAGS+=("-flto=full")
-    # ld.gold is required on Linux
-    if [ ${OS:-} == "linux" ] ; then
-        CUSTOM_FLAGS+=("-linker=gold")
-    fi
-fi
-
-case "$ARCH" in
-    64) ARCH_SUFFIX="x86_64";;
-    32) ARCH_SUFFIX="x86";;
-    *) echo "Unknown ARCH: $ARCH"; exit 1
-esac
-
-archiveName="dub-$VERSION-$OS-$ARCH_SUFFIX.tar.gz"
-
-echo "Building $archiveName"
-DMD="$(command -v $DMD)" ./build.d -release -m$ARCH ${CUSTOM_FLAGS[@]}
-if [[ "$OSTYPE" == darwin* ]]; then
-    TAR=gtar
-else
-    TAR=tar
-fi
-
-"$TAR" cvfz "bin/$archiveName" --owner=0 --group=0 -C bin dub
diff --git a/scripts/ci/setup-ldc-windows.sh b/scripts/ci/setup-ldc-windows.sh
deleted file mode 100644
index e95881e..0000000
--- a/scripts/ci/setup-ldc-windows.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env bash
-
-# sets up LDC for cross-compilation. Source this script, s.t. the new LDC is in PATH
-
-# Make sure this version matches the version of LDC2 used in .travis.yml,
-# otherwise the compiler and the lib used might mismatch.
-LDC_VERSION="1.22.0"
-ARCH=${ARCH:-32}
-VERSION=$(git describe --abbrev=0 --tags)
-OS=windows
-
-# LDC should already be installed (see .travis.yml)
-# However, we need the libraries, so download them
-# We can't use the downloaded ldc2 itself, because obviously it's for Windows
-
-if [ "${ARCH}" == 64 ]; then
-	ARCH_SUFFIX='x86_64'
-	ZIP_ARCH_SUFFIX='x64'
-else
-	ARCH_SUFFIX='i686'
-	ZIP_ARCH_SUFFIX='x86'
-fi
-
-LDC_DIR_PATH="$(pwd)/ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}"
-LDC_XDFLAGS="-conf=${LDC_DIR_PATH}/etc/ldc2.conf -mtriple=${ARCH_SUFFIX}-pc-windows-msvc"
-
-# Step 1: download the LDC Windows release
-# Check if the user already have it (e.g. building locally)
-if [ ! -d ${LDC_DIR_PATH} ]; then
-    if [ ! -d "ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z" ]; then
-        wget "https://github.com/ldc-developers/ldc/releases/download/v${LDC_VERSION}/ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z"
-    fi
-    7z x "ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z" > /dev/null
-fi
-
-# Step 2: Generate a config file with the proper path
-cat > ${LDC_DIR_PATH}/etc/ldc2.conf <<EOF
-default:
-{
-	switches = [
-		"-defaultlib=phobos2-ldc,druntime-ldc",
-		"-link-defaultlib-shared=false",
-	];
-    post-switches = [
-        "-I${LDC_DIR_PATH}/import",
-    ];
-	lib-dirs = [
-		"${LDC_DIR_PATH}/lib/",
-		"${LDC_DIR_PATH}/lib/mingw/",
-	];
-};
-EOF
diff --git a/scripts/ci/travis.sh b/scripts/ci/travis.sh
deleted file mode 100755
index a811e75..0000000
--- a/scripts/ci/travis.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/bash
-
-set -v -e -o pipefail
-
-vibe_ver=$(jq -r '.versions | .["vibe-d"]' < dub.selections.json)
-dub fetch vibe-d@$vibe_ver # get optional dependency
-dub test --compiler=${DC} -c library-nonet
-
-export DMD="$(command -v $DMD)"
-
-if [ "$FRONTEND" \> 2.087.z ]; then
-    ./build.d -preview=dip1000 -preview=in -w -g -debug
-fi
-
-function clean() {
-    # Hard reset of the DUB local folder is necessary as some tests
-    # currently don't properly clean themselves
-    rm -rf ~/.dub
-    git clean -dxf -- test
-}
-
-if [ "$COVERAGE" = true ]; then
-    # library-nonet fails to build with coverage (Issue 13742)
-    dub test --compiler=${DC} -b unittest-cov
-    ./build.d -cov
-
-    wget https://codecov.io/bash -O codecov.sh
-    bash codecov.sh
-else
-    ./build.d
-    DUB=`pwd`/bin/dub DC=${DC} dub --single ./test/run-unittest.d
-    DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh
-fi
-
-## Checks that only need to be done once per CI run
-## Here the `COVERAGE` variable is abused for this purpose,
-## as it's only defined once in the whole Travis matrix
-if [ "$COVERAGE" = true ]; then
-    # run tests with different compilers
-    DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh
-    clean
-
-    export FRONTEND=2.077
-    source $(~/dlang/install.sh ldc-1.7.0 --activate)
-    DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh
-    deactivate
-    clean
-
-    export FRONTEND=2.068
-    source $(~/dlang/install.sh gdc-4.8.5 --activate)
-    DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh
-    deactivate
-
-    # check for trailing whitespace
-    find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \;
-    # check that the man page generation still works
-    source $(~/dlang/install.sh dmd --activate)
-    source $(~/dlang/install.sh dub --activate)
-    dub --single -v scripts/man/gen_man.d
-fi
diff --git a/scripts/man/gen_man.d b/scripts/man/gen_man.d
index 3d4a127..eb95930 100755
--- a/scripts/man/gen_man.d
+++ b/scripts/man/gen_man.d
@@ -404,9 +404,9 @@
 				manFile.mode == ManWriter.Mode.markdown ? "\n\n" : "\n"
 			));
 		}
-	}  
-  
-  
+	}
+
+
 
 	writeln(manFile.header("COMMON OPTIONS"));
 	manFile.writeArgs("-", args);
diff --git a/source/dub/commandline.d b/source/dub/commandline.d
index 587095e..92092ad 100644
--- a/source/dub/commandline.d
+++ b/source/dub/commandline.d
@@ -82,44 +82,58 @@
 		args = a list of string arguments that will be processed
 
 	Returns:
-		A structure with two members. `value` is the command name
-		`remaining` is a list of unprocessed arguments
+		The command name that was found (may be null).
 */
-auto extractCommandNameArgument(string[] args)
+string commandNameArgument(ref string[] args)
 {
-	struct Result {
-		string value;
-		string[] remaining;
-	}
-
 	if (args.length >= 1 && !args[0].startsWith("-") && !args[0].canFind(":")) {
-		return Result(args[0], args[1 .. $]);
+		const result = args[0];
+		args = args[1 .. $];
+		return result;
 	}
-
-	return Result(null, args);
+	return null;
 }
 
 /// test extractCommandNameArgument usage
 unittest {
-	/// It returns an empty string on when there are no args
-	assert(extractCommandNameArgument([]).value == "");
-	assert(extractCommandNameArgument([]).remaining == []);
+    {
+        string[] args;
+        /// It returns an empty string on when there are no args
+        assert(commandNameArgument(args) is null);
+        assert(!args.length);
+    }
 
-	/// It returns the first argument when it does not start with `-`
-	assert(extractCommandNameArgument(["test"]).value == "test");
+    {
+        string[] args = [ "test" ];
+        /// It returns the first argument when it does not start with `-`
+        assert(commandNameArgument(args) == "test");
+        /// There is nothing to extract when the arguments only contain the `test` cmd
+        assert(!args.length);
+    }
 
-	/// There is nothing to extract when the arguments only contain the `test` cmd
-	assert(extractCommandNameArgument(["test"]).remaining == []);
+    {
+        string[] args = [ "-a", "-b" ];
+        /// It extracts two arguments when they are not a command
+        assert(commandNameArgument(args) is null);
+        assert(args == ["-a", "-b"]);
+    }
 
-	/// It extracts two arguments when they are not a command
-	assert(extractCommandNameArgument(["-a", "-b"]).remaining == ["-a", "-b"]);
+    {
+        string[] args = [ "-test" ];
+        /// It returns the an empty string when it starts with `-`
+        assert(commandNameArgument(args) is null);
+        assert(args.length == 1);
+    }
 
-	/// It returns the an empty string when it starts with `-`
-	assert(extractCommandNameArgument(["-test"]).value == "");
-
-	// Sub package names are ignored as command names
-	assert(extractCommandNameArgument(["foo:bar"]).value == "");
-	assert(extractCommandNameArgument([":foo"]).value == "");
+    {
+        string[] args = [ "foo:bar" ];
+        // Sub package names are ignored as command names
+        assert(commandNameArgument(args) is null);
+        assert(args.length == 1);
+        args[0] = ":foo";
+        assert(commandNameArgument(args) is null);
+        assert(args.length == 1);
+    }
 }
 
 /** Handles the Command Line options and commands.
@@ -139,13 +153,13 @@
 	*/
 	string[] commandNames()
 	{
-		return commandGroups.map!(g => g.commands.map!(c => c.name).array).join;
+		return commandGroups.map!(g => g.commands).joiner.map!(c => c.name).array;
 	}
 
 	/** Parses the general options and sets up the log level
 		and the root_path
 	*/
-	void prepareOptions(CommandArgs args) {
+	string[] prepareOptions(CommandArgs args) {
 		LogLevel loglevel = LogLevel.info;
 
 		options.prepare(args);
@@ -186,6 +200,7 @@
 				setLoggingColorsEnabled(false); // disable colors, no matter what
 				break;
 		}
+		return args.extractAllRemainingArgs();
 	}
 
 	/** Get an instance of the requested command.
@@ -405,7 +420,6 @@
 	}
 
 	auto handler = CommandLineHandler(getCommands());
-	auto commandNames = handler.commandNames();
 
 	// Special syntaxes need to be handled before regular argument parsing
 	if (args.length >= 2)
@@ -435,7 +449,7 @@
 		// We have to assume it isn't, and to reduce the risk of false positive
 		// we only consider the case where the file name is the first argument,
 		// as the shell invocation cannot be controlled.
-		else if (!commandNames.canFind(args[1]) && !args[1].startsWith("-")) {
+		else if (handler.getCommand(args[1]) is null && !args[1].startsWith("-")) {
 			if (exists(args[1])) {
 				auto path = getTempFile("app", ".d");
 				copy(args[1], path.toNativeString());
@@ -448,7 +462,8 @@
 
 	auto common_args = new CommandArgs(args[1..$]);
 
-	try handler.prepareOptions(common_args);
+	try
+		args = handler.prepareOptions(common_args);
 	catch (Exception e) {
 		logError("Error processing arguments: %s", e.msg);
 		logDiagnostic("Full exception: %s", e.toString().sanitize);
@@ -462,16 +477,12 @@
 		return 0;
 	}
 
-	// extract the command
-	args = common_args.extractAllRemainingArgs();
-
-	auto command_name_argument = extractCommandNameArgument(args);
-
-	auto command_args = new CommandArgs(command_name_argument.remaining);
+	const command_name = commandNameArgument(args);
+	auto command_args = new CommandArgs(args);
 	Command cmd;
 
 	try {
-		cmd = handler.prepareCommand(command_name_argument.value, command_args);
+		cmd = handler.prepareCommand(command_name, command_args);
 	} catch (Exception e) {
 		logError("Error processing arguments: %s", e.msg);
 		logDiagnostic("Full exception: %s", e.toString().sanitize);
@@ -482,14 +493,14 @@
 	if (cmd is null) {
 		logInfoNoTag("USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]");
 		logInfoNoTag("");
-		logError("Unknown command: %s", command_name_argument.value);
+		logError("Unknown command: %s", command_name);
 		import std.algorithm.iteration : filter;
 		import std.uni : toUpper;
 		foreach (CommandGroup key; handler.commandGroups)
 		{
 			foreach (Command command; key.commands)
 			{
-				if (levenshteinDistance(command_name_argument.value, command.name) < 4) {
+				if (levenshteinDistance(command_name, command.name) < 4) {
 					logInfo("Did you mean '%s'?", command.name);
 				}
 			}
@@ -885,7 +896,7 @@
 
 	private bool loadCwdPackage(Dub dub, bool warn_missing_package)
 	{
-		auto filePath = Package.findPackageFile(dub.rootPath);
+		auto filePath = dub.packageManager.findPackageFile(dub.rootPath);
 
 		if (filePath.empty) {
 			if (warn_missing_package) {
@@ -1005,14 +1016,15 @@
 
 		static string input(string caption, string default_value)
 		{
-			writef("%s [%s]: ", caption.color(Mode.bold), default_value);
-			stdout.flush();
+			import dub.internal.colorize;
+			cwritef("%s [%s]: ", caption.color(Mode.bold), default_value);
 			auto inp = readln();
 			return inp.length > 1 ? inp[0 .. $-1] : default_value;
 		}
 
 		static string select(string caption, bool free_choice, string default_value, const string[] options...)
 		{
+			import dub.internal.colorize.cwrite;
 			assert(options.length);
 			import std.math : floor, log10;
 			auto ndigits = (size_t val) => log10(cast(double) val).floor.to!uint + 1;
@@ -1035,17 +1047,17 @@
 			auto user_to_idx = (size_t i) => cast(uint)i - 1;
 
 			assert(default_idx >= 0);
-			writeln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold));
+			cwriteln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold));
 			foreach (i, option; options_matrix)
 			{
-				if (i != 0 && (i % num_columns) == 0) writeln();
+				if (i != 0 && (i % num_columns) == 0) cwriteln();
 				if (!option.length)
 					continue;
 				auto user_id = idx_to_user(option);
-				writef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id,
+				cwritef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id,
 					leftJustifier(option, max_width));
 			}
-			writeln();
+			cwriteln();
 			immutable default_choice = (default_idx + 1).to!string;
 			while (true)
 			{
@@ -1294,11 +1306,11 @@
 
 	protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug")
 	{
-		PackageAndVersion package_info = splitPackageName(str_package_info);
-		setupPackage(dub, package_info.name, default_build_type, package_info.version_);
+		UserPackageDesc udesc = UserPackageDesc.fromString(str_package_info);
+		setupPackage(dub, udesc, default_build_type);
 	}
 
-	protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug", string ver = "")
+	protected void setupPackage(Dub dub, UserPackageDesc udesc, string default_build_type = "debug")
 	{
 		if (!m_compilerName.length) m_compilerName = dub.defaultCompiler;
 		if (!m_arch.length) m_arch = dub.defaultArchitecture;
@@ -1318,7 +1330,7 @@
 		this.baseSettings.buildSettings.addVersions(m_dVersions);
 
 		m_defaultConfig = null;
-		enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package.");
+		enforce(loadSpecificPackage(dub, udesc), "Failed to load package.");
 
 		if (this.baseSettings.config.length != 0 &&
 			!dub.configurations.canFind(this.baseSettings.config) &&
@@ -1355,35 +1367,34 @@
 		}
 	}
 
-	private bool loadSpecificPackage(Dub dub, string package_name, string ver)
+	private bool loadSpecificPackage(Dub dub, UserPackageDesc udesc)
 	{
 		if (this.baseSettings.single) {
-			enforce(package_name.length, "Missing file name of single-file package.");
-			dub.loadSingleFilePackage(package_name);
+			enforce(udesc.name.length, "Missing file name of single-file package.");
+			dub.loadSingleFilePackage(udesc.name);
 			return true;
 		}
 
 
-		bool from_cwd = package_name.length == 0 || package_name.startsWith(":");
+		bool from_cwd = udesc.name.length == 0 || udesc.name.startsWith(":");
 		// load package in root_path to enable searching for sub packages
 		if (loadCwdPackage(dub, from_cwd)) {
-			if (package_name.startsWith(":"))
+			if (udesc.name.startsWith(":"))
 			{
-				auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false);
+				auto pack = dub.packageManager.getSubPackage(
+					dub.project.rootPackage, udesc.name[1 .. $], false);
 				dub.loadPackage(pack);
 				return true;
 			}
 			if (from_cwd) return true;
 		}
 
-		enforce(package_name.length, "No valid root package found - aborting.");
+		enforce(udesc.name.length, "No valid root package found - aborting.");
 
-		const vers = ver.length ? VersionRange.fromString(ver) : VersionRange.Any;
-		auto pack = dub.packageManager.getBestPackage(package_name, vers);
+		auto pack = dub.packageManager.getBestPackage(
+			PackageName(udesc.name), udesc.range);
 
-		enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name,
-			ver == "" ? "" : ("@" ~ ver)
-		));
+		enforce(pack, format!"Failed to find package '%s' locally."(udesc));
 		logInfo("Building package %s in %s", pack.name, pack.path.toNativeString());
 		dub.loadPackage(pack);
 		return true;
@@ -1530,16 +1541,15 @@
 
 		if (!m_nonInteractive)
 		{
-			const packageParts = splitPackageName(free_args[0]);
+			const packageParts = UserPackageDesc.fromString(free_args[0]);
 			if (auto rc = fetchMissingPackages(dub, packageParts))
 				return rc;
 		}
 		return super.execute(dub, free_args, app_args);
 	}
 
-	private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts)
+	private int fetchMissingPackages(Dub dub, in UserPackageDesc packageParts)
 	{
-
 		static bool input(string caption, bool default_value = true) {
 			writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N");
 			auto inp = readln();
@@ -1556,42 +1566,39 @@
 			}
 		}
 
-		VersionRange dep;
-
-		if (packageParts.version_.length > 0) {
-			// the user provided a version manually
-			dep = VersionRange.fromString(packageParts.version_);
-		} else if (packageParts.name.startsWith(":")) {
-			// Sub-packages are always assumed to be present
+		// Local subpackages are always assumed to be present
+		if (packageParts.name.startsWith(":"))
 			return 0;
-		} else if (dub.packageManager.getBestPackage(packageParts.name)) {
-			// found locally
+
+		const baseName = PackageName(packageParts.name).main;
+		// Found locally
+		if (dub.packageManager.getBestPackage(baseName, packageParts.range))
 			return 0;
-		} else {
-			// search for the package and filter versions for exact matches
-			auto basePackageName = getBasePackageName(packageParts.name);
-			auto search = dub.searchPackages(basePackageName)
-				.map!(tup => tup[1].find!(p => p.name == basePackageName))
-				.filter!(ps => !ps.empty);
-			if (search.empty) {
-				logWarn("Package '%s' was neither found locally nor online.", packageParts.name);
-				return 2;
-			}
 
-			const p = search.front.front;
-			logInfo("Package '%s' was not found locally but is available online:", packageParts.name);
-			logInfo("---");
-			logInfo("Description: %s", p.description);
-			logInfo("Version: %s", p.version_);
-			logInfo("---");
-
-			const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name));
-			if (!answer)
-				return 0;
-			dep = VersionRange.fromString(p.version_);
+		// Non-interactive, either via flag, or because a version was provided
+		if (m_yes || !packageParts.range.matchesAny()) {
+			dub.fetch(baseName, packageParts.range);
+			return 0;
+		}
+		// Otherwise we go the long way of asking the user.
+		// search for the package and filter versions for exact matches
+		auto search = dub.searchPackages(baseName.toString())
+			.map!(tup => tup[1].find!(p => p.name == baseName.toString()))
+			.filter!(ps => !ps.empty);
+		if (search.empty) {
+			logWarn("Package '%s' was neither found locally nor online.", packageParts);
+			return 2;
 		}
 
-		dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none);
+		const p = search.front.front;
+		logInfo("Package '%s' was not found locally but is available online:", packageParts);
+		logInfo("---");
+		logInfo("Description: %s", p.description);
+		logInfo("Version: %s", p.version_);
+		logInfo("---");
+
+		if (input("Do you want to fetch '%s@%s' now?".format(packageParts, p.version_)))
+			dub.fetch(baseName, VersionRange.fromString(p.version_));
 		return 0;
 	}
 }
@@ -2122,64 +2129,140 @@
 }
 
 class FetchCommand : FetchRemoveCommand {
+	private enum FetchStatus
+	{
+		/// Package is already present and on the right version
+		Present = 0,
+		/// Package was fetched from the registry
+		Fetched = 1,
+		/// Attempts at fetching the package failed
+		Failed = 2,
+	}
+
+	protected bool recursive;
+	protected size_t[FetchStatus.max + 1] result;
+
 	this() @safe pure nothrow
 	{
 		this.name = "fetch";
 		this.argumentsPattern = "<package>[@<version-spec>]";
-		this.description = "Manually retrieves and caches a package";
+		this.description = "Explicitly retrieves and caches packages";
 		this.helpText = [
-			"Note: Use \"dub add <dependency>\" if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.",
+			"When run with one or more arguments, regardless of the location it is run in,",
+			"it will fetch the packages matching the argument(s).",
+			"Examples:",
+			"$ dub fetch vibe-d",
+			"$ dub fetch vibe-d@v0.9.0 --cache=local --recursive",
 			"",
-			"Explicit retrieval/removal of packages is only needed when you want to put packages in a place where several applications can share them. If you just have a dependency to add, use the `add` command. Dub will do the rest for you.",
+			"When run in a project with no arguments, it will fetch all dependencies for that project.",
+			"If the project doesn't have set dependencies (no 'dub.selections.json'), it will also perform dependency resolution.",
+			"Example:",
+			"$ cd myProject && dub fetch",
 			"",
-			"Without specified options, placement/removal will default to a user wide shared location.",
-			"",
-			"Complete applications can be retrieved and run easily by e.g.",
-			"$ dub fetch vibelog --cache=local",
-			"$ dub run vibelog --cache=local",
-			"",
-			"This will grab all needed dependencies and compile and run the application.",
+			"Note that the 'build', 'run', and any other command that need packages will automatically perform fetch,",
+			"hence it is not generally necessary to run this command before any other."
 		];
 	}
 
 	override void prepare(scope CommandArgs args)
 	{
+		args.getopt("r|recursive", &this.recursive, [
+			"Also fetches dependencies of specified packages",
+		]);
 		super.prepare(args);
 	}
 
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
-		enforceUsage(free_args.length == 1, "Expecting exactly one argument.");
 		enforceUsage(app_args.length == 0, "Unexpected application arguments.");
 
-		auto location = dub.defaultPlacementLocation;
-
-		auto name = free_args[0];
-
-		FetchOptions fetchOpts;
-		fetchOpts |= FetchOptions.forceBranchUpgrade;
-		if (m_version.length) { // remove then --version removed
-			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
+		// remove then --version removed
+		if (m_version.length) {
+			enforceUsage(free_args.length == 1, "Expecting exactly one argument when using --version.");
+			const name = free_args[0];
 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version);
-			dub.fetch(name, VersionRange.fromString(m_version), location, fetchOpts);
-		} else if (name.canFindVersionSplitter) {
-			const parts = name.splitPackageName;
-			dub.fetch(parts.name, VersionRange.fromString(parts.version_), location, fetchOpts);
-		} else {
-			try {
-				dub.fetch(name, VersionRange.Any, location, fetchOpts);
-				logInfo("Finished", Color.green, "%s fetched", name.color(Mode.bold));
-				logInfo(
-					"Please note that you need to use `dub run <pkgname>` " ~
-					"or add it to dependencies of your package to actually use/run it. "
-				);
-			}
-			catch(Exception e){
-				logInfo("Getting a release version failed: %s", e.msg);
-				return 1;
-			}
+			enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed.");
+			this.fetchPackage(dub, UserPackageDesc(name, VersionRange.fromString(m_version)));
+			return this.result[FetchStatus.Failed] ? 1 : 0;
 		}
-		return 0;
+
+		// Fetches dependencies of the project
+		// This is obviously mutually exclusive with the below foreach
+		if (!free_args.length) {
+			if (!this.loadCwdPackage(dub, true))
+				return 1;
+			// retrieve missing packages
+			if (!dub.project.hasAllDependencies) {
+				logInfo("Resolving", Color.green, "missing dependencies for project");
+				dub.upgrade(UpgradeOptions.select);
+			}
+			else
+				logInfo("All %s dependencies are already present locally",
+						dub.project.dependencies.length);
+			return 0;
+		}
+
+        // Fetches packages named explicitly
+		foreach (name; free_args) {
+			const udesc = UserPackageDesc.fromString(name);
+			this.fetchPackage(dub, udesc);
+		}
+        // Note that this does not include packages indirectly fetched.
+        // Hence it is not currently displayed in the no-argument version,
+        // and will only include directly mentioned packages in the arg version.
+		logInfoNoTag("%s packages fetched, %s already present, %s failed",
+				this.result[FetchStatus.Fetched], this.result[FetchStatus.Present],
+				this.result[FetchStatus.Failed]);
+		return this.result[FetchStatus.Failed] ? 1 : 0;
+	}
+
+    /// Shell around `fetchSinglePackage` with logs and recursion support
+    private void fetchPackage(Dub dub, UserPackageDesc udesc)
+    {
+        auto r = this.fetchSinglePackage(dub, udesc);
+        this.result[r] += 1;
+        final switch (r) {
+        case FetchStatus.Failed:
+            // Error displayed in `fetchPackage` as it has more information
+            // However we need to return here as we can't recurse.
+            return;
+        case FetchStatus.Present:
+            logInfo("Existing", Color.green, "package %s found locally", udesc);
+            break;
+        case FetchStatus.Fetched:
+            logInfo("Fetched", Color.green, "package %s successfully", udesc);
+            break;
+        }
+        if (this.recursive) {
+            auto pack = dub.packageManager.getBestPackage(
+				PackageName(udesc.name), udesc.range);
+            auto proj = new Project(dub.packageManager, pack);
+            if (!proj.hasAllDependencies) {
+				logInfo("Resolving", Color.green, "missing dependencies for project");
+				dub.loadPackage(pack);
+				dub.upgrade(UpgradeOptions.select);
+			}
+        }
+    }
+
+	/// Implementation for argument version
+	private FetchStatus fetchSinglePackage(Dub dub, UserPackageDesc udesc)
+	{
+		auto fspkg = dub.packageManager.getBestPackage(
+			PackageName(udesc.name), udesc.range);
+		// Avoid dub fetch if the package is present on the filesystem.
+		if (fspkg !is null && udesc.range.isExactVersion())
+			return FetchStatus.Present;
+
+		try {
+			auto pkg = dub.fetch(PackageName(udesc.name), udesc.range,
+				FetchOptions.forceBranchUpgrade);
+			assert(pkg !is null, "dub.fetch returned a null Package");
+			return pkg is fspkg ? FetchStatus.Present : FetchStatus.Fetched;
+		} catch (Exception e) {
+			logError("Fetching %s failed: %s", udesc, e.msg);
+			return FetchStatus.Failed;
+		}
 	}
 }
 
@@ -2242,13 +2325,15 @@
 		if (!m_version.empty) { // remove then --version removed
 			enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed.");
 			logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version);
-			dub.remove(package_id, m_version, location);
+			dub.remove(PackageName(package_id), m_version, location);
 		} else {
-			const parts = package_id.splitPackageName;
-			if (m_nonInteractive || parts.version_.length) {
-				dub.remove(parts.name, parts.version_, location);
+			const parts = UserPackageDesc.fromString(package_id);
+            const explicit = package_id.canFindVersionSplitter;
+			if (m_nonInteractive || explicit || parts.range != VersionRange.Any) {
+                const str = parts.range.matchesAny() ? "*" : parts.range.toString();
+				dub.remove(PackageName(parts.name), str, location);
 			} else {
-				dub.remove(package_id, location, &resolveVersion);
+				dub.remove(PackageName(package_id), location, &resolveVersion);
 			}
 		}
 		return 0;
@@ -2267,8 +2352,8 @@
 	override void prepare(scope CommandArgs args)
 	{
 		args.getopt("system", &m_system, [
-			"Register system-wide instead of user-wide"
-		]);
+			"DEPRECATED: Use --cache=system instead"
+		], true);
 	}
 
 	abstract override int execute(Dub dub, string[] free_args, string[] app_args);
@@ -2295,7 +2380,12 @@
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
 		enforceUsage(free_args.length == 1, "Missing search path.");
-		dub.addSearchPath(free_args[0], m_system);
+		enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user,
+			"Cannot use both --system and --cache, prefer --cache");
+		if (this.m_system)
+			dub.addSearchPath(free_args[0], PlacementLocation.system);
+		else
+			dub.addSearchPath(free_args[0], dub.defaultPlacementLocation);
 		return 0;
 	}
 }
@@ -2312,7 +2402,12 @@
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
 		enforceUsage(free_args.length == 1, "Expected one argument.");
-		dub.removeSearchPath(free_args[0], m_system);
+		enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user,
+			"Cannot use both --system and --cache, prefer --cache");
+		if (this.m_system)
+			dub.removeSearchPath(free_args[0], PlacementLocation.system);
+		else
+			dub.removeSearchPath(free_args[0], dub.defaultPlacementLocation);
 		return 0;
 	}
 }
@@ -2334,9 +2429,16 @@
 
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
-		enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments.");
+		enforceUsage(free_args.length == 1 || free_args.length == 2,
+			"Expecting one or two arguments.");
+		enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user,
+			"Cannot use both --system and --cache, prefer --cache");
+
 		string ver = free_args.length == 2 ? free_args[1] : null;
-		dub.addLocalPackage(free_args[0], ver, m_system);
+		if (this.m_system)
+			dub.addLocalPackage(free_args[0], ver, PlacementLocation.system);
+		else
+			dub.addLocalPackage(free_args[0], ver, dub.defaultPlacementLocation);
 		return 0;
 	}
 }
@@ -2353,8 +2455,15 @@
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
 		enforceUsage(free_args.length >= 1, "Missing package path argument.");
-		enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument.");
-		dub.removeLocalPackage(free_args[0], m_system);
+		enforceUsage(free_args.length <= 1,
+			"Expected the package path to be the only argument.");
+		enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user,
+			"Cannot use both --system and --cache, prefer --cache");
+
+		if (this.m_system)
+			dub.removeLocalPackage(free_args[0], PlacementLocation.system);
+		else
+			dub.removeLocalPackage(free_args[0], dub.defaultPlacementLocation);
 		return 0;
 	}
 }
@@ -2376,13 +2485,12 @@
 	override int execute(Dub dub, string[] free_args, string[] app_args)
 	{
 		enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments.");
-		const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*");
+		const pinfo = free_args.length ? UserPackageDesc.fromString(free_args[0]) : UserPackageDesc("",VersionRange.Any);
 		const pname = pinfo.name;
-		const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_);
 		enforceUsage(app_args.length == 0, "The list command supports no application arguments.");
 		logInfoNoTag("Packages present in the system and known to dub:");
 		foreach (p; dub.packageManager.getPackageIterator()) {
-			if ((pname == "" || pname == p.name) && pvlim.matches(p.version_))
+			if ((pname == "" || pname == p.name) && pinfo.range.matches(p.version_))
 				logInfoNoTag("  %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString());
 		}
 		logInfo("");
@@ -2538,7 +2646,7 @@
 			logInfoNoTag("# %s", caption);
 			foreach (ovr; overrides)
 				ovr.target.match!(
-					t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.version_, t));
+					t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.source, t));
 		}
 		printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides");
 		printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides");
@@ -2638,7 +2746,7 @@
 		import std.format : formattedWrite;
 
 		if (m_testPackage.length) {
-			setupPackage(dub, m_testPackage);
+			setupPackage(dub, UserPackageDesc(m_testPackage));
 			m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform);
 
 			GeneratorSettings gensettings = this.baseSettings;
@@ -2667,7 +2775,7 @@
 			if (!path.absolute) path = getWorkingDirectory() ~ path;
 			enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!");
 
-			setupPackage(dub, null);
+			setupPackage(dub, UserPackageDesc.init);
 			auto prj = dub.project;
 			if (this.baseSettings.config.empty)
 				this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform);
@@ -2690,11 +2798,10 @@
 				}
 			}
 
-			static void fixPathDependency(string pack, ref Dependency dep) {
+			static void fixPathDependency(in PackageName name, ref Dependency dep) {
 				dep.visit!(
 					(NativePath path) {
-						auto mainpack = getBasePackageName(pack);
-						dep = Dependency(NativePath("../") ~ mainpack);
+						dep = Dependency(NativePath("../") ~ name.main.toString());
 					},
 					(any) { /* Nothing to do */ },
 				);
@@ -2703,11 +2810,11 @@
 			void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path)
 			{
 				foreach (name, ref dep; recipe.buildSettings.dependencies)
-					fixPathDependency(name, dep);
+					fixPathDependency(PackageName(name), dep);
 
 				foreach (ref cfg; recipe.configurations)
 					foreach (name, ref dep; cfg.buildSettings.dependencies)
-						fixPathDependency(name, dep);
+						fixPathDependency(PackageName(name), dep);
 
 				foreach (ref subp; recipe.subPackages)
 					if (subp.path.length) {
@@ -3026,11 +3133,9 @@
 private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec)
 {
 	Dependency dep;
-	const parts = splitPackageName(depspec);
-	const depname = parts.name;
-	if (parts.version_)
-		dep = Dependency(parts.version_);
-	else
+	const parts = UserPackageDesc.fromString(depspec);
+	const depname = PackageName(parts.name);
+	if (parts.range == VersionRange.Any)
 	{
 		try {
 			const ver = dub.getLatestVersion(depname);
@@ -3041,52 +3146,88 @@
 			return false;
 		}
 	}
-	recipe.buildSettings.dependencies[depname] = dep;
+	else
+		dep = Dependency(parts.range);
+	recipe.buildSettings.dependencies[depname.toString()] = dep;
 	logInfo("Adding dependency %s %s", depname, dep.toString());
 	return true;
 }
 
-private struct PackageAndVersion
+/**
+ * A user-provided package description
+ *
+ * User provided package description currently only covers packages
+ * referenced by their name with an associated version.
+ * Hence there is an implicit assumption that they are in the registry.
+ * Future improvements could support `Dependency` instead of `VersionRange`.
+ */
+private struct UserPackageDesc
 {
 	string name;
-	string version_;
-}
+	VersionRange range = VersionRange.Any;
 
-/* Split <package>=<version-specifier> and <package>@<version-specifier>
-   into `name` and `version_`. */
-private PackageAndVersion splitPackageName(string packageName)
-{
-	// split <package>@<version-specifier>
-	auto parts = packageName.findSplit("@");
-	if (parts[1].empty) {
-		// split <package>=<version-specifier>
-		parts = packageName.findSplit("=");
+	/// Provides a string representation for the user
+	public string toString() const
+	{
+		if (this.range.matchesAny())
+			return this.name;
+		return format("%s@%s", this.name, range);
 	}
 
-	PackageAndVersion p;
-	p.name = parts[0];
-	if (!parts[1].empty)
-		p.version_ = parts[2];
-	return p;
+	/**
+	 * Breaks down a user-provided string into its name and version range
+	 *
+	 * User-provided strings (via the command line) are either in the form
+	 * `<package>=<version-specifier>` or `<package>@<version-specifier>`.
+	 * As it is more explicit, we recommend the latter (the `@` version
+	 * is not used by names or `VersionRange`, but `=` is).
+	 *
+	 * If no version range is provided, the returned struct has its `range`
+	 * property set to `VersionRange.Any` as this is the most usual usage
+	 * in the command line. Some cakkers may want to distinguish between
+	 * user-provided version and implicit version, but this is discouraged.
+	 *
+	 * Params:
+	 *   str = User-provided string
+	 *
+	 * Returns:
+	 *   A populated struct.
+	 */
+	static UserPackageDesc fromString(string packageName)
+	{
+		// split <package>@<version-specifier>
+		auto parts = packageName.findSplit("@");
+		if (parts[1].empty) {
+			// split <package>=<version-specifier>
+			parts = packageName.findSplit("=");
+		}
+
+		UserPackageDesc p;
+		p.name = parts[0];
+		p.range = !parts[1].empty
+			? VersionRange.fromString(parts[2])
+			: VersionRange.Any;
+		return p;
+	}
 }
 
 unittest
 {
 	// https://github.com/dlang/dub/issues/1681
-	assert(splitPackageName("") == PackageAndVersion("", null));
+	assert(UserPackageDesc.fromString("") == UserPackageDesc("", VersionRange.Any));
 
-	assert(splitPackageName("foo") == PackageAndVersion("foo", null));
-	assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1"));
-	assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1"));
-	assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1"));
-	assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1"));
-	assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1"));
-	assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1"));
+	assert(UserPackageDesc.fromString("foo") == UserPackageDesc("foo", VersionRange.Any));
+	assert(UserPackageDesc.fromString("foo=1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1")));
+	assert(UserPackageDesc.fromString("foo@1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1")));
+	assert(UserPackageDesc.fromString("foo@==1.0.1") == UserPackageDesc("foo", VersionRange.fromString("==1.0.1")));
+	assert(UserPackageDesc.fromString("foo@>=1.0.1") == UserPackageDesc("foo", VersionRange.fromString(">=1.0.1")));
+	assert(UserPackageDesc.fromString("foo@~>1.0.1") == UserPackageDesc("foo", VersionRange.fromString("~>1.0.1")));
+	assert(UserPackageDesc.fromString("foo@<1.0.1") == UserPackageDesc("foo", VersionRange.fromString("<1.0.1")));
 }
 
 private ulong canFindVersionSplitter(string packageName)
 {
-	// see splitPackageName
+	// see UserPackageDesc.fromString
 	return packageName.canFind("@", "=");
 }
 
diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d
index 6a0014e..4dba3db 100644
--- a/source/dub/compilers/compiler.d
+++ b/source/dub/compilers/compiler.d
@@ -9,7 +9,7 @@
 
 public import dub.compilers.buildsettings;
 deprecated("Please `import dub.dependency : Dependency` instead") public import dub.dependency : Dependency;
-public import dub.data.platform : BuildPlatform, matchesSpecification;
+public import dub.platform : BuildPlatform, matchesSpecification;
 
 import dub.internal.vibecompat.inet.path;
 import dub.internal.vibecompat.core.file;
@@ -177,24 +177,21 @@
 			args			=	arguments for the probe compilation
 			arch_override	=	special handler for x86_mscoff
 	*/
-	protected final BuildPlatform probePlatform(string compiler_binary, string[] args,
-		string arch_override)
+	protected final BuildPlatform probePlatform(string compiler_binary, string[] args, string arch_override)
 	{
-		import dub.compilers.utils : generatePlatformProbeFile, readPlatformJsonProbe;
+		import dub.compilers.utils : generatePlatformProbeFile, readPlatformSDLProbe;
 		import std.string : format, strip;
 
-		auto fil = generatePlatformProbeFile();
+		NativePath fil = generatePlatformProbeFile();
 
 		auto result = execute(compiler_binary ~ args ~ fil.toNativeString());
 		enforce!CompilerInvocationException(result.status == 0,
 				format("Failed to invoke the compiler %s to determine the build platform: %s",
 				compiler_binary, result.output));
-
-		auto build_platform = readPlatformJsonProbe(result.output);
+		BuildPlatform build_platform = readPlatformSDLProbe(result.output);
+		string ver = determineVersion(compiler_binary, result.output).strip;
 		build_platform.compilerBinary = compiler_binary;
 
-		auto ver = determineVersion(compiler_binary, result.output)
-			.strip;
 		if (ver.empty) {
 			logWarn(`Could not probe the compiler version for "%s". ` ~
 				`Toolchain requirements might be ineffective`, build_platform.compiler);
diff --git a/source/dub/compilers/dmd.d b/source/dub/compilers/dmd.d
index 78fd9d4..5b59d2b 100644
--- a/source/dub/compilers/dmd.d
+++ b/source/dub/compilers/dmd.d
@@ -230,6 +230,11 @@
 	{
 		enforceBuildRequirements(settings);
 
+		// Keep the current dflags at the end of the array so that they will overwrite other flags.
+		// This allows user $DFLAGS to modify flags added by us.
+		const dflagsTail = settings.dflags;
+		settings.dflags = [];
+
 		if (!(fields & BuildSetting.options)) {
 			foreach (t; s_options)
 				if (settings.options & t[0])
@@ -282,6 +287,8 @@
 		if (platform.platform.canFind("posix") && (settings.options & BuildOption.pic))
 			settings.addDFlags("-fPIC");
 
+		settings.addDFlags(dflagsTail);
+
 		assert(fields & BuildSetting.dflags);
 		assert(fields & BuildSetting.copyFiles);
 	}
diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d
index 0d34446..3df8eda 100644
--- a/source/dub/compilers/gdc.d
+++ b/source/dub/compilers/gdc.d
@@ -89,6 +89,11 @@
 	{
 		enforceBuildRequirements(settings);
 
+		// Keep the current dflags at the end of the array so that they will overwrite other flags.
+		// This allows user $DFLAGS to modify flags added by us.
+		const dflagsTail = settings.dflags;
+		settings.dflags = [];
+
 		if (!(fields & BuildSetting.options)) {
 			foreach (t; s_options)
 				if (settings.options & t[0])
@@ -138,6 +143,8 @@
 		if (settings.options & BuildOption.pic)
 			settings.addDFlags("-fPIC");
 
+		settings.addDFlags(dflagsTail);
+
 		assert(fields & BuildSetting.dflags);
 		assert(fields & BuildSetting.copyFiles);
 	}
diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d
index 3cb90e5..3959df6 100644
--- a/source/dub/compilers/ldc.d
+++ b/source/dub/compilers/ldc.d
@@ -107,6 +107,11 @@
 		import std.format : format;
 		enforceBuildRequirements(settings);
 
+		// Keep the current dflags at the end of the array so that they will overwrite other flags.
+		// This allows user $DFLAGS to modify flags added by us.
+		const dflagsTail = settings.dflags;
+		settings.dflags = [];
+
 		if (!(fields & BuildSetting.options)) {
 			foreach (t; s_options)
 				if (settings.options & t[0])
@@ -170,6 +175,8 @@
 			}
 		}
 
+		settings.addDFlags(dflagsTail);
+
 		assert(fields & BuildSetting.dflags);
 		assert(fields & BuildSetting.copyFiles);
 	}
diff --git a/source/dub/compilers/utils.d b/source/dub/compilers/utils.d
index b3bb1e6..2b8addf 100644
--- a/source/dub/compilers/utils.d
+++ b/source/dub/compilers/utils.d
@@ -8,7 +8,7 @@
 module dub.compilers.utils;
 
 import dub.compilers.buildsettings;
-import dub.platform : BuildPlatform, archCheck, compilerCheck, platformCheck;
+import dub.platform : BuildPlatform, archCheck, compilerCheckPragmas, platformCheck, pragmaGen;
 import dub.internal.vibecompat.inet.path;
 import dub.internal.logging;
 
@@ -269,77 +269,101 @@
 NativePath generatePlatformProbeFile()
 {
 	import dub.internal.vibecompat.core.file;
-	import dub.internal.vibecompat.data.json;
 	import dub.internal.utils;
 	import std.string : format;
 
-	// try to not use phobos in the probe to avoid long import times
+	enum moduleInfo = q{
+		module object;
+		alias string = const(char)[];
+	};
+
+	// avoid druntime so that this compiles without a compiler's builtin object.d
 	enum probe = q{
-		module dub_platform_probe;
-
-		template toString(int v) { enum toString = v.stringof; }
-		string stringArray(string[] ary) {
-			string res;
-			foreach (i, e; ary) {
-				if (i)
-					res ~= ", ";
-				res ~= '"' ~ e ~ '"';
-			}
-			return res;
-		}
-
-		pragma(msg, `%1$s`
-			~ '\n' ~ `{`
-			~ '\n' ~ `  "compiler": "`~ determineCompiler() ~ `",`
-			~ '\n' ~ `  "frontendVersion": ` ~ toString!__VERSION__ ~ `,`
-			~ '\n' ~ `  "compilerVendor": "` ~ __VENDOR__ ~ `",`
-			~ '\n' ~ `  "platform": [`
-			~ '\n' ~ `    ` ~ determinePlatform().stringArray
-			~ '\n' ~ `  ],`
-			~ '\n' ~ `  "architecture": [`
-			~ '\n' ~ `    ` ~ determineArchitecture().stringArray
-			~ '\n' ~ `   ],`
-			~ '\n' ~ `}`
-			~ '\n' ~ `%2$s`);
-
-		string[] determinePlatform() { %3$s }
-		string[] determineArchitecture() { %4$s }
-		string determineCompiler() { %5$s }
-
-		}.format(probeBeginMark, probeEndMark, platformCheck, archCheck, compilerCheck);
+		%1$s
+		pragma(msg, `%2$s`);
+		pragma(msg, `\n`);
+		pragma(msg, `compiler`);
+		%6$s
+		pragma(msg, `\n`);
+		pragma(msg, `frontendVersion "`);
+		pragma(msg, __VERSION__.stringof);
+		pragma(msg, `"\n`);
+		pragma(msg, `compilerVendor "`);
+		pragma(msg, __VENDOR__);
+		pragma(msg, `"\n`);
+		pragma(msg, `platform`);
+		%4$s
+		pragma(msg, `\n`);
+		pragma(msg, `architecture `);
+		%5$s
+		pragma(msg, `\n`);
+		pragma(msg, `%3$s`);
+	}.format(moduleInfo, probeBeginMark, probeEndMark, pragmaGen(platformCheck), pragmaGen(archCheck), compilerCheckPragmas);
 
 	auto path = getTempFile("dub_platform_probe", ".d");
 	writeFile(path, probe);
-
 	return path;
 }
 
+
 /**
-	Processes the JSON output generated by compiling the platform probe file.
+	Processes the SDL output generated by compiling the platform probe file.
 
 	See_Also: `generatePlatformProbeFile`.
 */
-BuildPlatform readPlatformJsonProbe(string output)
+BuildPlatform readPlatformSDLProbe(string output)
 {
-	import std.algorithm : map;
+	import std.algorithm : map, max, splitter, joiner, count, filter;
 	import std.array : array;
 	import std.exception : enforce;
+	import std.range : front;
+	import std.ascii : newline;
 	import std.string;
+	import dub.internal.sdlang.parser;
+	import dub.internal.sdlang.ast;
+	import std.conv;
 
 	// work around possible additional output of the compiler
-	auto idx1 = output.indexOf(probeBeginMark);
-	auto idx2 = output.lastIndexOf(probeEndMark);
+	auto idx1 = output.indexOf(probeBeginMark ~ newline ~ "\\n");
+	auto idx2 = output[max(0, idx1) .. $].indexOf(probeEndMark) + idx1;
 	enforce(idx1 >= 0 && idx1 < idx2,
 		"Unexpected platform information output - does not contain a JSON object.");
-	output = output[idx1+probeBeginMark.length .. idx2];
+	output = output[idx1 + probeBeginMark.length .. idx2].replace(newline, "").replace("\\n", "\n");
 
-	import dub.internal.vibecompat.data.json;
-	auto json = parseJsonString(output);
+	output = output.splitter("\n").filter!((e) => e.length > 0)
+		.map!((e) {
+			if (e.count("\"") == 0)
+			{
+				return e ~ ` ""`;
+			}
+			return e;
+		})
+		.joiner("\n").array().to!string;
 
 	BuildPlatform build_platform;
-	build_platform.platform = json["platform"].get!(Json[]).map!(e => e.get!string()).array();
-	build_platform.architecture = json["architecture"].get!(Json[]).map!(e => e.get!string()).array();
-	build_platform.compiler = json["compiler"].get!string;
-	build_platform.frontendVersion = json["frontendVersion"].get!int;
+	Tag sdl = parseSource(output);
+
+	foreach (n; sdl.all.tags)
+	{
+		switch (n.name)
+		{
+		default:
+			break;
+		case "platform":
+			build_platform.platform = n.values.map!(e => e.toString()).array();
+			break;
+		case "architecture":
+			build_platform.architecture = n.values.map!(e => e.toString()).array();
+			break;
+		case "compiler":
+			build_platform.compiler = n.values.front.toString();
+			break;
+		case "frontendVersion":
+			build_platform.frontendVersion = n.values.front.toString()
+				.filter!((e) => e >= '0' && e <= '9').array().to!string
+				.to!int;
+			break;
+		}
+	}
 	return build_platform;
 }
diff --git a/source/dub/data/platform.d b/source/dub/data/platform.d
deleted file mode 100644
index fb0870e..0000000
--- a/source/dub/data/platform.d
+++ /dev/null
@@ -1,128 +0,0 @@
-/*******************************************************************************
-
-    Represent a target platform
-
-    Platform informations can be embedded in recipe, such that some settings
-    only target a certain platform (e.g. sourceFiles, lflags, etc...).
-    The struct in this module represent that information, structured.
-
-*******************************************************************************/
-
-module dub.data.platform;
-
-/// Represents a platform a package can be build upon.
-struct BuildPlatform {
-	/// Special constant used to denote matching any build platform.
-	enum any = BuildPlatform(null, null, null, null, -1);
-
-	/// Platform identifiers, e.g. ["posix", "windows"]
-	string[] platform;
-	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
-	string[] architecture;
-	/// Canonical compiler name e.g. "dmd"
-	string compiler;
-	/// Compiler binary name e.g. "ldmd2"
-	string compilerBinary;
-	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
-	int frontendVersion;
-	/// Compiler version e.g. "1.11.0"
-	string compilerVersion;
-	/// Frontend version string from frontendVersion
-	/// e.g: 2067 => "2.067"
-	string frontendVersionString() const
-	{
-		import std.format : format;
-
-		const maj = frontendVersion / 1000;
-		const min = frontendVersion % 1000;
-		return format("%d.%03d", maj, min);
-	}
-	///
-	unittest
-	{
-		BuildPlatform bp;
-		bp.frontendVersion = 2067;
-		assert(bp.frontendVersionString == "2.067");
-	}
-
-	/// Checks to see if platform field contains windows
-	bool isWindows() const {
-		import std.algorithm : canFind;
-		return this.platform.canFind("windows");
-	}
-	///
-	unittest {
-		BuildPlatform bp;
-		bp.platform = ["windows"];
-		assert(bp.isWindows);
-		bp.platform = ["posix"];
-		assert(!bp.isWindows);
-	}
-}
-
-/** Matches a platform specification string against a build platform.
-
-	Specifications are build upon the following scheme, where each component
-	is optional (indicated by []), but the order is obligatory:
-	"[-platform][-architecture][-compiler]"
-
-	So the following strings are valid specifications: `"-windows-x86-dmd"`,
-	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
-
-	Params:
-		platform = The build platform to match against the platform specification
-	    specification = The specification being matched. It must either be an
-			empty string or start with a dash.
-
-	Returns:
-	    `true` if the given specification matches the build platform, `false`
-	    otherwise. Using an empty string as the platform specification will
-	    always result in a match.
-*/
-bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
-{
-    import std.range : empty;
-	import std.string : chompPrefix, format;
-	import std.algorithm : canFind, splitter;
-	import std.exception : enforce;
-
-	if (specification.empty) return true;
-	if (platform == BuildPlatform.any) return true;
-
-	auto splitted = specification.chompPrefix("-").splitter('-');
-	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
-
-	if (platform.platform.canFind(splitted.front)) {
-		splitted.popFront();
-		if (splitted.empty)
-			return true;
-	}
-	if (platform.architecture.canFind(splitted.front)) {
-		splitted.popFront();
-		if (splitted.empty)
-			return true;
-	}
-	if (platform.compiler == splitted.front) {
-		splitted.popFront();
-		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
-		return true;
-	}
-	return false;
-}
-
-///
-unittest {
-	auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
-	assert(platform.matchesSpecification(""));
-	assert(platform.matchesSpecification("posix"));
-	assert(platform.matchesSpecification("linux"));
-	assert(platform.matchesSpecification("linux-dmd"));
-	assert(platform.matchesSpecification("linux-x86_64-dmd"));
-	assert(platform.matchesSpecification("x86_64"));
-	assert(!platform.matchesSpecification("windows"));
-	assert(!platform.matchesSpecification("ldc"));
-	assert(!platform.matchesSpecification("windows-dmd"));
-
-	// Before PR#2279, a leading '-' was required
-	assert(platform.matchesSpecification("-x86_64"));
-}
diff --git a/source/dub/dependency.d b/source/dub/dependency.d
index c0562fc..fc444e0 100644
--- a/source/dub/dependency.d
+++ b/source/dub/dependency.d
@@ -18,12 +18,89 @@
 import std.exception;
 import std.string;
 
+/// Represents a fully-qualified package name
+public struct PackageName
+{
+	/// The underlying full name of the package
+	private string fullName;
+	/// Where the separator lies, if any
+	private size_t separator;
+
+	/// Creates a new instance of this struct
+	public this(string fn) @safe pure
+	{
+		this.fullName = fn;
+		if (auto idx = fn.indexOf(':'))
+			this.separator = idx > 0 ? idx : fn.length;
+		else // We were given `:foo`
+			assert(0, "Argument to PackageName constructor needs to be " ~
+				"a fully qualified string");
+	}
+
+	/// Private constructor to have nothrow / @nogc
+	private this(string fn, size_t sep) @safe pure nothrow @nogc
+	{
+		this.fullName = fn;
+		this.separator = sep;
+	}
+
+	/// The base package name in which the subpackages may live
+	public PackageName main () const return @safe pure nothrow @nogc
+	{
+		return PackageName(this.fullName[0 .. this.separator], this.separator);
+	}
+
+	/// The subpackage name, or an empty string if there isn't
+	public string sub () const return @safe pure nothrow @nogc
+	{
+		// Return `null` instead of an empty string so that
+		// it can be used in a boolean context, e.g.
+		// `if (name.sub)` would be true with empty string
+		return this.separator < this.fullName.length
+			? this.fullName[this.separator + 1 .. $]
+			: null;
+	}
+
+	/// Human readable representation
+	public string toString () const return scope @safe pure nothrow @nogc
+	{
+		return this.fullName;
+	}
+
+    ///
+    public int opCmp (in PackageName other) const scope @safe pure nothrow @nogc
+    {
+        import core.internal.string : dstrcmp;
+        return dstrcmp(this.toString(), other.toString());
+    }
+
+    ///
+    public bool opEquals (in PackageName other) const scope @safe pure nothrow @nogc
+    {
+        return this.toString() == other.toString();
+    }
+}
 
 /** Encapsulates the name of a package along with its dependency specification.
 */
 struct PackageDependency {
+	/// Backward compatibility
+	deprecated("Use the constructor that accepts a `PackageName` as first argument")
+	this(string n, Dependency s = Dependency.init) @safe pure
+	{
+		this.name = PackageName(n);
+		this.spec = s;
+	}
+
+	// Remove once deprecated overload is gone
+	this(PackageName n, Dependency s = Dependency.init) @safe pure nothrow @nogc
+	{
+		this.name = n;
+		this.spec = s;
+	}
+
 	/// Name of the referenced package.
-	string name;
+	PackageName name;
 
 	/// Dependency specification used to select a particular version of the package.
 	Dependency spec;
@@ -47,14 +124,19 @@
 	// Shortcut to create >=0.0.0
 	private enum ANY_IDENT = "*";
 
-	private Value m_value;
+	private Value m_value = Value(VersionRange.Invalid);
 	private bool m_optional;
 	private bool m_default;
 
 	/// A Dependency, which matches every valid version.
-	static @property Dependency any() @safe { return Dependency(VersionRange.Any); }
+	public static immutable Dependency Any = Dependency(VersionRange.Any);
 
 	/// An invalid dependency (with no possible version matches).
+	public static immutable Dependency Invalid = Dependency(VersionRange.Invalid);
+
+	deprecated("Use `Dependency.Any` instead")
+	static @property Dependency any() @safe { return Dependency(VersionRange.Any); }
+	deprecated("Use `Dependency.Invalid` instead")
 	static @property Dependency invalid() @safe
 	{
 		return Dependency(VersionRange.Invalid);
@@ -433,11 +515,11 @@
 	*/
 	Dependency merge(ref const(Dependency) o) const @trusted {
 		alias Merger = match!(
-			(const NativePath a, const NativePath b) => a == b ? this : invalid,
+			(const NativePath a, const NativePath b) => a == b ? this : Invalid,
 			(const NativePath a,       any         ) => o,
 			(      any         , const NativePath b) => this,
 
-			(const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : invalid,
+			(const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : Invalid,
 			(const Repository a,       any         ) => this,
 			(      any         , const Repository b) => o,
 
@@ -447,7 +529,7 @@
 
 				VersionRange copy = a;
 				copy.merge(b);
-				if (!copy.isValid()) return invalid;
+				if (!copy.isValid()) return Invalid;
 				return Dependency(copy);
 			}
 		);
@@ -605,7 +687,7 @@
 	assert(a.valid);
 	assert(a.version_ == Version("~d2test"));
 
-	a = Dependency.any;
+	a = Dependency.Any;
 	assert(!a.optional);
 	assert(a.valid);
 	assertThrown(a.version_);
diff --git a/source/dub/dependencyresolver.d b/source/dub/dependencyresolver.d
index d064c61..8848814 100644
--- a/source/dub/dependencyresolver.d
+++ b/source/dub/dependencyresolver.d
@@ -61,7 +61,7 @@
 		possible configurations for the target package.
 	*/
 	static struct TreeNodes {
-		string pack;
+		PackageName pack;
 		CONFIGS configs;
 		DependencyType depType = DependencyType.required;
 
@@ -83,7 +83,7 @@
 		Nodes are a combination of a package and a single package configuration.
 	*/
 	static struct TreeNode {
-		string pack;
+		PackageName pack;
 		CONFIG config;
 
 		size_t toHash() const nothrow @trusted {
@@ -99,9 +99,9 @@
 		}
 	}
 
-	CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true)
+	CONFIG[PackageName] resolve(TreeNode root, bool throw_on_failure = true)
 	{
-		auto rootbase = root.pack.basePackageName;
+		auto rootbase = root.pack.main;
 
 		// build up the dependency graph, eliminating as many configurations/
 		// versions as possible
@@ -124,8 +124,8 @@
 		return context.result;
 	}
 
-	protected abstract CONFIG[] getAllConfigs(string pack);
-	protected abstract CONFIG[] getSpecificConfigs(string pack, TreeNodes nodes);
+	protected abstract CONFIG[] getAllConfigs(in PackageName pack);
+	protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes);
 	protected abstract TreeNodes[] getChildren(TreeNode node);
 	protected abstract bool matches(CONFIGS configs, CONFIG config);
 
@@ -139,19 +139,19 @@
 
 			The key is the qualified name of the package (base + sub)
 		*/
-		void[0][string] visited;
+		void[0][PackageName] visited;
 
 		/// The finally chosen configurations for each package
-		CONFIG[string] result;
+		CONFIG[PackageName] result;
 
 		/// The set of available configurations for each package
-		ResolveConfig[][string] configs;
+		ResolveConfig[][PackageName] configs;
 
 		/// Determines if a certain package has already been processed
-		bool isVisited(string package_) const { return (package_ in visited) !is null; }
+		bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; }
 
 		/// Marks a package as processed
-		void setVisited(string package_) { visited[package_] = (void[0]).init; }
+		void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; }
 
 		/// Returns a deep clone
 		ResolveContext clone()
@@ -172,7 +172,7 @@
 	*/
 	private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations)
 	{
-		auto base = n.pack.basePackageName;
+		auto base = n.pack.main;
 		assert(base in context.configs);
 		if (context.isVisited(n.pack)) return;
 		context.setVisited(n.pack);
@@ -184,7 +184,7 @@
 
 		foreach (dep; dependencies) {
 			// lazily load all dependency configurations
-			auto depbase = dep.pack.basePackageName;
+			auto depbase = dep.pack.main;
 			auto di = depbase in context.configs;
 			if (!di) {
 				context.configs[depbase] =
@@ -238,7 +238,7 @@
 			~ " recipe that reproduces this error.");
 
 		auto dep = &dependencies[depidx];
-		auto depbase = dep.pack.basePackageName;
+		auto depbase = dep.pack.main;
 		auto depconfigs = context.configs[depbase];
 
 		Exception first_err;
@@ -277,18 +277,18 @@
 			dep.pack, dep.configs, n.pack, n.config));
 	}
 
-	private void purgeOptionalDependencies(TreeNode root, ref CONFIG[string] configs)
+	private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs)
 	{
-		bool[string] required;
-		bool[string] visited;
+		bool[PackageName] required;
+		bool[PackageName] visited;
 
 		void markRecursively(TreeNode node)
 		{
 			if (node.pack in visited) return;
 			visited[node.pack] = true;
-			required[node.pack.basePackageName] = true;
+			required[node.pack.main] = true;
 			foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
-				if (auto dp = dep.pack.basePackageName in configs)
+				if (auto dp = dep.pack.main in configs)
 					markRecursively(TreeNode(dep.pack, *dp));
 		}
 
@@ -305,23 +305,23 @@
 		import std.range : chain, only;
 		import std.typecons : tuple;
 
-		string failedNode;
+		PackageName failedNode;
 
 		this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__)
 		{
-			auto m = format("Unresolvable dependencies to package %s:", dep.pack.basePackageName);
+			auto m = format("Unresolvable dependencies to package %s:", dep.pack.main);
 			super(m, file, line);
 
 			this.failedNode = dep.pack;
 
-			auto failbase = failedNode.basePackageName;
+			auto failbase = failedNode.main;
 
 			// get the list of all dependencies to the failed package
 			auto deps = context.visited.byKey
-				.filter!(p => p.basePackageName in context.result)
-				.map!(p => TreeNode(p, context.result[p.basePackageName]))
+				.filter!(p => !!(p.main in context.result))
+				.map!(p => TreeNode(p, context.result[p.main]))
 				.map!(n => getChildren(n)
-					.filter!(d => d.pack.basePackageName == failbase)
+					.filter!(d => d.pack.main == failbase)
 					.map!(d => tuple(n, d))
 				)
 				.join
@@ -329,7 +329,7 @@
 
 			foreach (d; deps) {
 				// filter out trivial self-dependencies
-				if (d[0].pack.basePackageName == failbase
+				if (d[0].pack.main == failbase
 					&& matches(d[1].configs, d[0].config))
 					continue;
 				msg ~= format("\n  %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs);
@@ -359,12 +359,6 @@
 	optional
 }
 
-private string basePackageName(string p)
-{
-	import std.algorithm.searching : findSplit;
-	return p.findSplit(":")[0];
-}
-
 unittest {
 	static struct IntConfig {
 		int value;
@@ -377,119 +371,130 @@
 		alias configs this;
 	}
 	static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
+	static PackageName pn(string name)		{ return PackageName(name); }
 
 	static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
 		private TreeNodes[][string] m_children;
 		this(TreeNodes[][string] children) { super(ulong.max); m_children = children; }
-		protected override IntConfig[] getAllConfigs(string pack) {
+		protected override IntConfig[] getAllConfigs(in PackageName pack) {
 			auto ret = appender!(IntConfig[]);
-			foreach (p; m_children.byKey) {
-				if (p.length <= pack.length+1) continue;
-				if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue;
-				auto didx = p.lastIndexOf(':');
-				ret ~= ic(p[didx+1 .. $].to!uint);
+			foreach (p_; m_children.byKey) {
+				// Note: We abuse subpackage notation to store configs
+				const p = PackageName(p_);
+				if (p.main != pack.main) continue;
+				ret ~= ic(p.sub.to!uint);
 			}
 			ret.data.sort!"a>b"();
 			return ret.data;
 		}
-		protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; }
-		protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); }
+		protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; }
+		protected override TreeNodes[] getChildren(TreeNode node) {
+			assert(node.pack.sub.length == 0);
+			return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null);
+		}
 		protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
 	}
 
 	// properly back up if conflicts are detected along the way (d:2 vs d:1)
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))],
-			"b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))],
-			"b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))],
+			"a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))],
+			"b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))],
+			"b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))],
 			"c:1": [], "c:2": [], "c:3": [],
 			"d:1": [], "d:2": [],
 			"e:1": [], "e:2": [],
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2), "c":ic(3), "d":ic(1), "e":ic(2)], format("%s", res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)],
+			format("%s", res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// handle cyclic dependencies gracefully
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]))],
-			"b:1": [TreeNodes("b", ics([ic(1)]))]
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))],
+			"b:1": [TreeNodes(pn("b"), ics([ic(1)]))]
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]);
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]);
 	}
 
 	// don't choose optional dependencies by default
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
 			"b:1": []
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// choose default optional dependencies by default
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)],
 			"b:1": []
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)],
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// choose optional dependency if non-optional within the dependency tree
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))],
 			"b:1": [],
-			"c:1": [TreeNodes("b", ics([ic(1)]))]
+			"c:1": [TreeNodes(pn("b"), ics([ic(1)]))]
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)],
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// don't choose optional dependency if non-optional outside of final dependency tree
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
 			"b:1": [],
-			"preset:0": [TreeNodes("b", ics([ic(1)]))]
+			"preset:0": [TreeNodes(pn("b"), ics([ic(1)]))]
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// don't choose optional dependency if non-optional in a non-selected version
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
-			"b:1": [TreeNodes("c", ics([ic(1)]))],
-			"b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
+			"b:1": [TreeNodes(pn("c"), ics([ic(1)]))],
+			"b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)],
 			"c:1": []
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
-			"b:1": [TreeNodes("c", ics([ic(2)]))],
-			"b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
+			"b:1": [TreeNodes(pn("c"), ics([ic(2)]))],
+			"b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)],
 			"c:1": []
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
+			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
 	}
 
 	// check error message for multiple conflicting dependencies
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)])), TreeNodes("c", ics([ic(1)]))],
-			"b:1": [TreeNodes("d", ics([ic(1)]))],
-			"c:1": [TreeNodes("d", ics([ic(2)]))],
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))],
+			"b:1": [TreeNodes(pn("d"), ics([ic(1)]))],
+			"c:1": [TreeNodes(pn("d"), ics([ic(2)]))],
 			"d:1": [],
 			"d:2": []
 		]);
 		try {
-			res.resolve(TreeNode("a", ic(0)));
+			res.resolve(TreeNode(pn("a"), ic(0)));
 			assert(false, "Expected resolve to throw.");
 		} catch (ResolveException e) {
 			assert(e.msg ==
@@ -502,10 +507,10 @@
 	// check error message for invalid dependency
 	with (TestResolver) {
 		auto res = new TestResolver([
-			"a:0": [TreeNodes("b", ics([ic(1)]))]
+			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))]
 		]);
 		try {
-			res.resolve(TreeNode("a", ic(0)));
+			res.resolve(TreeNode(pn("a"), ic(0)));
 			assert(false, "Expected resolve to throw.");
 		} catch (DependencyLoadException e) {
 			assert(e.msg == "Failed to find any versions for package b, referenced by a 0");
@@ -516,12 +521,12 @@
 	with (TestResolver) {
 		auto res = new TestResolver([
 			"a:0": [
-				TreeNodes("b", ics([ic(2)]), DependencyType.optional),
-				TreeNodes("c", ics([ic(1)]))
+				TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional),
+				TreeNodes(pn("c"), ics([ic(1)]))
 			],
 			"b:1": [],
 			"c:1": []
 		]);
-		assert(res.resolve(TreeNode("a", ic(0))) == ["c":ic(1)]);
+		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]);
 	}
 }
diff --git a/source/dub/dub.d b/source/dub/dub.d
index 19c97e1..69518ab 100644
--- a/source/dub/dub.d
+++ b/source/dub/dub.d
@@ -64,7 +64,7 @@
 PackageSupplier[] defaultPackageSuppliers()
 {
 	logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]);
-	return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)];
+	return [new FallbackPackageSupplier(defaultRegistryURLs.map!_getRegistryPackageSupplier.array)];
 }
 
 /** Returns a registry package supplier according to protocol.
@@ -74,6 +74,13 @@
 deprecated("This function wasn't intended for public use - open an issue with Dub if you need it")
 PackageSupplier getRegistryPackageSupplier(string url)
 {
+    return _getRegistryPackageSupplier(url);
+}
+
+// Private to avoid a bug in `defaultPackageSuppliers` with `map` triggering a deprecation
+// even though the context is deprecated.
+private PackageSupplier _getRegistryPackageSupplier(string url)
+{
 	switch (url.startsWith("dub+", "mvn+", "file://"))
 	{
 		case 1:
@@ -205,7 +212,7 @@
 	 * generally in a library setup, one may wish to provide a custom
 	 * implementation, which can be done by overriding this method.
 	 */
-	protected PackageManager makePackageManager() const
+	protected PackageManager makePackageManager()
 	{
 		return new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false);
 	}
@@ -361,15 +368,15 @@
 		import dub.test.base : TestDub;
 
 		scope (exit) environment.remove("DUB_REGISTRY");
-		auto dub = new TestDub(".", null, SkipPackageSuppliers.configured);
+		auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured);
 		assert(dub.packageSuppliers.length == 0);
 		environment["DUB_REGISTRY"] = "http://example.com/";
-		dub = new TestDub(".", null, SkipPackageSuppliers.configured);
+		dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured);
 		assert(dub.packageSuppliers.length == 1);
 		environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/";
-		dub = new TestDub(".", null, SkipPackageSuppliers.configured);
+		dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured);
 		assert(dub.packageSuppliers.length == 2);
-		dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured);
+		dub = new TestDub(null, ".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured);
 		assert(dub.packageSuppliers.length == 3);
 
 		dub = new TestDub();
@@ -499,7 +506,9 @@
 	/// Loads the package from the specified path as the main project package.
 	void loadPackage(NativePath path)
 	{
-		m_project = new Project(m_packageManager, path);
+		auto pack = this.m_packageManager.getOrLoadPackage(
+			path, NativePath.init, false, StrictMode.Warn);
+		this.loadPackage(pack);
 	}
 
 	/// Loads a specific package as the main project package (can be a sub package)
@@ -575,7 +584,8 @@
 		recipe_content = recipe_content[idx+1 .. $];
 		auto recipe_default_package_name = path.toString.baseName.stripExtension.strip;
 
-		auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name);
+		const PackageName empty;
+		auto recipe = parsePackageRecipe(recipe_content, recipe_filename, empty, recipe_default_package_name);
 		enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files.");
 		enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths.");
 		enforce(recipe.buildSettings.cSourcePaths.length == 0, "Single-file packages are not allowed to specify C source paths.");
@@ -621,20 +631,21 @@
 		if (!(options & UpgradeOptions.upgrade)) {
 			next_pack:
 			foreach (p; m_project.selections.selectedPackages) {
-				auto dep = m_project.selections.getSelectedVersion(p);
+				const name = PackageName(p); // Always a main package name
+				auto dep = m_project.selections.getSelectedVersion(name);
 				if (!dep.path.empty) {
 					auto path = dep.path;
 					if (!path.absolute) path = this.rootPath ~ path;
 					try if (m_packageManager.getOrLoadPackage(path)) continue;
 					catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); }
 				} else if (!dep.repository.empty) {
-					if (m_packageManager.loadSCMPackage(getBasePackageName(p), dep.repository))
+					if (m_packageManager.loadSCMPackage(name, dep.repository))
 						continue;
 				} else {
-					if (m_packageManager.getPackage(p, dep.version_)) continue;
+					if (m_packageManager.getPackage(name, dep.version_)) continue;
 					foreach (ps; m_packageSuppliers) {
 						try {
-							auto versions = ps.getVersions(p);
+							auto versions = ps.getVersions(name);
 							if (versions.canFind!(v => dep.matches(v, VersionMatchMode.strict)))
 								continue next_pack;
 						} catch (Exception e) {
@@ -645,23 +656,23 @@
 				}
 
 				logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep);
-				m_project.selections.deselectVersion(p);
+				m_project.selections.deselectVersion(name);
 			}
 		}
 
 		auto resolver = new DependencyVersionResolver(
 			this, options, m_project.rootPackage, m_project.selections);
-		Dependency[string] versions = resolver.resolve(packages_to_upgrade);
+		Dependency[PackageName] versions = resolver.resolve(packages_to_upgrade);
 
 		if (options & UpgradeOptions.dryRun) {
 			bool any = false;
-			string rootbasename = getBasePackageName(m_project.rootPackage.name);
+			string rootbasename = PackageName(m_project.rootPackage.name).main.toString();
 
 			foreach (p, ver; versions) {
 				if (!ver.path.empty || !ver.repository.empty) continue;
 
-				auto basename = getBasePackageName(p);
-				if (basename == rootbasename) continue;
+				auto basename = p.main;
+				if (basename.toString() == rootbasename) continue;
 
 				if (!m_project.selections.hasSelectedVersion(basename)) {
 					logInfo("Upgrade", Color.cyan,
@@ -674,15 +685,15 @@
 				if (ver.version_ <= sver.version_) continue;
 				logInfo("Upgrade", Color.cyan,
 					"%s would be upgraded from %s to %s.",
-					basename.color(Mode.bold), sver, ver);
+					basename.toString().color(Mode.bold), sver, ver);
 				any = true;
 			}
 			if (any) logInfo("Use \"%s\" to perform those changes", "dub upgrade".color(Mode.bold));
 			return;
 		}
 
-		foreach (p, ver; versions) {
-			assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p);
+		foreach (name, ver; versions) {
+			assert(!name.sub, "Resolved packages contain a sub package!?: " ~ name.toString());
 			Package pack;
 			if (!ver.path.empty) {
 				try pack = m_packageManager.getOrLoadPackage(ver.path);
@@ -691,15 +702,15 @@
 					continue;
 				}
 			} else if (!ver.repository.empty) {
-				pack = m_packageManager.loadSCMPackage(p, ver.repository);
+				pack = m_packageManager.loadSCMPackage(name, ver.repository);
 			} else {
 				assert(ver.isExactVersion, "Resolved dependency is neither path, nor repository, nor exact version based!?");
-				pack = m_packageManager.getPackage(p, ver.version_);
+				pack = m_packageManager.getPackage(name, ver.version_);
 				if (pack && m_packageManager.isManagedPackage(pack)
 					&& ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0)
 				{
 					// TODO: only re-install if there is actually a new commit available
-					logInfo("Re-installing branch based dependency %s %s", p, ver.toString());
+					logInfo("Re-installing branch based dependency %s %s", name, ver.toString());
 					m_packageManager.remove(pack);
 					pack = null;
 				}
@@ -707,16 +718,16 @@
 
 			FetchOptions fetchOpts;
 			fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none;
-			if (!pack) fetch(p, ver.version_, defaultPlacementLocation, fetchOpts, "getting selected version");
-			if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) {
+			if (!pack) this.fetch(name, ver.version_, fetchOpts, defaultPlacementLocation, "getting selected version");
+			if ((options & UpgradeOptions.select) && name.toString() != m_project.rootPackage.name) {
 				if (!ver.repository.empty) {
-					m_project.selections.selectVersion(p, ver.repository);
+					m_project.selections.selectVersion(name, ver.repository);
 				} else if (ver.path.empty) {
-					m_project.selections.selectVersion(p, ver.version_);
+					m_project.selections.selectVersion(name, ver.version_);
 				} else {
 					NativePath relpath = ver.path;
 					if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path);
-					m_project.selections.selectVersion(p, relpath);
+					m_project.selections.selectVersion(name, relpath);
 				}
 			}
 		}
@@ -783,12 +794,12 @@
 	{
 		if (m_dryRun) return;
 
-		auto tool = "dscanner";
+		auto tool = PackageName("dscanner");
 
 		auto tool_pack = m_packageManager.getBestPackage(tool);
 		if (!tool_pack) {
-			logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it user wide", tool);
-			tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
+			logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it locally", tool);
+			tool_pack = this.fetch(tool);
 		}
 
 		auto dscanner_dub = new Dub(null, m_packageSuppliers);
@@ -878,7 +889,6 @@
 			rmdirRecurse(cache.toNativeString());
 	}
 
-	/// Fetches the package matching the dependency and places it in the specified location.
 	deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument")
 	Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "")
 	{
@@ -889,41 +899,100 @@
 		return this.fetch(packageId, vrange, location, options, reason);
 	}
 
-	/// Ditto
-	Package fetch(string packageId, in Version vers, PlacementLocation location, FetchOptions options, string reason = "")
+	deprecated("Use `fetch(PackageName, Version, [FetchOptions, PlacementLocation, string])`")
+	Package fetch(string name, in Version vers, PlacementLocation location, FetchOptions options, string reason = "")
 	{
-		return this.fetch(packageId, VersionRange(vers, vers), location, options, reason);
+		const n = PackageName(name);
+		return this.fetch(n, VersionRange(vers, vers), options, location, reason);
+	}
+
+	deprecated("Use `fetch(PackageName, VersionRange, [FetchOptions, PlacementLocation, string])`")
+	Package fetch(string name, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "")
+	{
+		const n = PackageName(name);
+		return this.fetch(n, range, options, location, reason);
+	}
+
+	/**
+	 * Fetches a missing package and stores it locally
+	 *
+	 * This will query the configured PackageSuppliers for a package
+	 * matching the `range` specification, store it locally, and load
+	 * it in the `PackageManager`. Note that unlike the command line
+	 * version, this function is not idempotent and will remove an
+	 * existing package and re-download it.
+	 *
+	 * Params:
+	 *   name = Name of the package to retrieve. Subpackages will lead
+	 *          to the main package being retrieved and the subpackage
+	 *          being returned (if it exists).
+	 *   vers = For `Version` overloads, the exact version to return.
+	 *   range = The `VersionRange` to match. Default to `Any` to fetch
+	 *           the latest version.
+	 *   options = A set of options used for fetching / matching versions.
+	 *   location = Where to store the retrieved package. Default to the
+	 *              configured `defaultPlacementLocation`.
+	 *   reason = Optionally, the reason for retriving this package.
+	 *            This is used only for logging.
+	 *
+	 * Returns:
+	 *	 The fetched or loaded `Package`, or `null` in dry-run mode.
+	 *
+	 * Throws:
+	 *	 If the package cannot be fetched or loaded.
+	 */
+	Package fetch(in PackageName name, in Version vers,
+		FetchOptions options = FetchOptions.none, string reason = "")
+	{
+		return this.fetch(name, VersionRange(vers, vers), options,
+			this.defaultPlacementLocation, reason);
 	}
 
 	/// Ditto
-	Package fetch(string packageId, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "")
+	Package fetch(in PackageName name, in Version vers, FetchOptions options,
+		PlacementLocation location, string reason = "")
 	{
-		auto basePackageName = getBasePackageName(packageId);
+		return this.fetch(name, VersionRange(vers, vers), options,
+			this.defaultPlacementLocation, reason);
+	}
+
+	/// Ditto
+	Package fetch(in PackageName name, in VersionRange range = VersionRange.Any,
+		FetchOptions options = FetchOptions.none, string reason = "")
+	{
+		return this.fetch(name, range, options, this.defaultPlacementLocation, reason);
+	}
+
+	/// Ditto
+	Package fetch(in PackageName name, in VersionRange range, FetchOptions options,
+		PlacementLocation location, string reason = "")
+	{
 		Json pinfo;
 		PackageSupplier supplier;
 		foreach(ps; m_packageSuppliers){
 			try {
-				pinfo = ps.fetchPackageRecipe(basePackageName, range, (options & FetchOptions.usePrerelease) != 0);
+				pinfo = ps.fetchPackageRecipe(name.main, range, (options & FetchOptions.usePrerelease) != 0);
 				if (pinfo.type == Json.Type.null_)
 					continue;
 				supplier = ps;
 				break;
 			} catch(Exception e) {
-				logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg);
+				logWarn("Package %s not found for %s: %s", name, ps.description, e.msg);
 				logDebug("Full error: %s", e.toString().sanitize());
 			}
 		}
 		enforce(!pinfo.type.among(Json.Type.undefined, Json.Type.null_),
-				"No package " ~ packageId ~ " was found matching the dependency " ~ range.toString());
+			"No package %s was found matching the dependency %s"
+				.format(name, range));
 		Version ver = Version(pinfo["version"].get!string);
 
 		// always upgrade branch based versions - TODO: actually check if there is a new commit available
-		Package existing = m_packageManager.getPackage(packageId, ver, location);
+		Package existing = m_packageManager.getPackage(name, ver, location);
 		if (options & FetchOptions.printOnly) {
 			if (existing && existing.version_ != ver)
 				logInfo("A new version for %s is available (%s -> %s). Run \"%s\" to switch.",
-					packageId.color(Mode.bold), existing, ver,
-					text("dub upgrade ", packageId).color(Mode.bold));
+                    name.toString().color(Mode.bold), existing, ver,
+					text("dub upgrade ", name.main).color(Mode.bold));
 			return null;
 		}
 
@@ -931,16 +1000,18 @@
 			if (!ver.isBranch() || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) {
 				// TODO: support git working trees by performing a "git pull" instead of this
 				logDiagnostic("Package %s %s (in %s packages) is already present with the latest version, skipping upgrade.",
-					packageId, ver, location.toString);
+					name, ver, location.toString);
 				return existing;
 			} else {
-				logInfo("Removing", Color.yellow, "%s %s to prepare replacement with a new version", packageId.color(Mode.bold), ver);
+				logInfo("Removing", Color.yellow, "%s %s to prepare " ~
+					"replacement with a new version", name.toString().color(Mode.bold),
+					ver);
 				if (!m_dryRun) m_packageManager.remove(existing);
 			}
 		}
 
-		if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", packageId.color(Mode.bold), ver, reason);
-		else logInfo("Fetching", Color.yellow, "%s %s", packageId.color(Mode.bold), ver);
+		if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", name.toString().color(Mode.bold), ver, reason);
+		else logInfo("Fetching", Color.yellow, "%s %s", name.toString().color(Mode.bold), ver);
 		if (m_dryRun) return null;
 
 		logDebug("Acquiring package zip file");
@@ -950,15 +1021,13 @@
 		{
 			import std.zip : ZipException;
 
-			auto path = getTempFile(basePackageName, ".zip");
-			supplier.fetchPackage(path, basePackageName, range, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail?
-			scope(exit) removeFile(path);
+			auto data = supplier.fetchPackage(name.main, range, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail?
 			logDiagnostic("Placing to %s...", location.toString());
 
 			try {
-				return m_packageManager.store(path, location, basePackageName, ver);
+				return m_packageManager.store(data, location, name.main, ver);
 			} catch (ZipException e) {
-				logInfo("Failed to extract zip archive for %s %s...", packageId, ver);
+				logInfo("Failed to extract zip archive for %s@%s...", name, ver);
 				// re-throw the exception at the end of the loop
 				if (i == 0)
 					throw e;
@@ -998,15 +1067,17 @@
 		is set to `RemoveVersionWildcard`.
 
 		Params:
-			package_id = Name of the package to be removed
+			name = Name of the package to be removed
 			location = Specifies the location to look for the given package
 				name/version.
 			resolve_version = Callback to select package version.
 	*/
-	void remove(string package_id, PlacementLocation location,
-				scope size_t delegate(in Package[] packages) resolve_version)
+	void remove(in PackageName name, PlacementLocation location,
+		scope size_t delegate(in Package[] packages) resolve_version)
 	{
-		enforce(!package_id.empty);
+		enforce(name.main.toString().length);
+		enforce(!name.sub.length, "Cannot remove subpackage %s, remove %s instead"
+			.format(name, name.main));
 		if (location == PlacementLocation.local) {
 			logInfo("To remove a locally placed package, make sure you don't have any data"
 					~ "\nleft in it's directory and then simply remove the whole directory.");
@@ -1016,16 +1087,13 @@
 		Package[] packages;
 
 		// Retrieve packages to be removed.
-		foreach(pack; m_packageManager.getPackageIterator(package_id))
+		foreach(pack; m_packageManager.getPackageIterator(name.toString()))
 			if (m_packageManager.isManagedPackage(pack))
 				packages ~= pack;
 
 		// Check validity of packages to be removed.
-		if(packages.empty) {
-			throw new Exception("Cannot find package to remove. ("
-				~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'"
-				~ ")");
-		}
+		enforce(!packages.empty, "Cannot find package '%s' to remove at %s location"
+			.format(name, location.toString()));
 
 		// Sort package list in ascending version order
 		packages.sort!((a, b) => a.version_ < b.version_);
@@ -1041,12 +1109,19 @@
 			try {
 				remove(pack);
 			} catch (Exception e) {
-				logError("Failed to remove %s %s: %s", package_id, pack, e.msg);
+				logError("Failed to remove %s %s: %s", name, pack, e.msg);
 				logInfo("Continuing with other packages (if any).");
 			}
 		}
 	}
 
+	deprecated("Use `remove(PackageName, PlacementLocation, delegate)`")
+	void remove(string name, PlacementLocation location,
+		scope size_t delegate(in Package[] packages) resolve_version)
+	{
+		this.remove(PackageName(name), location, resolve_version);
+	}
+
 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
 	deprecated("Use the overload without the 3rd argument (`force_remove`) instead")
 	void remove(string package_id, PlacementLocation location, bool force_remove,
@@ -1058,7 +1133,7 @@
 	/** Removes a specific version of a package.
 
 		Params:
-			package_id = Name of the package to be removed
+			name = Name of the package to be removed
 			version_ = Identifying a version or a wild card. If an empty string
 				is passed, the package will be removed from the location, if
 				there is only one version retrieved. This will throw an
@@ -1066,9 +1141,9 @@
 			location = Specifies the location to look for the given package
 				name/version.
 	 */
-	void remove(string package_id, string version_, PlacementLocation location)
+	void remove(in PackageName name, string version_, PlacementLocation location)
 	{
-		remove(package_id, location, (in packages) {
+		remove(name, location, (in packages) {
 			if (version_ == RemoveVersionWildcard || version_.empty)
 				return packages.length;
 
@@ -1076,12 +1151,17 @@
 				if (p.version_ == Version(version_))
 					return i;
 			}
-			throw new Exception("Cannot find package to remove. ("
-				~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'"
-				~ ")");
+			throw new Exception("Cannot find package '%s@%s' to remove at %s location"
+				.format(name, version_, location.toString()));
 		});
 	}
 
+	deprecated("Use `remove(PackageName, string, PlacementLocation)`")
+	void remove(string name, string version_, PlacementLocation location)
+	{
+		this.remove(PackageName(name), version_, location);
+	}
+
 	/// Compatibility overload. Use the version without a `force_remove` argument instead.
 	deprecated("Use the overload without force_remove instead")
 	void remove(string package_id, string version_, PlacementLocation location, bool force_remove)
@@ -1102,10 +1182,17 @@
 
 		See_Also: `removeLocalPackage`
 	*/
+	deprecated("Use `addLocalPackage(string, string, PlacementLocation)` instead")
 	void addLocalPackage(string path, string ver, bool system)
 	{
+		this.addLocalPackage(path, ver, system ? PlacementLocation.system : PlacementLocation.user);
+	}
+
+	/// Ditto
+	void addLocalPackage(string path, string ver, PlacementLocation loc)
+	{
 		if (m_dryRun) return;
-		m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? PlacementLocation.system : PlacementLocation.user);
+		this.m_packageManager.addLocalPackage(makeAbsolute(path), ver, loc);
 	}
 
 	/** Removes a directory from the list of locally known packages.
@@ -1119,10 +1206,17 @@
 
 		See_Also: `addLocalPackage`
 	*/
+	deprecated("Use `removeLocalPackage(string, string, PlacementLocation)` instead")
 	void removeLocalPackage(string path, bool system)
 	{
+		this.removeLocalPackage(path, system ? PlacementLocation.system : PlacementLocation.user);
+	}
+
+	/// Ditto
+	void removeLocalPackage(string path, PlacementLocation loc)
+	{
 		if (m_dryRun) return;
-		m_packageManager.removeLocalPackage(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
+		this.m_packageManager.removeLocalPackage(makeAbsolute(path), loc);
 	}
 
 	/** Registers a local directory to search for packages to use for satisfying
@@ -1135,10 +1229,17 @@
 
 		See_Also: `removeSearchPath`
 	*/
+	deprecated("Use `addSearchPath(string, PlacementLocation)` instead")
 	void addSearchPath(string path, bool system)
 	{
+		this.addSearchPath(path, system ? PlacementLocation.system : PlacementLocation.user);
+	}
+
+	/// Ditto
+	void addSearchPath(string path, PlacementLocation loc)
+	{
 		if (m_dryRun) return;
-		m_packageManager.addSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
+		this.m_packageManager.addSearchPath(makeAbsolute(path), loc);
 	}
 
 	/** Deregisters a local directory search path.
@@ -1150,10 +1251,17 @@
 
 		See_Also: `addSearchPath`
 	*/
+	deprecated("Use `removeSearchPath(string, PlacementLocation)` instead")
 	void removeSearchPath(string path, bool system)
 	{
+		this.removeSearchPath(path, system ? PlacementLocation.system : PlacementLocation.user);
+	}
+
+	/// Ditto
+	void removeSearchPath(string path, PlacementLocation loc)
+	{
 		if (m_dryRun) return;
-		m_packageManager.removeSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user);
+		this.m_packageManager.removeSearchPath(makeAbsolute(path), loc);
 	}
 
 	/** Queries all package suppliers with the given query string.
@@ -1189,12 +1297,11 @@
 
 		See_also: `getLatestVersion`
 	*/
-	Version[] listPackageVersions(string name)
+	Version[] listPackageVersions(in PackageName name)
 	{
 		Version[] versions;
-		auto basePackageName = getBasePackageName(name);
 		foreach (ps; this.m_packageSuppliers) {
-			try versions ~= ps.getVersions(basePackageName);
+			try versions ~= ps.getVersions(name);
 			catch (Exception e) {
 				logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg);
 			}
@@ -1202,6 +1309,13 @@
 		return versions.sort().uniq.array;
 	}
 
+	deprecated("Use `listPackageVersions(PackageName)`")
+	Version[] listPackageVersions(string name)
+	{
+		const n = PackageName(name);
+		return this.listPackageVersions(n);
+	}
+
 	/** Returns the latest available version for a particular package.
 
 		This function returns the latest numbered version of a package. If no
@@ -1209,21 +1323,30 @@
 		preferring "~master".
 
 		Params:
-			package_name = The name of the package in question.
+			name = The name of the package in question.
 			prefer_stable = If set to `true` (the default), returns the latest
 				stable version, even if there are newer pre-release versions.
 
 		See_also: `listPackageVersions`
 	*/
-	Version getLatestVersion(string package_name, bool prefer_stable = true)
+	Version getLatestVersion(in PackageName name, bool prefer_stable = true)
 	{
-		auto vers = listPackageVersions(package_name);
-		enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'.");
+		auto vers = this.listPackageVersions(name);
+		enforce(!vers.empty,
+			"Failed to find any valid versions for a package name of '%s'."
+			.format(name));
 		auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array;
 		if (prefer_stable && final_versions.length) return final_versions[$-1];
 		else return vers[$-1];
 	}
 
+	deprecated("Use `getLatestVersion(PackageName, bool)`")
+	Version getLatestVersion(string name, bool prefer_stable = true)
+	{
+		const n = PackageName(name);
+		return this.getLatestVersion(n, prefer_stable);
+	}
+
 	/** Initializes a directory with a package skeleton.
 
 		Params:
@@ -1247,8 +1370,9 @@
 		VersionRange[string] depVers;
 		string[] notFound; // keep track of any failed packages in here
 		foreach (dep; deps) {
+			const name = PackageName(dep);
 			try {
-				Version ver = getLatestVersion(dep);
+				Version ver = this.getLatestVersion(name);
 				if (ver.isBranch())
 					depVers[dep] = VersionRange(ver);
 				else
@@ -1277,13 +1401,19 @@
 		logInfo("Success", Color.green, "created empty project in %s", path.toNativeString().color(Mode.bold));
 	}
 
-	private void runCustomInitialization(NativePath path, string type, string[] runArgs)
+	/**
+	 * Run initialization code from a template project
+	 *
+	 * Looks up a project, then get its `init-exec` subpackage,
+	 * and run this to initialize the repository with a default structure.
+	 */
+	private void runCustomInitialization(NativePath path, string name, string[] runArgs)
 	{
-		string packageName = type;
-		auto template_pack = m_packageManager.getBestPackage(packageName);
+		auto name_ = PackageName(name);
+		auto template_pack = m_packageManager.getBestPackage(name_);
 		if (!template_pack) {
-			logInfo("%s is not present, getting and storing it user wide", packageName);
-			template_pack = fetch(packageName, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
+			logInfo("%s is not present, getting and storing it locally", name);
+			template_pack = fetch(name_);
 		}
 
 		Package initSubPackage = m_packageManager.getSubPackage(template_pack, "init-exec", false);
@@ -1344,13 +1474,14 @@
 		if (m_dryRun) return;
 
 		// allow to choose a custom ddox tool
-		auto tool = m_project.rootPackage.recipe.ddoxTool;
-		if (tool.empty) tool = "ddox";
+		auto tool = m_project.rootPackage.recipe.ddoxTool.empty
+			? PackageName("ddox")
+			: PackageName(m_project.rootPackage.recipe.ddoxTool);
 
 		auto tool_pack = m_packageManager.getBestPackage(tool);
 		if (!tool_pack) {
 			logInfo("%s is not present, getting and storing it user wide", tool);
-			tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none);
+			tool_pack = this.fetch(tool);
 		}
 
 		auto ddox_dub = new Dub(null, m_packageSuppliers);
@@ -1515,7 +1646,7 @@
 		import dub.test.base : TestDub;
 		import std.path: buildPath, absolutePath;
 
-		auto dub = new TestDub(".", null, SkipPackageSuppliers.configured);
+		auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured);
 		immutable olddc = environment.get("DC", null);
 		immutable oldpath = environment.get("PATH", null);
 		immutable testdir = "test-determineDefaultCompiler";
@@ -1594,11 +1725,11 @@
 	protected {
 		Dub m_dub;
 		UpgradeOptions m_options;
-		Dependency[][string] m_packageVersions;
+		Dependency[][PackageName] m_packageVersions;
 		Package[string] m_remotePackages;
 		SelectedVersions m_selectedVersions;
 		Package m_rootPackage;
-		bool[string] m_packagesToUpgrade;
+		bool[PackageName] m_packagesToUpgrade;
 		Package[PackageDependency] m_packages;
 		TreeNodes[][TreeNode] m_children;
 	}
@@ -1621,20 +1752,20 @@
 		m_selectedVersions = selected_versions;
 	}
 
-	Dependency[string] resolve(string[] filter)
+	Dependency[PackageName] resolve(string[] filter)
 	{
 		foreach (name; filter)
-			m_packagesToUpgrade[name] = true;
-		return super.resolve(TreeNode(m_rootPackage.name, Dependency(m_rootPackage.version_)),
+			m_packagesToUpgrade[PackageName(name)] = true;
+		return super.resolve(TreeNode(PackageName(m_rootPackage.name), Dependency(m_rootPackage.version_)),
 			(m_options & UpgradeOptions.dryRun) == 0);
 	}
 
-	protected bool isFixedPackage(string pack)
+	protected bool isFixedPackage(in PackageName pack)
 	{
 		return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade;
 	}
 
-	protected override Dependency[] getAllConfigs(string pack)
+	protected override Dependency[] getAllConfigs(in PackageName pack)
 	{
 		if (auto pvers = pack in m_packageVersions)
 			return *pvers;
@@ -1648,7 +1779,7 @@
 
 		logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length);
 		Version[] versions;
-		foreach (p; m_dub.packageManager.getPackageIterator(pack))
+		foreach (p; m_dub.packageManager.getPackageIterator(pack.toString()))
 			versions ~= p.version_;
 
 		foreach (ps; m_dub.m_packageSuppliers) {
@@ -1690,7 +1821,7 @@
 		return ret;
 	}
 
-	protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes)
+	protected override Dependency[] getSpecificConfigs(in PackageName pack, TreeNodes nodes)
 	{
 		if (!nodes.configs.path.empty || !nodes.configs.repository.empty) {
 			if (getPackage(nodes.pack, nodes.configs)) return [nodes.configs];
@@ -1722,18 +1853,18 @@
 		auto basepack = pack.basePackage;
 
 		foreach (d; pack.getAllDependenciesRange()) {
-			auto dbasename = getBasePackageName(d.name);
+			auto dbasename = d.name.main.toString();
 
 			// detect dependencies to the root package (or sub packages thereof)
 			if (dbasename == basepack.name) {
 				auto absdeppath = d.spec.mapToPath(pack.path).path;
 				absdeppath.endsWithSlash = true;
-				auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true);
+				auto subpack = m_dub.m_packageManager.getSubPackage(basepack, d.name.sub, true);
 				if (subpack) {
 					auto desireddeppath = basepack.path;
 					desireddeppath.endsWithSlash = true;
 
-					auto altdeppath = d.name == dbasename ? basepack.path : subpack.path;
+					auto altdeppath = d.name == d.name.main ? basepack.path : subpack.path;
 					altdeppath.endsWithSlash = true;
 
 					if (!d.spec.path.empty && absdeppath != desireddeppath)
@@ -1757,14 +1888,14 @@
 			Dependency dspec = d.spec.mapToPath(pack.path);
 
 			// if not upgrading, use the selected version
-			if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(dbasename))
-				dspec = m_selectedVersions.getSelectedVersion(dbasename);
+			if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(d.name.main))
+				dspec = m_selectedVersions.getSelectedVersion(d.name.main);
 
 			// keep selected optional dependencies and avoid non-selected optional-default dependencies by default
 			if (!m_selectedVersions.bare) {
-				if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename))
+				if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(d.name.main))
 					dt = DependencyType.optional;
-				else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename))
+				else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(d.name.main))
 					dt = DependencyType.optionalDefault;
 			}
 
@@ -1779,7 +1910,7 @@
 		return configs.merge(config).valid;
 	}
 
-	private Package getPackage(string name, Dependency dep)
+	private Package getPackage(PackageName name, Dependency dep)
 	{
 		auto key = PackageDependency(name, dep);
 		if (auto pp = key in m_packages)
@@ -1789,42 +1920,30 @@
 		return p;
 	}
 
-	private Package getPackageRaw(string name, Dependency dep)
+	private Package getPackageRaw(in PackageName name, Dependency dep)
 	{
-		auto basename = getBasePackageName(name);
+		import dub.recipe.json;
 
 		// for sub packages, first try to get them from the base package
-		if (basename != name) {
-			auto subname = getSubPackageName(name);
-			auto basepack = getPackage(basename, dep);
+		if (name.main != name) {
+			auto subname = name.sub;
+			auto basepack = getPackage(name.main, dep);
 			if (!basepack) return null;
-			if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) {
+			if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true))
 				return sp;
-			} else if (!basepack.subPackages.canFind!(p => p.path.length)) {
-				// note: external sub packages are handled further below
-				auto spr = basepack.getInternalSubPackage(subname);
-				if (!spr.isNull) {
-					auto sp = new Package(spr.get, basepack.path, basepack);
-					m_remotePackages[sp.name] = sp;
-					return sp;
-				} else {
-					logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep);
-					return null;
-				}
-			} else {
-				logDiagnostic("External sub package %s %s not found.", name, dep);
-				return null;
-			}
+			logDiagnostic("Subpackage %s@%s not found.", name, dep);
+			return null;
 		}
 
 		// shortcut if the referenced package is the root package
-		if (basename == m_rootPackage.basePackage.name)
+		if (name.main.toString() == m_rootPackage.basePackage.name)
 			return m_rootPackage.basePackage;
 
 		if (!dep.repository.empty) {
 			auto ret = m_dub.packageManager.loadSCMPackage(name, dep.repository);
 			return ret !is null && dep.matches(ret.version_) ? ret : null;
-		} else if (!dep.path.empty) {
+		}
+		if (!dep.path.empty) {
 			try {
 				return m_dub.packageManager.getOrLoadPackage(dep.path);
 			} catch (Exception e) {
@@ -1838,21 +1957,21 @@
 		if (auto ret = m_dub.m_packageManager.getBestPackage(name, vers))
 			return ret;
 
-		auto key = name ~ ":" ~ vers.toString();
+		auto key = name.toString() ~ ":" ~ vers.toString();
 		if (auto ret = key in m_remotePackages)
 			return *ret;
 
 		auto prerelease = (m_options & UpgradeOptions.preRelease) != 0;
 
-		auto rootpack = name.split(":")[0];
-
 		foreach (ps; m_dub.m_packageSuppliers) {
-			if (rootpack == name) {
+			if (name.main == name) {
 				try {
 					auto desc = ps.fetchPackageRecipe(name, VersionRange(vers, vers), prerelease);
 					if (desc.type == Json.Type.null_)
 						continue;
-					auto ret = new Package(desc);
+					PackageRecipe recipe;
+					parseJson(recipe, desc);
+					auto ret = new Package(recipe);
 					m_remotePackages[key] = ret;
 					return ret;
 				} catch (Exception e) {
@@ -1864,16 +1983,16 @@
 				try {
 					FetchOptions fetchOpts;
 					fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none;
-					m_dub.fetch(rootpack, vers, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description");
+					m_dub.fetch(name.main, vers, fetchOpts, m_dub.defaultPlacementLocation, "need sub package description");
 					auto ret = m_dub.m_packageManager.getBestPackage(name, vers);
 					if (!ret) {
-						logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep, name);
+						logWarn("Package %s %s doesn't have a sub package %s", name.main, dep, name);
 						return null;
 					}
 					m_remotePackages[key] = ret;
 					return ret;
 				} catch (Exception e) {
-					logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg);
+					logDiagnostic("Package %s could not be downloaded from %s: %s", name.main, ps.description, e.msg);
 					logDebug("Full error: %s", e.toString().sanitize);
 				}
 			}
diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d
index ed0f5f1..c893fb0 100644
--- a/source/dub/generators/build.d
+++ b/source/dub/generators/build.d
@@ -216,7 +216,7 @@
 		auto cwd = settings.toolWorkingDirectory;
 		bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
 
-		auto build_id = buildsettings.computeBuildID(config, settings);
+		auto build_id = buildsettings.computeBuildID(pack.path, config, settings);
 
 		// make all paths relative to shrink the command line
 		string makeRelative(string path) { return shrinkPath(NativePath(path), cwd); }
@@ -321,7 +321,7 @@
 		const dbPathStr = dbPath.toNativeString();
 		Json db;
 		if (exists(dbPathStr)) {
-			const text = stripUTF8Bom(cast(string)readFile(dbPath));
+			const text = readText(dbPath);
 			db = parseJsonString(text, dbPathStr);
 			enforce(db.type == Json.Type.array, "Expected a JSON array in " ~ dbPathStr);
 		}
diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d
index 88d719e..7402438 100644
--- a/source/dub/generators/generator.d
+++ b/source/dub/generators/generator.d
@@ -828,7 +828,8 @@
  * library-debug-Z7qINYX4IxM8muBSlyNGrw
  * ```
  */
-package(dub) string computeBuildID(in BuildSettings buildsettings, string config, GeneratorSettings settings)
+package(dub) string computeBuildID(in BuildSettings buildsettings,
+	in NativePath packagePath, string config, GeneratorSettings settings)
 {
 	import std.conv : to;
 
@@ -843,6 +844,8 @@
 		settings.platform.architecture,
 		[
 			(cast(uint)(buildsettings.options & ~BuildOption.color)).to!string, // exclude color option from id
+			// Needed for things such as `__FULL_FILE_PATH__`
+			packagePath.toNativeString(),
 			settings.platform.compilerBinary,
 			settings.platform.compiler,
 			settings.platform.compilerVersion,
diff --git a/source/dub/generators/targetdescription.d b/source/dub/generators/targetdescription.d
index 077126e..646fa1e 100644
--- a/source/dub/generators/targetdescription.d
+++ b/source/dub/generators/targetdescription.d
@@ -45,7 +45,7 @@
 			d.packages = ti.packages.map!(p => p.name).array;
 			d.rootConfiguration = ti.config;
 			d.buildSettings = ti.buildSettings.dup;
-			const buildId = computeBuildID(d.buildSettings, ti.config, settings);
+			const buildId = computeBuildID(d.buildSettings, ti.pack.path, ti.config, settings);
 			const filename = settings.compiler.getTargetFileName(d.buildSettings, settings.platform);
 			d.cacheArtifactPath = (targetCacheDir(settings.cache, ti.pack, buildId) ~ filename).toNativeString();
 			d.dependencies = ti.dependencies.dup;
diff --git a/source/dub/internal/configy/Attributes.d b/source/dub/internal/configy/Attributes.d
index 7829064..474d33a 100644
--- a/source/dub/internal/configy/Attributes.d
+++ b/source/dub/internal/configy/Attributes.d
@@ -289,6 +289,19 @@
     return Converter!RType(func);
 }
 
+/*******************************************************************************
+
+    Interface that is passed to `fromYAML` hook
+
+    The `ConfigParser` exposes the raw YAML node (`see `node` method),
+    the path within the file (`path` method), and a simple ability to recurse
+    via `parseAs`.
+
+    Params:
+      T = The type of the structure which defines a `fromYAML` hook
+
+*******************************************************************************/
+
 public interface ConfigParser (T)
 {
     import dub.internal.dyaml.node;
@@ -301,7 +314,20 @@
     /// Returns: current location we are parsing
     public string path () const @safe pure nothrow @nogc;
 
-    ///
+    /***************************************************************************
+
+        Parse this struct as another type
+
+        This allows implementing union-like behavior, where a `struct` which
+        implements `fromYAML` can parse a simple representation as one type,
+        and one more advanced as another type.
+
+        Params:
+          OtherType = The type to parse as
+          defaultValue = The instance to use as a default value for fields
+
+    ***************************************************************************/
+
     public final auto parseAs (OtherType)
         (auto ref OtherType defaultValue = OtherType.init)
     {
diff --git a/source/dub/internal/configy/Exceptions.d b/source/dub/internal/configy/Exceptions.d
index 00a6349..7209df3 100644
--- a/source/dub/internal/configy/Exceptions.d
+++ b/source/dub/internal/configy/Exceptions.d
@@ -380,3 +380,38 @@
             sink(this.next.message);
     }
 }
+
+/// Thrown when an array read from config does not match a static array size
+public class ArrayLengthException : ConfigException
+{
+    private size_t actual;
+    private size_t expected;
+
+    /// Constructor
+    public this (size_t actual, size_t expected,
+                 string path, string key, Mark position,
+                 string file = __FILE__, size_t line = __LINE__)
+        @safe pure nothrow @nogc
+    {
+        assert(actual != expected);
+        this.actual = actual;
+        this.expected = expected;
+        super(path, key, position, file, line);
+    }
+
+    /// Format the message with or without colors
+    protected override void formatMessage (
+        scope SinkType sink, in FormatSpec!char spec)
+        const scope @trusted
+    {
+        import core.internal.string : unsignedToTempString;
+
+        char[20] buffer = void;
+        sink("Too ");
+        sink((this.actual > this.expected) ? "many" : "few");
+        sink(" entries for sequence: Expected ");
+        sink(unsignedToTempString(this.expected, buffer));
+        sink(", got ");
+        sink(unsignedToTempString(this.actual, buffer));
+    }
+}
diff --git a/source/dub/internal/configy/FieldRef.d b/source/dub/internal/configy/FieldRef.d
index 1af4b67..1c8ce8d 100644
--- a/source/dub/internal/configy/FieldRef.d
+++ b/source/dub/internal/configy/FieldRef.d
@@ -73,11 +73,20 @@
 
     /// Evaluates to `true` if this field is to be considered optional
     /// (does not need to be present in the YAML document)
-    public enum Optional = forceOptional ||
-        hasUDA!(Ref, CAOptional) ||
-        is(immutable(Type) == immutable(bool)) ||
-        is(Type : SetInfo!FT, FT) ||
-        (Default != Type.init);
+    static if (forceOptional || hasUDA!(Ref, CAOptional))
+        public enum Optional = true;
+    // Booleans are always optional
+    else static if (is(immutable(Type) == immutable(bool)))
+        public enum Optional = true;
+    // A mandatory SetInfo would not make sense
+    else static if (is(Type : SetInfo!FT, FT))
+        public enum Optional = true;
+    // Use `is` to avoid calling `opEquals` which might not be CTFEable,
+    // except for static arrays as that triggers a deprecation warning.
+    else static if (is(Type : E[k], E, size_t k))
+        public enum Optional = (Default[] !is Type.init[]);
+    else
+        public enum Optional = (Default !is Type.init);
 }
 
 unittest
@@ -118,7 +127,14 @@
     static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2");
 }
 
-/// A pseudo `FieldRef` used for structs which are not fields (top-level)
+/**
+ * A pseudo `FieldRef` used for structs which are not fields (top-level)
+ *
+ * Params:
+ *   ST = Type for which this pseudo-FieldRef is
+ *   DefaultName = A name to give to this FieldRef, default to `null`,
+ *                 but required to prevent forward references in `parseAs`.
+ */
 package template StructFieldRef (ST, string DefaultName = null)
 {
     ///
diff --git a/source/dub/internal/configy/Read.d b/source/dub/internal/configy/Read.d
index 44afcf1..71f877f 100644
--- a/source/dub/internal/configy/Read.d
+++ b/source/dub/internal/configy/Read.d
@@ -21,14 +21,17 @@
       To mark a field as optional even with its default value,
       use the `Optional` UDA: `@Optional int count = 0;`.
 
-    Converter:
-      Because config structs may contain complex types such as
-      a Phobos type, a user-defined `Amount`, or Vibe.d's `URL`,
-      one may need to apply a converter to a struct's field.
-      Converters are functions that take a YAML `Node` as argument
-      and return a type that is implicitly convertible to the field type
-      (usually just the field type). They offer the most power to users,
-      as they can inspect the YAML structure, but should be used as a last resort.
+    fromYAML:
+      Because config structs may contain complex types outside of the project's
+      control (e.g. a Phobos type, Vibe.d's `URL`, etc...) or one may want
+      the config format to be more dynamic (e.g. by exposing union-like behavior),
+      one may need to apply more custom logic than what Configy does.
+      For this use case, one can define a `fromYAML` static method in the type:
+      `static S fromYAML(scope ConfigParser!S parser)`, where `S` is the type of
+      the enclosing structure. Structs with `fromYAML` will have this method
+      called instead of going through the normal parsing rules.
+      The `ConfigParser` exposes the current path of the field, as well as the
+      raw YAML `Node` itself, allowing for maximum flexibility.
 
     Composite_Types:
       Processing starts from a `struct` at the top level, and recurse into
@@ -400,8 +403,8 @@
                      fullyQualifiedName!T,
                      strict == StrictMode.Warn ?
                        strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red));
-            return node.parseMapping!(StructFieldRef!T)(
-                null, T.init, const(Context)(cmdln, strict), null);
+            return node.parseField!(StructFieldRef!T)(
+                null, T.init, const(Context)(cmdln, strict));
     case NodeID.sequence:
     case NodeID.scalar:
     case NodeID.invalid:
@@ -791,13 +794,28 @@
         if (node.nodeID != NodeID.sequence)
             throw new TypeConfigException(node, "sequence (array)", path);
 
+        typeof(return) validateLength (E[] res)
+        {
+            static if (is(FR.Type : E_[k], E_, size_t k))
+            {
+                if (res.length != k)
+                    throw new ArrayLengthException(
+                        res.length, k, path, null, node.startMark());
+                return res[0 .. k];
+            }
+            else
+                return res;
+        }
+
         // We pass `E.init` as default value as it is not going to be used:
         // Either there is something in the YAML document, and that will be
         // converted, or `sequence` will not iterate.
-        return node.sequence.enumerate.map!(
+        return validateLength(
+            node.sequence.enumerate.map!(
             kv => kv.value.parseField!(NestedFieldRef!(E, FR))(
                 format("%s[%s]", path, kv.index), E.init, ctx))
-            .array();
+            .array()
+        );
     }
     else
     {
diff --git a/source/dub/internal/configy/Test.d b/source/dub/internal/configy/Test.d
index a334b6d..bab92c1 100644
--- a/source/dub/internal/configy/Test.d
+++ b/source/dub/internal/configy/Test.d
@@ -692,3 +692,238 @@
     catch (Exception exc)
         assert(exc.toString() == `/dev/null(2:6): data.array[0]: Parsing failed!`);
 }
+
+/// Test for error message: Has to be versioned out, uncomment to check manually
+unittest
+{
+    static struct Nested
+    {
+        int field1;
+
+        private this (string arg) {}
+    }
+
+    static struct Config
+    {
+        Nested nested;
+    }
+
+    static struct Config2
+    {
+        Nested nested;
+        alias nested this;
+    }
+
+    version(none) auto c1 = parseConfigString!Config(null, null);
+    version(none) auto c2 = parseConfigString!Config2(null, null);
+}
+
+/// Test support for `fromYAML` hook
+unittest
+{
+    static struct PackageDef
+    {
+        string name;
+        @Optional string target;
+        int build = 42;
+    }
+
+    static struct Package
+    {
+        string path;
+        PackageDef def;
+
+        public static Package fromYAML (scope ConfigParser!Package parser)
+        {
+            if (parser.node.nodeID == NodeID.mapping)
+                return Package(null, parser.parseAs!PackageDef);
+            else
+                return Package(parser.parseAs!string);
+        }
+    }
+
+    static struct Config
+    {
+        string name;
+        Package[] deps;
+    }
+
+    auto c = parseConfigString!Config(
+`
+name: myPkg
+deps:
+  - /foo/bar
+  - name: foo
+    target: bar
+    build: 24
+  - name: fur
+  - /one/last/path
+`, "/dev/null");
+    assert(c.name == "myPkg");
+    assert(c.deps.length == 4);
+    assert(c.deps[0] == Package("/foo/bar"));
+    assert(c.deps[1] == Package(null, PackageDef("foo", "bar", 24)));
+    assert(c.deps[2] == Package(null, PackageDef("fur", null, 42)));
+    assert(c.deps[3] == Package("/one/last/path"));
+}
+
+/// Test top level hook (fromYAML / fromString)
+unittest
+{
+    static struct Version1 {
+        uint fileVersion;
+        uint value;
+    }
+
+    static struct Version2 {
+        uint fileVersion;
+        string str;
+    }
+
+    static struct Config
+    {
+        uint fileVersion;
+        union {
+            Version1 v1;
+            Version2 v2;
+        }
+        static Config fromYAML (scope ConfigParser!Config parser)
+        {
+            static struct OnlyVersion { uint fileVersion; }
+            auto vers = parseConfig!OnlyVersion(
+                CLIArgs.init, parser.node, StrictMode.Ignore);
+            switch (vers.fileVersion) {
+            case 1:
+                return Config(1, parser.parseAs!Version1);
+            case 2:
+                Config conf = Config(2);
+                conf.v2 = parser.parseAs!Version2;
+                return conf;
+            default:
+                assert(0);
+            }
+        }
+    }
+
+    auto v1 = parseConfigString!Config("fileVersion: 1\nvalue: 42", "/dev/null");
+    auto v2 = parseConfigString!Config("fileVersion: 2\nstr: hello world", "/dev/null");
+
+    assert(v1.fileVersion == 1);
+    assert(v1.v1.fileVersion == 1);
+    assert(v1.v1.value == 42);
+
+    assert(v2.fileVersion == 2);
+    assert(v2.v2.fileVersion == 2);
+    assert(v2.v2.str == "hello world");
+}
+
+/// Don't call `opCmp` / `opEquals` as they might not be CTFEable
+/// Also various tests around static arrays
+unittest
+{
+    static struct NonCTFEAble
+    {
+        int value;
+
+        public bool opEquals (const NonCTFEAble other) const scope
+        {
+            assert(0);
+        }
+
+        public bool opEquals (const ref NonCTFEAble other) const scope
+        {
+            assert(0);
+        }
+
+        public int opCmp (const NonCTFEAble other) const scope
+        {
+            assert(0);
+        }
+
+        public int opCmp (const ref NonCTFEAble other) const scope
+        {
+            assert(0);
+        }
+    }
+
+    static struct Config
+    {
+        NonCTFEAble fixed;
+        @Name("static") NonCTFEAble[3] static_;
+        NonCTFEAble[] dynamic;
+    }
+
+    auto c = parseConfigString!Config(`fixed:
+  value: 42
+static:
+  - value: 84
+  - value: 126
+  - value: 168
+dynamic:
+  - value: 420
+  - value: 840
+`, "/dev/null");
+
+    assert(c.fixed.value == 42);
+    assert(c.static_[0].value == 84);
+    assert(c.static_[1].value == 126);
+    assert(c.static_[2].value == 168);
+    assert(c.dynamic.length == 2);
+    assert(c.dynamic[0].value == 420);
+    assert(c.dynamic[1].value == 840);
+
+    try parseConfigString!Config(`fixed:
+  value: 42
+dynamic:
+  - value: 420
+  - value: 840
+`, "/dev/null");
+    catch (ConfigException e)
+        assert(e.toString() == "/dev/null(0:0): static: Required key was not found in configuration or command line arguments");
+
+    try parseConfigString!Config(`fixed:
+  value: 42
+static:
+  - value: 1
+  - value: 2
+dynamic:
+  - value: 420
+  - value: 840
+`, "/dev/null");
+    catch (ConfigException e)
+        assert(e.toString() == "/dev/null(3:2): static: Too few entries for sequence: Expected 3, got 2");
+
+    try parseConfigString!Config(`fixed:
+  value: 42
+static:
+  - value: 1
+  - value: 2
+  - value: 3
+  - value: 4
+dynamic:
+  - value: 420
+  - value: 840
+`, "/dev/null");
+    catch (ConfigException e)
+        assert(e.toString() == "/dev/null(3:2): static: Too many entries for sequence: Expected 3, got 4");
+
+    // Check that optional static array work
+    static struct ConfigOpt
+    {
+        NonCTFEAble fixed;
+        @Name("static") NonCTFEAble[3] static_ = [
+            NonCTFEAble(69),
+            NonCTFEAble(70),
+            NonCTFEAble(71),
+        ];
+    }
+
+    auto c1 = parseConfigString!ConfigOpt(`fixed:
+  value: 1100
+`, "/dev/null");
+
+    assert(c1.fixed.value == 1100);
+    assert(c1.static_[0].value == 69);
+    assert(c1.static_[1].value == 70);
+    assert(c1.static_[2].value == 71);
+}
diff --git a/source/dub/internal/tinyendian.d b/source/dub/internal/tinyendian.d
index d9b227a..a6e9681 100644
--- a/source/dub/internal/tinyendian.d
+++ b/source/dub/internal/tinyendian.d
@@ -122,7 +122,7 @@
     static immutable Endian[5] bomEndian = [ endian,
                                              Endian.littleEndian,
                                              Endian.bigEndian,
-                                             Endian.littleEndian, 
+                                             Endian.littleEndian,
                                              Endian.bigEndian ];
 
     // Documented in function ddoc.
diff --git a/source/dub/internal/utils.d b/source/dub/internal/utils.d
index b9109cc..d0026c9 100644
--- a/source/dub/internal/utils.d
+++ b/source/dub/internal/utils.d
@@ -92,7 +92,7 @@
 
 Json jsonFromFile(NativePath file, bool silent_fail = false) {
 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
-	auto text = stripUTF8Bom(cast(string)readFile(file));
+	auto text = readText(file);
 	return parseJsonString(text, file.toNativeString());
 }
 
@@ -207,7 +207,7 @@
 
 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
 */
-void download(string url, string filename, uint timeout = 8)
+private void download(string url, string filename, uint timeout = 8)
 {
 	version(DubUseCurl) {
 		auto conn = HTTP();
@@ -226,18 +226,18 @@
 	} else assert(false);
 }
 /// ditto
-void download(URL url, NativePath filename, uint timeout = 8)
+private void download(URL url, NativePath filename, uint timeout = 8)
 {
 	download(url.toString(), filename.toNativeString(), timeout);
 }
 /// ditto
-ubyte[] download(string url, uint timeout = 8)
+private ubyte[] download(string url, uint timeout = 8)
 {
 	version(DubUseCurl) {
 		auto conn = HTTP();
 		setupHTTPClient(conn, timeout);
 		logDebug("Getting %s...", url);
-		return cast(ubyte[])get(url, conn);
+		return get!(HTTP, ubyte)(url, conn);
 	} else version (Have_vibe_d_http) {
 		import vibe.inet.urltransfer;
 		import vibe.stream.operations;
@@ -247,7 +247,7 @@
 	} else assert(false);
 }
 /// ditto
-ubyte[] download(URL url, uint timeout = 8)
+private ubyte[] download(URL url, uint timeout = 8)
 {
 	return download(url.toString(), timeout);
 }
@@ -319,13 +319,15 @@
 			catch(HTTPStatusException e) {
 				if (e.status == 404) throw e;
 				else {
-					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
+					logDebug("Failed to download %s (Attempt %s of %s): %s",
+						url, i + 1, retryCount, e.message);
 					if (i == retryCount - 1) throw e;
 					else continue;
 				}
 			}
 			catch(CurlException e) {
-				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
+				logDebug("Failed to download %s (Attempt %s of %s): %s",
+					url, i + 1, retryCount, e.message);
 				continue;
 			}
 		}
@@ -451,7 +453,7 @@
 	}
 }
 
-string stripUTF8Bom(string str)
+private string stripUTF8Bom(string str)
 {
 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
 		return str[3 ..$];
diff --git a/source/dub/internal/vibecompat/core/file.d b/source/dub/internal/vibecompat/core/file.d
index 64faf3b..75a94cf 100644
--- a/source/dub/internal/vibecompat/core/file.d
+++ b/source/dub/internal/vibecompat/core/file.d
@@ -34,6 +34,12 @@
 	return cast(ubyte[]) std.file.read(path.toNativeString());
 }
 
+/// Returns the content of a file as text
+public string readText(NativePath path)
+{
+    return std.file.readText(path.toNativeString());
+}
+
 /**
 	Moves or renames a file.
 */
@@ -101,7 +107,7 @@
 	copyFile(NativePath(from), NativePath(to));
 }
 
-version (Windows) extern(Windows) int CreateHardLinkW(in wchar* to, in wchar* from, void* attr=null);
+version (Windows) extern(Windows) int CreateHardLinkW(const(wchar)* to, const(wchar)* from, void* attr=null);
 
 // guess whether 2 files are identical, ignores filename and content
 private bool sameFile(NativePath a, NativePath b)
@@ -287,9 +293,6 @@
 	/// Time of the last modification
 	SysTime timeModified;
 
-	/// Time of creation (not available on all operating systems/file systems)
-	SysTime timeCreated;
-
 	/// True if this is a symlink to an actual file
 	bool isSymlink;
 
@@ -326,8 +329,6 @@
 		ret.isDirectory = ent.isDir;
 		ret.size = ent.size;
 		ret.timeModified = ent.timeLastModified;
-		version(Windows) ret.timeCreated = ent.timeCreated;
-		else ret.timeCreated = ent.timeLastModified;
 	} catch (Exception e) {
 		logDiagnostic("Failed to get extended file information for %s: %s", ret.name, e.msg);
 	}
diff --git a/source/dub/internal/vibecompat/inet/path.d b/source/dub/internal/vibecompat/inet/path.d
index 745ebc0..7277e76 100644
--- a/source/dub/internal/vibecompat/inet/path.d
+++ b/source/dub/internal/vibecompat/inet/path.d
@@ -47,7 +47,7 @@
 	}
 
 	/// Constructs a path object from a list of PathEntry objects.
-	this(immutable(PathEntry)[] nodes, bool absolute)
+	this(immutable(PathEntry)[] nodes, bool absolute = false)
 	{
 		m_nodes = nodes;
 		m_absolute = absolute;
@@ -185,6 +185,8 @@
 
 	/// The parent path
 	@property NativePath parentPath() const { return this[0 .. length-1]; }
+	/// Forward compatibility with vibe-d
+	@property bool hasParentPath() const { return length > 1; }
 
 	/// The list of path entries of which this path is composed
 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
diff --git a/source/dub/package_.d b/source/dub/package_.d
index 7e2eef9..94d2597 100644
--- a/source/dub/package_.d
+++ b/source/dub/package_.d
@@ -80,9 +80,10 @@
 /**	Represents a package, including its sub packages.
 */
 class Package {
+	// `package` visibility as it is set from the PackageManager
+	package NativePath m_infoFile;
 	private {
 		NativePath m_path;
-		NativePath m_infoFile;
 		PackageRecipe m_info;
 		PackageRecipe m_rawRecipe;
 		Package m_parentPackage;
@@ -100,6 +101,7 @@
 				instead of the one declared in the package recipe, or the one
 				determined by invoking the VCS (GIT currently).
 	*/
+	deprecated("Provide an already parsed PackageRecipe instead of a JSON object")
 	this(Json json_recipe, NativePath root = NativePath(), Package parent = null, string version_override = "")
 	{
 		import dub.recipe.json;
@@ -151,6 +153,7 @@
 			Returns the full path to the package file, if any was found.
 			Otherwise returns an empty path.
 	*/
+	deprecated("Use `PackageManager.findPackageFile`")
 	static NativePath findPackageFile(NativePath directory)
 	{
 		foreach (file; packageInfoFiles) {
@@ -173,6 +176,7 @@
 				determined by invoking the VCS (GIT currently).
 			mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
 	*/
+	deprecated("Use `PackageManager.getOrLoadPackage` instead of loading packages directly")
 	static Package load(NativePath root, NativePath recipe_file = NativePath.init,
 		Package parent = null, string version_override = "",
 		StrictMode mode = StrictMode.Ignore)
@@ -309,13 +313,7 @@
 		writeJsonFile(filename, m_info.toJson());
 	}
 
-	/** Returns the package recipe of a non-path-based sub package.
-
-		For sub packages that are declared within the package recipe of the
-		parent package, this function will return the corresponding recipe. Sub
-		packages declared using a path must be loaded manually (or using the
-		`PackageManager`).
-	*/
+	deprecated("Use `PackageManager.getSubPackage` instead")
 	Nullable!PackageRecipe getInternalSubPackage(string name)
 	{
 		foreach (ref p; m_info.subPackages)
@@ -419,10 +417,6 @@
 	*/
 	void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
 	const {
-		import std.process : environment;
-		string dflags = environment.get("DFLAGS", "");
-		settings.addDFlags(dflags.split());
-
 		if (auto pbt = build_type in m_info.buildTypes) {
 			logDiagnostic("Using custom build type '%s'.", build_type);
 			pbt.getPlatformSettings(settings, platform, this.path);
@@ -447,6 +441,11 @@
 				case "syntax": settings.addOptions(syntaxOnly); break;
 			}
 		}
+
+		// Add environment DFLAGS last so that user specified values are not overriden by us.
+		import std.process : environment;
+		string dflags = environment.get("DFLAGS", "");
+		settings.addDFlags(dflags.split());
 	}
 
 	/** Returns the selected configuration for a certain dependency.
@@ -580,7 +579,7 @@
 				this.recipe.configurations.map!(c => c.buildSettings.dependencies.byKeyValue)
 			)
 			.joiner()
-			.map!(d => PackageDependency(d.key, d.value));
+			.map!(d => PackageDependency(PackageName(d.key), d.value));
 	}
 
 
diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d
index d7bff62..90eec57 100644
--- a/source/dub/packagemanager.d
+++ b/source/dub/packagemanager.d
@@ -9,12 +9,13 @@
 
 import dub.dependency;
 import dub.internal.utils;
-import dub.internal.vibecompat.core.file;
+import dub.internal.vibecompat.core.file : FileInfo;
 import dub.internal.vibecompat.data.json;
 import dub.internal.vibecompat.inet.path;
 import dub.internal.logging;
 import dub.package_;
 import dub.recipe.io;
+import dub.recipe.selection;
 import dub.internal.configy.Exceptions;
 public import dub.internal.configy.Read : StrictMode;
 
@@ -188,11 +189,11 @@
 	 * Returns:
 	 *	 A `Package` if one was found, `null` if none exists.
 	 */
-	protected Package lookup (string name, Version vers) {
+	protected Package lookup (in PackageName name, in Version vers) {
 		if (!this.m_initialized)
 			this.refresh();
 
-		if (auto pkg = this.m_internal.lookup(name, vers, this))
+		if (auto pkg = this.m_internal.lookup(name, vers))
 			return pkg;
 
 		foreach (ref location; this.m_repositories)
@@ -219,12 +220,12 @@
 		Returns:
 			The matching package or null if no match was found.
 	*/
-	Package getPackage(string name, Version ver, bool enable_overrides = true)
+	Package getPackage(in PackageName name, in Version ver, bool enable_overrides = true)
 	{
 		if (enable_overrides) {
 			foreach (ref repo; m_repositories)
 				foreach (ovr; repo.overrides)
-					if (ovr.package_ == name && ovr.source.matches(ver)) {
+					if (ovr.package_ == name.toString() && ovr.source.matches(ver)) {
 						Package pack = ovr.target.match!(
 							(NativePath path) => getOrLoadPackage(path),
 							(Version	vers) => getPackage(name, vers, false),
@@ -243,6 +244,12 @@
 		return this.lookup(name, ver);
 	}
 
+	deprecated("Use the overload that accepts a `PackageName` instead")
+	Package getPackage(string name, Version ver, bool enable_overrides = true)
+	{
+		return this.getPackage(PackageName(name), ver, enable_overrides);
+	}
+
 	/// ditto
 	deprecated("Use the overload that accepts a `Version` as second argument")
 	Package getPackage(string name, string ver, bool enable_overrides = true)
@@ -263,8 +270,15 @@
 	}
 
 	/// Ditto
+	deprecated("Use the overload that accepts a `PackageName` instead")
 	Package getPackage(string name, Version ver, PlacementLocation loc)
 	{
+		return this.getPackage(PackageName(name), ver, loc);
+	}
+
+	/// Ditto
+	Package getPackage(in PackageName name, in Version ver, PlacementLocation loc)
+	{
 		// Bare mode
 		if (loc >= this.m_repositories.length)
 			return null;
@@ -332,11 +346,76 @@
 		foreach (p; this.m_internal.fromPath)
 			if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path)))
 				return p;
-		auto pack = Package.load(path, recipe_path, null, null, mode);
+		auto pack = this.load(path, recipe_path, null, null, mode);
 		addPackages(this.m_internal.fromPath, pack);
 		return pack;
 	}
 
+	/**
+	 * Loads a `Package` from the filesystem
+	 *
+	 * This is called when a `Package` needs to be loaded from the path.
+	 * This does not change the internal state of the `PackageManager`,
+	 * it simply loads the `Package` and returns it - it is up to the caller
+	 * to call `addPackages`.
+	 *
+	 * Throws:
+	 *   If no package can be found at the `path` / with the `recipe`.
+	 *
+	 * Params:
+	 *     path = The directory in which the package resides.
+	 *     recipe = Optional path to the package recipe file. If left empty,
+	 *              the `path` directory will be searched for a recipe file.
+	 *     parent = Reference to the parent package, if the new package is a
+	 *              sub package.
+	 *     version_ = Optional version to associate to the package instead of
+	 *                the one declared in the package recipe, or the one
+	 *                determined by invoking the VCS (GIT currently).
+	 *     mode = Whether to issue errors, warning, or ignore unknown keys in
+	 *            dub.json
+	 *
+	 * Returns: A populated `Package`.
+	 */
+	protected Package load(NativePath path, NativePath recipe = NativePath.init,
+		Package parent = null, string version_ = null,
+		StrictMode mode = StrictMode.Ignore)
+	{
+		if (recipe.empty)
+			recipe = this.findPackageFile(path);
+
+		enforce(!recipe.empty,
+			"No package file found in %s, expected one of %s"
+				.format(path.toNativeString(),
+					packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
+
+		const PackageName pname = parent
+			? PackageName(parent.name) : PackageName.init;
+		string text = this.readText(recipe);
+		auto content = parsePackageRecipe(
+			text, recipe.toNativeString(), pname, null, mode);
+		auto ret = new Package(content, path, parent, version_);
+		ret.m_infoFile = recipe;
+		return ret;
+	}
+
+	/** Searches the given directory for package recipe files.
+	 *
+	 * Params:
+	 *   directory = The directory to search
+	 *
+	 * Returns:
+	 *   Returns the full path to the package file, if any was found.
+	 *   Otherwise returns an empty path.
+	 */
+	public NativePath findPackageFile(NativePath directory)
+	{
+		foreach (file; packageInfoFiles) {
+			auto filename = directory ~ file.filename;
+			if (this.existsFile(filename)) return filename;
+		}
+		return NativePath.init;
+	}
+
 	/** For a given SCM repository, returns the corresponding package.
 
 		An SCM repository is provided as its remote URL, the repository is cloned
@@ -353,13 +432,7 @@
 			The package loaded from the given SCM repository or null if the
 			package couldn't be loaded.
 	*/
-	deprecated("Use the overload that accepts a `dub.dependency : Repository`")
-	Package loadSCMPackage(string name, Dependency dependency)
-	in { assert(!dependency.repository.empty); }
-	do { return this.loadSCMPackage(name, dependency.repository); }
-
-	/// Ditto
-	Package loadSCMPackage(string name, Repository repo)
+	Package loadSCMPackage(in PackageName name, in Repository repo)
 	in { assert(!repo.empty); }
 	do {
 		Package pack;
@@ -367,39 +440,60 @@
 		final switch (repo.kind)
 		{
 			case repo.Kind.git:
-				pack = loadGitPackage(name, repo);
+				return this.loadGitPackage(name, repo);
 		}
-		if (pack !is null) {
-			addPackages(this.m_internal.fromPath, pack);
-		}
-		return pack;
 	}
 
-	private Package loadGitPackage(string name, in Repository repo)
-	{
-		import dub.internal.git : cloneRepository;
+	deprecated("Use the overload that accepts a `dub.dependency : Repository`")
+	Package loadSCMPackage(string name, Dependency dependency)
+	in { assert(!dependency.repository.empty); }
+	do { return this.loadSCMPackage(name, dependency.repository); }
 
+	deprecated("Use `loadSCMPackage(PackageName, Repository)`")
+	Package loadSCMPackage(string name, Repository repo)
+	{
+		return this.loadSCMPackage(PackageName(name), repo);
+	}
+
+	private Package loadGitPackage(in PackageName name, in Repository repo)
+	{
 		if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) {
 			return null;
 		}
 
 		string gitReference = repo.ref_.chompPrefix("~");
 		NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_);
-		// For libraries leaking their import path
-		destination ~= name;
-		destination.endsWithSlash = true;
 
-		foreach (p; getPackageIterator(name)) {
+		foreach (p; getPackageIterator(name.toString())) {
 			if (p.path == destination) {
 				return p;
 			}
 		}
 
-		if (!cloneRepository(repo.remote, gitReference, destination.toNativeString())) {
+		if (!this.gitClone(repo.remote, gitReference, destination))
 			return null;
-		}
 
-		return Package.load(destination);
+		Package result = this.load(destination);
+		if (result !is null)
+			this.addPackages(this.m_internal.fromPath, result);
+		return result;
+	}
+
+	/**
+	 * Perform a `git clone` operation at `dest` using `repo`
+	 *
+	 * Params:
+	 *   remote = The remote to clone from
+	 *   gitref = The git reference to use
+	 *   dest   = Where the result of git clone operation is to be stored
+	 *
+	 * Returns:
+	 *	 Whether or not the clone operation was successfull.
+	 */
+	protected bool gitClone(string remote, string gitref, in NativePath dest)
+	{
+		static import dub.internal.git;
+		return dub.internal.git.cloneRepository(remote, gitref, dest.toNativeString());
 	}
 
 	/**
@@ -407,7 +501,7 @@
 	 *
 	 * See `Location.getPackagePath`.
 	 */
-	package(dub) NativePath getPackagePath (PlacementLocation base, string name, string vers)
+	package(dub) NativePath getPackagePath(PlacementLocation base, in PackageName name, string vers)
 	{
 		assert(this.m_repositories.length == 3, "getPackagePath called in bare mode");
 		return this.m_repositories[base].getPackagePath(name, vers);
@@ -428,14 +522,28 @@
 	 * Returns:
 	 *	 The best package matching the parameters, or `null` if none was found.
 	 */
+	deprecated("Use the overload that accepts a `PackageName` instead")
 	Package getBestPackage(string name, Version vers)
 	{
+		return this.getBestPackage(PackageName(name), vers);
+	}
+
+	/// Ditto
+	Package getBestPackage(in PackageName name, in Version vers)
+	{
 		return this.getBestPackage(name, VersionRange(vers, vers));
 	}
 
 	/// Ditto
+	deprecated("Use the overload that accepts a `PackageName` instead")
 	Package getBestPackage(string name, VersionRange range = VersionRange.Any)
 	{
+		return this.getBestPackage(PackageName(name), range);
+	}
+
+	/// Ditto
+	Package getBestPackage(in PackageName name, in VersionRange range = VersionRange.Any)
+	{
 		return this.getBestPackage_(name, Dependency(range));
 	}
 
@@ -450,14 +558,15 @@
 	deprecated("`getBestPackage` should only be used with a `Version` or `VersionRange` argument")
 	Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true)
 	{
-		return this.getBestPackage_(name, version_spec, enable_overrides);
+		return this.getBestPackage_(PackageName(name), version_spec, enable_overrides);
 	}
 
 	// TODO: Merge this into `getBestPackage(string, VersionRange)`
-	private Package getBestPackage_(string name, Dependency version_spec, bool enable_overrides = true)
+	private Package getBestPackage_(in PackageName name, in Dependency version_spec,
+		bool enable_overrides = true)
 	{
 		Package ret;
-		foreach (p; getPackageIterator(name)) {
+		foreach (p; getPackageIterator(name.toString())) {
 			auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard;
 			if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_))
 				ret = p;
@@ -472,9 +581,6 @@
 
 	/** Gets the a specific sub package.
 
-		In contrast to `Package.getSubPackage`, this function supports path
-		based sub packages.
-
 		Params:
 			base_package = The package from which to get a sub package
 			sub_name = Name of the sub package (not prefixed with the base
@@ -510,11 +616,9 @@
 	*/
 	bool isManagedPath(NativePath path)
 	const {
-		foreach (rep; m_repositories) {
-			NativePath rpath = rep.packagePath;
-			if (path.startsWith(rpath))
+		foreach (rep; m_repositories)
+			if (rep.isManaged(path))
 				return true;
-		}
 		return false;
 	}
 
@@ -585,14 +689,14 @@
 	void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target)
 	{
 		m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
-		m_repositories[scope_].writeOverrides();
+		m_repositories[scope_].writeOverrides(this);
 	}
 	/// ditto
 	deprecated("Use the overload that accepts a `VersionRange` as 3rd argument")
 	void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target)
 	{
 		m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target);
-		m_repositories[scope_].writeOverrides();
+		m_repositories[scope_].writeOverrides(this);
 	}
 
 	/// Ditto
@@ -612,13 +716,13 @@
 	package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target)
 	{
 		m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
-		m_repositories[scope_].writeOverrides();
+		m_repositories[scope_].writeOverrides(this);
 	}
 	// Non deprecated version that is used by `commandline`. Do not use!
 	package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target)
 	{
 		m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target);
-		m_repositories[scope_].writeOverrides();
+		m_repositories[scope_].writeOverrides(this);
 	}
 
 	/** Removes an existing package override.
@@ -645,7 +749,7 @@
 			if (ovr.package_ != package_ || ovr.source != src)
 				continue;
 			rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $];
-			(*rep).writeOverrides();
+			(*rep).writeOverrides(this);
 			return;
 		}
 		throw new Exception(format("No override exists for %s %s", package_, src));
@@ -654,7 +758,10 @@
 	deprecated("Use `store(NativePath source, PlacementLocation dest, string name, Version vers)`")
 	Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination)
 	{
-		return this.store_(zip_file_path, destination, package_info["name"].get!string,
+		import dub.internal.vibecompat.core.file;
+
+		return this.store_(readFile(zip_file_path), destination,
+			PackageName(package_info["name"].get!string),
 			Version(package_info["version"].get!string));
 	}
 
@@ -679,43 +786,58 @@
 	 *   If the package cannot be loaded / the zip is corrupted / the package
 	 *   already exists, etc...
 	 */
+	deprecated("Use the overload that accepts a `PackageName` instead")
 	Package store(NativePath src, PlacementLocation dest, string name, Version vers)
 	{
+		return this.store(src, dest, PackageName(name), vers);
+	}
+
+	/// Ditto
+	Package store(NativePath src, PlacementLocation dest, in PackageName name,
+		in Version vers)
+	{
+		import dub.internal.vibecompat.core.file;
+
+		auto data = readFile(src);
+		return this.store(data, dest, name, vers);
+	}
+
+	/// Ditto
+	Package store(ubyte[] data, PlacementLocation dest,
+		in PackageName name, in Version vers)
+	{
+		import dub.internal.vibecompat.core.file;
+
+		assert(!name.sub.length, "Cannot store a subpackage, use main package instead");
 		NativePath dstpath = this.getPackagePath(dest, name, vers.toString());
-		ensureDirectory(dstpath);
-		// For libraries leaking their import path
-		dstpath = dstpath ~ name;
+		ensureDirectory(dstpath.parentPath());
+		const lockPath = dstpath.parentPath() ~ ".lock";
 
 		// possibly wait for other dub instance
 		import core.time : seconds;
-		auto lock = lockFile(dstpath.toNativeString() ~ ".lock", 30.seconds);
-		if (dstpath.existsFile()) {
+		auto lock = lockFile(lockPath.toNativeString(), 30.seconds);
+		if (this.existsFile(dstpath)) {
 			return this.getPackage(name, vers, dest);
 		}
-		return this.store_(src, dstpath, name, vers);
+		return this.store_(data, dstpath, name, vers);
 	}
 
 	/// Backward-compatibility for deprecated overload, simplify once `storeFetchedPatch`
 	/// is removed
-	private Package store_(NativePath src, NativePath destination, string name, Version vers)
+	private Package store_(ubyte[] data, NativePath destination,
+		in PackageName name, in Version vers)
 	{
+		import dub.internal.vibecompat.core.file;
 		import std.range : walkLength;
 
-		logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'",
-			name, vers, destination.toNativeString(), src.toNativeString());
+		logDebug("Placing package '%s' version '%s' to location '%s'",
+			name, vers, destination.toNativeString());
 
-		if( existsFile(destination) ){
-			throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.",
-				name, vers, destination));
-		}
+		enforce(!this.existsFile(destination),
+			"%s (%s) needs to be removed from '%s' prior placement."
+			.format(name, vers, destination));
 
-		// open zip file
-		ZipArchive archive;
-		{
-			logDebug("Opening file %s", src);
-			archive = new ZipArchive(readFile(src));
-		}
-
+		ZipArchive archive = new ZipArchive(data);
 		logDebug("Extracting from zip.");
 
 		// In a GitHub zip, the actual contents are in a sub-folder
@@ -759,7 +881,7 @@
 			auto dst_path = destination ~ cleanedPath;
 
 			logDebug("Creating %s", cleanedPath);
-			if( dst_path.endsWithSlash ){
+			if (dst_path.endsWithSlash) {
 				ensureDirectory(dst_path);
 			} else {
 				ensureDirectory(dst_path.parentPath);
@@ -778,7 +900,7 @@
 					}
 				}
 
-				writeFile(dst_path, archive.expand(a));
+				this.writeFile(dst_path, archive.expand(a));
 				setAttributes(dst_path.toNativeString(), a);
 symlink_exit:
 				++countFiles;
@@ -787,7 +909,7 @@
 		logDebug("%s file(s) copied.", to!string(countFiles));
 
 		// overwrite dub.json (this one includes a version field)
-		auto pack = Package.load(destination, NativePath.init, null, vers.toString());
+		auto pack = this.load(destination, NativePath.init, null, vers.toString());
 
 		if (pack.recipePath.head != defaultPackageFilename)
 			// Storeinfo saved a default file, this could be different to the file from the zip.
@@ -802,6 +924,7 @@
 	{
 		logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path);
 		enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
+		enforce(pack.parentPackage is null, "Cannot remove subpackage %s".format(pack.name));
 
 		// remove package from repositories' list
 		bool found = false;
@@ -850,7 +973,7 @@
 			this.refresh();
 
 		path.endsWithSlash = true;
-		auto pack = Package.load(path);
+		auto pack = this.load(path);
 		enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
 		if (verName.length)
 			pack.version_ = Version(verName);
@@ -867,7 +990,7 @@
 
 		addPackages(*packs, pack);
 
-		this.m_repositories[type].writeLocalPackageList();
+		this.m_repositories[type].writeLocalPackageList(this);
 
 		logInfo("Registered package: %s (version: %s)", pack.name, pack.version_);
 		return pack;
@@ -897,7 +1020,7 @@
 			.filter!(en => !to_remove.canFind(en.index))
 			.map!(en => en.value).array;
 
-		this.m_repositories[type].writeLocalPackageList();
+		this.m_repositories[type].writeLocalPackageList(this);
 
 		foreach(ver, name; removed)
 			logInfo("Deregistered package: %s (version: %s)", name, ver);
@@ -907,14 +1030,14 @@
 	void addSearchPath(NativePath path, PlacementLocation type)
 	{
 		m_repositories[type].searchPath ~= path;
-		this.m_repositories[type].writeLocalPackageList();
+		this.m_repositories[type].writeLocalPackageList(this);
 	}
 
 	/// Removes a search path from the given type.
 	void removeSearchPath(NativePath path, PlacementLocation type)
 	{
 		m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
-		this.m_repositories[type].writeLocalPackageList();
+		this.m_repositories[type].writeLocalPackageList(this);
 	}
 
 	deprecated("Use `refresh()` without boolean argument(same as `refresh(false)`")
@@ -943,7 +1066,7 @@
 			repository.scan(this, refresh);
 
 		foreach (ref repository; this.m_repositories)
-			repository.loadOverrides();
+			repository.loadOverrides(this);
 		this.m_initialized = true;
 	}
 
@@ -953,6 +1076,8 @@
 	/// .svn folders)
 	Hash hashPackage(Package pack)
 	{
+		import dub.internal.vibecompat.core.file;
+
 		string[] ignored_directories = [".git", ".dub", ".svn"];
 		// something from .dub_ignore or what?
 		string[] ignored_files = [];
@@ -978,9 +1103,70 @@
 		return digest[].dup;
 	}
 
+	/**
+	 * Writes the selections file (`dub.selections.json`)
+	 *
+	 * The selections file is only used for the root package / project.
+	 * However, due to it being a filesystem interaction, it is managed
+	 * from the `PackageManager`.
+	 *
+	 * Params:
+	 *   project = The root package / project to read the selections file for.
+	 *   selections = The `SelectionsFile` to write.
+	 *   overwrite = Whether to overwrite an existing selections file.
+	 *               True by default.
+	 */
+	public void writeSelections(in Package project, in Selections!1 selections,
+		bool overwrite = true)
+	{
+		const path = project.path ~ "dub.selections.json";
+		if (!overwrite && this.existsFile(path))
+			return;
+		this.writeFile(path, selectionsToString(selections));
+	}
+
+	/// Package function to avoid code duplication with deprecated
+	/// SelectedVersions.save, merge with `writeSelections` in
+	/// the future.
+	package static string selectionsToString (in Selections!1 s)
+	{
+		Json json = selectionsToJSON(s);
+		assert(json.type == Json.Type.object);
+		assert(json.length == 2);
+		assert(json["versions"].type != Json.Type.undefined);
+
+		auto result = appender!string();
+		result.put("{\n\t\"fileVersion\": ");
+		result.writeJsonString(json["fileVersion"]);
+		result.put(",\n\t\"versions\": {");
+		auto vers = json["versions"].get!(Json[string]);
+		bool first = true;
+		foreach (k; vers.byKey.array.sort()) {
+			if (!first) result.put(",");
+			else first = false;
+			result.put("\n\t\t");
+			result.writeJsonString(Json(k));
+			result.put(": ");
+			result.writeJsonString(vers[k]);
+		}
+		result.put("\n\t}\n}\n");
+		return result.data;
+	}
+
+	/// Ditto
+	package static Json selectionsToJSON (in Selections!1 s)
+	{
+		Json serialized = Json.emptyObject;
+		serialized["fileVersion"] = s.fileVersion;
+		serialized["versions"] = Json.emptyObject;
+		foreach (p, dep; s.versions)
+			serialized["versions"][p] = dep.toJson(true);
+		return serialized;
+	}
+
 	/// Adds the package and scans for sub-packages.
-	private void addPackages(ref Package[] dst_repos, Package pack)
-	const {
+	protected void addPackages(ref Package[] dst_repos, Package pack)
+	{
 		// Add the main package.
 		dst_repos ~= pack;
 
@@ -995,7 +1181,7 @@
 				p.normalize();
 				enforce(!p.absolute, "Sub package paths must be sub paths of the parent package.");
 				auto path = pack.path ~ p;
-				sp = Package.load(path, NativePath.init, pack);
+				sp = this.load(path, NativePath.init, pack);
 			} else sp = new Package(spr.recipe, pack.path, pack);
 
 			// Add the sub-package.
@@ -1008,6 +1194,58 @@
 			}
 		}
 	}
+
+	/// Used for dependency injection
+	protected bool existsDirectory(NativePath path)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.existsDirectory(path);
+	}
+
+	/// Ditto
+	protected void ensureDirectory(NativePath path)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.ensureDirectory(path);
+	}
+
+	/// Ditto
+	protected bool existsFile(NativePath path)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.existsFile(path);
+	}
+
+	/// Ditto
+	protected void writeFile(NativePath path, const(ubyte)[] data)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.writeFile(path, data);
+	}
+
+	/// Ditto
+	protected void writeFile(NativePath path, const(char)[] data)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.writeFile(path, data);
+	}
+
+	/// Ditto
+	protected string readText(NativePath path)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.readText(path);
+	}
+
+	/// Ditto
+	protected alias IterateDirDg = int delegate(scope int delegate(ref FileInfo));
+
+	/// Ditto
+	protected IterateDirDg iterateDirectory(NativePath path)
+	{
+		static import dub.internal.vibecompat.core.file;
+		return dub.internal.vibecompat.core.file.iterateDirectory(path);
+	}
 }
 
 deprecated(OverrideDepMsg)
@@ -1147,15 +1385,17 @@
 		this.packagePath = path;
 	}
 
-	void loadOverrides()
+	void loadOverrides(PackageManager mgr)
 	{
 		this.overrides = null;
 		auto ovrfilepath = this.packagePath ~ LocalOverridesFilename;
-		if (existsFile(ovrfilepath)) {
+		if (mgr.existsFile(ovrfilepath)) {
 			logWarn("Found local override file: %s", ovrfilepath);
 			logWarn(OverrideDepMsg);
 			logWarn("Replace with a path-based dependency in your project or a custom cache path");
-			foreach (entry; jsonFromFile(ovrfilepath)) {
+			const text = mgr.readText(ovrfilepath);
+			auto json = parseJsonString(text, ovrfilepath.toNativeString());
+			foreach (entry; json) {
 				PackageOverride_ ovr;
 				ovr.package_ = entry["name"].get!string;
 				ovr.source = VersionRange.fromString(entry["version"].get!string);
@@ -1166,7 +1406,7 @@
 		}
 	}
 
-	private void writeOverrides()
+	private void writeOverrides(PackageManager mgr)
 	{
 		Json[] newlist;
 		foreach (ovr; this.overrides) {
@@ -1180,11 +1420,13 @@
 			newlist ~= jovr;
 		}
 		auto path = this.packagePath;
-		ensureDirectory(path);
-		writeJsonFile(path ~ LocalOverridesFilename, Json(newlist));
+		mgr.ensureDirectory(path);
+		auto app = appender!string();
+		app.writePrettyJsonString(Json(newlist));
+		mgr.writeFile(path ~ LocalOverridesFilename, app.data);
 	}
 
-	private void writeLocalPackageList()
+	private void writeLocalPackageList(PackageManager mgr)
 	{
 		Json[] newlist;
 		foreach (p; this.searchPath) {
@@ -1204,8 +1446,10 @@
 		}
 
 		NativePath path = this.packagePath;
-		ensureDirectory(path);
-		writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
+		mgr.ensureDirectory(path);
+		auto app = appender!string();
+		app.writePrettyJsonString(Json(newlist));
+		mgr.writeFile(path ~ LocalPackagesFilename, app.data);
 	}
 
 	// load locally defined packages
@@ -1216,10 +1460,12 @@
 		NativePath[] paths;
 		try {
 			auto local_package_file = list_path ~ LocalPackagesFilename;
-			if (!existsFile(local_package_file)) return;
+			if (!manager.existsFile(local_package_file)) return;
 
 			logDiagnostic("Loading local package map at %s", local_package_file.toNativeString());
-			auto packlist = jsonFromFile(local_package_file);
+			const text = manager.readText(local_package_file);
+			auto packlist = parseJsonString(
+				text, local_package_file.toNativeString());
 			enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array.");
 			foreach (pentry; packlist) {
 				try {
@@ -1240,8 +1486,8 @@
 						}
 
 						if (!pp) {
-							auto infoFile = Package.findPackageFile(path);
-							if (!infoFile.empty) pp = Package.load(path, infoFile);
+							auto infoFile = manager.findPackageFile(path);
+							if (!infoFile.empty) pp = manager.load(path, infoFile);
 							else {
 								logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.",
 										name, ver, path.toNativeString());
@@ -1292,20 +1538,20 @@
 	void scanPackageFolder(NativePath path, PackageManager mgr,
 		Package[] existing_packages)
 	{
-		if (!path.existsDirectory())
+		if (!mgr.existsDirectory(path))
 			return;
 
 		void loadInternal (NativePath pack_path, NativePath packageFile)
 		{
-			Package p;
+			import std.algorithm.searching : find;
+
+			// If the package has already been loaded, no need to re-load it.
+			auto rng = existing_packages.find!(pp => pp.path == pack_path);
+			if (!rng.empty)
+				return mgr.addPackages(this.fromPath, rng.front);
+
 			try {
-				foreach (pp; existing_packages)
-					if (pp.path == pack_path) {
-						p = pp;
-						break;
-					}
-				if (!p) p = Package.load(pack_path, packageFile);
-				mgr.addPackages(this.fromPath, p);
+				mgr.addPackages(this.fromPath, mgr.load(pack_path, packageFile));
 			} catch (ConfigException exc) {
 				// Configy error message already include the path
 				logError("Invalid recipe for local package: %S", exc);
@@ -1316,34 +1562,45 @@
 		}
 
 		logDebug("iterating dir %s", path.toNativeString());
-		try foreach (pdir; iterateDirectory(path)) {
+		try foreach (pdir; mgr.iterateDirectory(path)) {
 			logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
 			if (!pdir.isDirectory) continue;
 
-			// Old / flat directory structure, used in non-standard path
-			// Packages are stored in $ROOT/$SOMETHING/`
 			const pack_path = path ~ (pdir.name ~ "/");
-			auto packageFile = Package.findPackageFile(pack_path);
-			if (!packageFile.empty) {
-				// Deprecated unmanaged directory structure
-				logWarn("Package at path '%s' should be under '%s'",
-						pack_path.toNativeString().color(Mode.bold),
-						(pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold));
-				logWarn("The package will no longer be detected starting from v1.42.0");
-				loadInternal(pack_path, packageFile);
-			}
+			auto packageFile = mgr.findPackageFile(pack_path);
 
-			// Managed structure: $ROOT/$NAME/$VERSION/$NAME
-			// This is the most common code path
-			else {
-				// Iterate over versions of a package
-				foreach (versdir; iterateDirectory(pack_path)) {
-					if (!versdir.isDirectory) continue;
-					auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/");
-					if (!vers_path.existsDirectory()) continue;
-					packageFile = Package.findPackageFile(vers_path);
-					loadInternal(vers_path, packageFile);
+			if (isManaged(path)) {
+				// Old / flat directory structure, used in non-standard path
+				// Packages are stored in $ROOT/$SOMETHING/`
+				if (!packageFile.empty) {
+					// Deprecated flat managed directory structure
+					logWarn("Package at path '%s' should be under '%s'",
+							pack_path.toNativeString().color(Mode.bold),
+							(pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold));
+					logWarn("The package will no longer be detected starting from v1.42.0");
+					loadInternal(pack_path, packageFile);
+				} else {
+					// New managed structure: $ROOT/$NAME/$VERSION/$NAME
+					// This is the most common code path
+
+					// Iterate over versions of a package
+					foreach (versdir; mgr.iterateDirectory(pack_path)) {
+						if (!versdir.isDirectory) continue;
+						auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/");
+						if (!mgr.existsDirectory(vers_path)) continue;
+						packageFile = mgr.findPackageFile(vers_path);
+						loadInternal(vers_path, packageFile);
+					}
 				}
+			} else {
+				// Unmanaged directories (dub add-path) are always stored as a
+				// flat list of packages, as these are the working copies managed
+				// by the user. The nested structure should not be supported,
+				// even optionally, because that would lead to bogus "no package
+				// file found" errors in case the internal directory structure
+				// accidentally matches the $NAME/$VERSION/$NAME scheme
+				if (!packageFile.empty)
+					loadInternal(pack_path, packageFile);
 			}
 		}
 		catch (Exception e)
@@ -1364,13 +1621,15 @@
 	 * Returns:
 	 *	 A `Package` if one was found, `null` if none exists.
 	 */
-	inout(Package) lookup(string name, Version ver, PackageManager mgr) inout {
+	inout(Package) lookup(in PackageName name, in Version ver) inout {
 		foreach (pkg; this.localPackages)
-			if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.standard))
+			if (pkg.name == name.toString() &&
+				pkg.version_.matches(ver, VersionMatchMode.standard))
 				return pkg;
 		foreach (pkg; this.fromPath) {
-			auto pvm = mgr.isManagedPackage(pkg) ? VersionMatchMode.strict : VersionMatchMode.standard;
-			if (pkg.name == name && pkg.version_.matches(ver, pvm))
+			auto pvm = this.isManaged(pkg.basePackage.path) ?
+				VersionMatchMode.strict : VersionMatchMode.standard;
+			if (pkg.name == name.toString() && pkg.version_.matches(ver, pvm))
 				return pkg;
 		}
 		return null;
@@ -1391,19 +1650,18 @@
 	 * Returns:
 	 *	 A `Package` if one was found, `null` if none exists.
 	 */
-	Package load (string name, Version vers, PackageManager mgr)
+	Package load (in PackageName name, Version vers, PackageManager mgr)
 	{
-		if (auto pkg = this.lookup(name, vers, mgr))
+		if (auto pkg = this.lookup(name, vers))
 			return pkg;
 
 		string versStr = vers.toString();
-		const lookupName = getBasePackageName(name);
-		const path = this.getPackagePath(lookupName, versStr) ~ (lookupName ~ "/");
-		if (!path.existsDirectory())
+		const path = this.getPackagePath(name, versStr);
+		if (!mgr.existsDirectory(path))
 			return null;
 
-		logDiagnostic("Lazily loading package %s:%s from %s", lookupName, vers, path);
-		auto p = Package.load(path);
+		logDiagnostic("Lazily loading package %s:%s from %s", name.main, vers, path);
+		auto p = mgr.load(path);
 		enforce(
 			p.version_ == vers,
 			format("Package %s located in %s has a different version than its path: Got %s, expected %s",
@@ -1419,16 +1677,31 @@
 	 * which expects their containing folder to have an exact name and use
 	 * `importPath "../"`.
 	 *
-	 * Hence the final format should be `$BASE/$NAME-$VERSION/$NAME`,
-	 * but this function returns `$BASE/$NAME-$VERSION/`
+	 * Hence the final format returned is `$BASE/$NAME/$VERSION/$NAME`,
 	 * `$BASE` is `this.packagePath`.
+	 *
+	 * Params:
+	 *   name = The package name - if the name is that of a subpackage,
+	 *          only the path to the main package is returned, as the
+	 *          subpackage path can only be known after reading the recipe.
+	 *   vers = A version string. Typed as a string because git hashes
+	 *          can be used with this function.
+	 *
+	 * Returns:
+	 *   An absolute `NativePath` nested in this location.
 	 */
-	NativePath getPackagePath (string name, string vers)
+	NativePath getPackagePath (in PackageName name, string vers)
 	{
-		NativePath result = this.packagePath ~ name ~ vers;
+		NativePath result = this.packagePath ~ name.main.toString() ~ vers ~
+			name.main.toString();
 		result.endsWithSlash = true;
 		return result;
 	}
+
+	/// Determines if a specific path is within a DUB managed Location.
+	bool isManaged(NativePath path) const {
+		return path.startsWith(this.packagePath);
+	}
 }
 
 private immutable string OverrideDepMsg =
diff --git a/source/dub/packagesuppliers/fallback.d b/source/dub/packagesuppliers/fallback.d
index b9235c7..a2009d6 100644
--- a/source/dub/packagesuppliers/fallback.d
+++ b/source/dub/packagesuppliers/fallback.d
@@ -28,9 +28,9 @@
 	}
 
 	// Workaround https://issues.dlang.org/show_bug.cgi?id=2525
-	abstract override Version[] getVersions(string package_id);
-	abstract override void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release);
-	abstract override Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release);
+	abstract override Version[] getVersions(in PackageName name);
+	abstract override ubyte[] fetchPackage(in PackageName name, in VersionRange dep, bool pre_release);
+	abstract override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, bool pre_release);
 	abstract override SearchResult[] searchPackages(string query);
 }
 
diff --git a/source/dub/packagesuppliers/filesystem.d b/source/dub/packagesuppliers/filesystem.d
index 4af88c4..657f4c2 100644
--- a/source/dub/packagesuppliers/filesystem.d
+++ b/source/dub/packagesuppliers/filesystem.d
@@ -1,7 +1,11 @@
 module dub.packagesuppliers.filesystem;
 
+import dub.internal.logging;
+import dub.internal.vibecompat.inet.path;
 import dub.packagesuppliers.packagesupplier;
 
+import std.exception : enforce;
+
 /**
 	File system based package supplier.
 
@@ -9,10 +13,6 @@
 	the form "[package name]-[version].zip".
 */
 class FileSystemPackageSupplier : PackageSupplier {
-	import dub.internal.logging;
-
-	version (Have_vibe_core) import dub.internal.vibecompat.inet.path : toNativeString;
-	import std.exception : enforce;
 	private {
 		NativePath m_path;
 	}
@@ -21,17 +21,22 @@
 
 	override @property string description() { return "file repository at "~m_path.toNativeString(); }
 
-	Version[] getVersions(string package_id)
+	Version[] getVersions(in PackageName name)
 	{
 		import std.algorithm.sorting : sort;
 		import std.file : dirEntries, DirEntry, SpanMode;
 		import std.conv : to;
+		import dub.semver : isValidVersion;
 		Version[] ret;
-		foreach (DirEntry d; dirEntries(m_path.toNativeString(), package_id~"*", SpanMode.shallow)) {
+        const zipFileGlob = name.main.toString() ~ "*.zip";
+		foreach (DirEntry d; dirEntries(m_path.toNativeString(), zipFileGlob, SpanMode.shallow)) {
 			NativePath p = NativePath(d.name);
+			auto vers = p.head.name[name.main.toString().length+1..$-4];
+			if (!isValidVersion(vers)) {
+				logDebug("Ignoring entry '%s' because it isn't a version of package '%s'", p, name.main);
+				continue;
+			}
 			logDebug("Entry: %s", p);
-			enforce(to!string(p.head)[$-4..$] == ".zip");
-			auto vers = p.head.name[package_id.length+1..$-4];
 			logDebug("Version: %s", vers);
 			ret ~= Version(vers);
 		}
@@ -39,17 +44,18 @@
 		return ret;
 	}
 
-	void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release)
+	override ubyte[] fetchPackage(in PackageName name,
+		in VersionRange dep, bool pre_release)
 	{
-		import dub.internal.vibecompat.core.file : copyFile, existsFile;
-		enforce(path.absolute);
-		logInfo("Storing package '%s', version requirements: %s", packageId, dep);
-		auto filename = bestPackageFile(packageId, dep, pre_release);
+		import dub.internal.vibecompat.core.file : readFile, existsFile;
+		logInfo("Storing package '%s', version requirements: %s", name.main, dep);
+		auto filename = bestPackageFile(name, dep, pre_release);
 		enforce(existsFile(filename));
-		copyFile(filename, path);
+		return readFile(filename);
 	}
 
-	Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release)
+	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
+		bool pre_release)
 	{
 		import std.array : split;
 		import std.path : stripExtension;
@@ -58,15 +64,16 @@
 		import dub.recipe.io : parsePackageRecipe;
 		import dub.recipe.json : toJson;
 
-		auto filePath = bestPackageFile(packageId, dep, pre_release);
+		auto filePath = bestPackageFile(name, dep, pre_release);
 		string packageFileName;
 		string packageFileContent = packageInfoFileFromZip(filePath, packageFileName);
 		auto recipe = parsePackageRecipe(packageFileContent, packageFileName);
 		Json json = toJson(recipe);
 		auto basename = filePath.head.name;
 		enforce(basename.endsWith(".zip"), "Malformed package filename: " ~ filePath.toNativeString);
-		enforce(basename.startsWith(packageId), "Malformed package filename: " ~ filePath.toNativeString);
-		json["version"] = basename[packageId.length + 1 .. $-4];
+		enforce(basename.startsWith(name.main.toString()),
+			"Malformed package filename: " ~ filePath.toNativeString);
+		json["version"] = basename[name.main.toString().length + 1 .. $-4];
 		return json;
 	}
 
@@ -76,16 +83,17 @@
 		return null;
 	}
 
-	private NativePath bestPackageFile(string packageId, in VersionRange dep, bool pre_release)
+	private NativePath bestPackageFile(in PackageName name, in VersionRange dep,
+		bool pre_release)
 	{
 		import std.algorithm.iteration : filter;
 		import std.array : array;
 		import std.format : format;
 		NativePath toPath(Version ver) {
-			return m_path ~ (packageId ~ "-" ~ ver.toString() ~ ".zip");
+			return m_path ~ "%s-%s.zip".format(name.main, ver);
 		}
-		auto versions = getVersions(packageId).filter!(v => dep.matches(v)).array;
-		enforce(versions.length > 0, format("No package %s found matching %s", packageId, dep));
+		auto versions = getVersions(name).filter!(v => dep.matches(v)).array;
+		enforce(versions.length > 0, format("No package %s found matching %s", name.main, dep));
 		foreach_reverse (ver; versions) {
 			if (pre_release || !ver.isPreRelease)
 				return toPath(ver);
diff --git a/source/dub/packagesuppliers/maven.d b/source/dub/packagesuppliers/maven.d
index ecb29b1..bb0a019 100644
--- a/source/dub/packagesuppliers/maven.d
+++ b/source/dub/packagesuppliers/maven.d
@@ -20,7 +20,7 @@
 		enum httpTimeout = 16;
 		URL m_mavenUrl;
 		struct CacheEntry { Json data; SysTime cacheTime; }
-		CacheEntry[string] m_metadataCache;
+		CacheEntry[PackageName] m_metadataCache;
 		Duration m_maxCacheTime;
 	}
 
@@ -32,10 +32,10 @@
 
 	override @property string description() { return "maven repository at "~m_mavenUrl.toString(); }
 
-	Version[] getVersions(string package_id)
+	override Version[] getVersions(in PackageName name)
 	{
 		import std.algorithm.sorting : sort;
-		auto md = getMetadata(package_id);
+		auto md = getMetadata(name.main);
 		if (md.type == Json.Type.null_)
 			return null;
 		Version[] ret;
@@ -47,74 +47,82 @@
 		return ret;
 	}
 
-	void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release)
+	override ubyte[] fetchPackage(in PackageName name,
+		in VersionRange dep, bool pre_release)
 	{
 		import std.format : format;
-		auto md = getMetadata(packageId);
-		Json best = getBestPackage(md, packageId, dep, pre_release);
+		auto md = getMetadata(name.main);
+		Json best = getBestPackage(md, name.main, dep, pre_release);
 		if (best.type == Json.Type.null_)
-			return;
+			return null;
 		auto vers = best["version"].get!string;
-		auto url = m_mavenUrl~NativePath("%s/%s/%s-%s.zip".format(packageId, vers, packageId, vers));
+		auto url = m_mavenUrl ~ NativePath(
+			"%s/%s/%s-%s.zip".format(name.main, vers, name.main, vers));
 
 		try {
-			retryDownload(url, path, 3, httpTimeout);
-			return;
+			return retryDownload(url, 3, httpTimeout);
 		}
 		catch(HTTPStatusException e) {
 			if (e.status == 404) throw e;
-			else logDebug("Failed to download package %s from %s", packageId, url);
+			else logDebug("Failed to download package %s from %s", name.main, url);
 		}
 		catch(Exception e) {
-			logDebug("Failed to download package %s from %s", packageId, url);
+			logDebug("Failed to download package %s from %s", name.main, url);
 		}
-		throw new Exception("Failed to download package %s from %s".format(packageId, url));
+		throw new Exception("Failed to download package %s from %s".format(name.main, url));
 	}
 
-	Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release)
+	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
+		bool pre_release)
 	{
-		auto md = getMetadata(packageId);
-		return getBestPackage(md, packageId, dep, pre_release);
+		auto md = getMetadata(name);
+		return getBestPackage(md, name, dep, pre_release);
 	}
 
-	private Json getMetadata(string packageId)
+	private Json getMetadata(in PackageName name)
 	{
 		import dub.internal.undead.xml;
 
 		auto now = Clock.currTime(UTC());
-		if (auto pentry = packageId in m_metadataCache) {
+		if (auto pentry = name.main in m_metadataCache) {
 			if (pentry.cacheTime + m_maxCacheTime > now)
 				return pentry.data;
-			m_metadataCache.remove(packageId);
+			m_metadataCache.remove(name.main);
 		}
 
-		auto url = m_mavenUrl~NativePath(packageId~"/maven-metadata.xml");
+		auto url = m_mavenUrl ~ NativePath(name.main.toString() ~ "/maven-metadata.xml");
 
-		logDebug("Downloading maven metadata for %s", packageId);
+		logDebug("Downloading maven metadata for %s", name.main);
 		string xmlData;
 
 		try
 			xmlData = cast(string)retryDownload(url, 3, httpTimeout);
 		catch(HTTPStatusException e) {
 			if (e.status == 404) {
-				logDebug("Maven metadata %s not found at %s (404): %s", packageId, description, e.msg);
+				logDebug("Maven metadata %s not found at %s (404): %s", name.main, description, e.msg);
 				return Json(null);
 			}
 			else throw e;
 		}
 
-		auto json = Json(["name": Json(packageId), "versions": Json.emptyArray]);
+		auto json = Json([
+			"name": Json(name.main.toString()),
+			"versions": Json.emptyArray
+		]);
 		auto xml = new DocumentParser(xmlData);
 
 		xml.onStartTag["versions"] = (ElementParser xml) {
 			 xml.onEndTag["version"] = (in Element e) {
-				json["versions"] ~= serializeToJson(["name": packageId, "version": e.text]);
+				json["versions"] ~= serializeToJson([
+					"name": name.main.toString(),
+					"version": e.text,
+				]);
 			 };
 			 xml.parse();
 		};
 		xml.parse();
 
-		m_metadataCache[packageId] = CacheEntry(json, now);
+		m_metadataCache[name.main] = CacheEntry(json, now);
 		return json;
 	}
 
@@ -122,10 +130,10 @@
 	{
 		// Only exact search is supported
 		// This enables retrieval of dub packages on dub run
-		auto md = getMetadata(query);
+		auto md = getMetadata(PackageName(query));
 		if (md.type == Json.Type.null_)
 			return null;
-		auto json = getBestPackage(md, query, VersionRange.Any, true);
+		auto json = getBestPackage(md, PackageName(query), VersionRange.Any, true);
 		return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)];
 	}
 }
diff --git a/source/dub/packagesuppliers/packagesupplier.d b/source/dub/packagesuppliers/packagesupplier.d
index 71f8b8d..fd7e40d 100644
--- a/source/dub/packagesuppliers/packagesupplier.d
+++ b/source/dub/packagesuppliers/packagesupplier.d
@@ -1,6 +1,6 @@
 module dub.packagesuppliers.packagesupplier;
 
-public import dub.dependency : Dependency, Version, VersionRange;
+public import dub.dependency : PackageName, Dependency, Version, VersionRange;
 import dub.dependency : visit;
 public import dub.internal.vibecompat.core.file : NativePath;
 public import dub.internal.vibecompat.data.json : Json;
@@ -23,25 +23,41 @@
 		Throws: Throws an exception if the package name is not known, or if
 			an error occurred while retrieving the version list.
 	*/
-	Version[] getVersions(string package_id);
+	deprecated("Use `getVersions(PackageName)` instead")
+	final Version[] getVersions(string name)
+	{
+		return this.getVersions(PackageName(name));
+	}
 
-	/** Downloads a package and stores it as a ZIP file.
+	Version[] getVersions(in PackageName name);
+
+
+	/** Downloads a package and returns its binary content
 
 		Params:
-			path = Absolute path of the target ZIP file
-			package_id = Name of the package to retrieve
+			name = Name of the package to retrieve
 			dep = Version constraint to match against
 			pre_release = If true, matches the latest pre-release version.
 				Otherwise prefers stable versions.
 	*/
-	void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release);
+	ubyte[] fetchPackage(in PackageName name, in VersionRange dep,
+		bool pre_release);
 
-    deprecated("Use the overload that accepts a `VersionRange` instead")
-	final void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release)
+	deprecated("Use `writeFile(path, fetchPackage(PackageName, VersionRange, bool))` instead")
+	final void fetchPackage(in NativePath path, in PackageName name,
+		in VersionRange dep, bool pre_release)
+	{
+		import dub.internal.vibecompat.core.file : writeFile;
+		if (auto res = this.fetchPackage(name, dep, pre_release))
+			writeFile(path, res);
+	}
+
+    deprecated("Use `fetchPackage(NativePath, PackageName, VersionRange, bool)` instead")
+	final void fetchPackage(NativePath path, string name, Dependency dep, bool pre_release)
     {
         return dep.visit!(
             (const VersionRange rng) {
-                return this.fetchPackage(path, package_id, rng, pre_release);
+                return this.fetchPackage(path, PackageName(name), rng, pre_release);
             }, (any) {
                 assert(0, "Trying to fetch a package with a non-version dependency: " ~ any.toString());
             },
@@ -56,14 +72,14 @@
 			pre_release = If true, matches the latest pre-release version.
 				Otherwise prefers stable versions.
 	*/
-	Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release);
+	Json fetchPackageRecipe(in PackageName name, in VersionRange dep, bool pre_release);
 
-    deprecated("Use the overload that accepts a `VersionRange` instead")
-	final Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release)
+    deprecated("Use `fetchPackageRecipe(PackageName, VersionRange, bool)` instead")
+	final Json fetchPackageRecipe(string name, Dependency dep, bool pre_release)
     {
         return dep.visit!(
             (const VersionRange rng) {
-                return this.fetchPackageRecipe(package_id, rng, pre_release);
+                return this.fetchPackageRecipe(PackageName(name), rng, pre_release);
             }, (any) {
                 return Json.init;
             },
@@ -84,9 +100,12 @@
 //       a package recipe instead of one (first get version list, then the
 //       package recipe)
 
-package Json getBestPackage(Json metadata, string packageId, in VersionRange dep, bool pre_release)
+package Json getBestPackage(Json metadata, in PackageName name,
+	in VersionRange dep, bool pre_release)
 {
 	import std.exception : enforce;
+	import std.format : format;
+
 	if (metadata.type == Json.Type.null_)
 		return metadata;
 	Json best = null;
@@ -102,6 +121,7 @@
 		} else if (!cur.isPreRelease && cur > bestver) best = json;
 		bestver = Version(cast(string)best["version"]);
 	}
-	enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString());
+	enforce(best != null,
+		"No package candidate found for %s@%s".format(name.main, dep));
 	return best;
 }
diff --git a/source/dub/packagesuppliers/registry.d b/source/dub/packagesuppliers/registry.d
index b509970..9ba9ccc 100644
--- a/source/dub/packagesuppliers/registry.d
+++ b/source/dub/packagesuppliers/registry.d
@@ -12,7 +12,7 @@
 	$(LINK https://code.dlang.org/)) to search for available packages.
 */
 class RegistryPackageSupplier : PackageSupplier {
-	import dub.internal.utils : download, retryDownload, HTTPStatusException;
+	import dub.internal.utils : retryDownload, HTTPStatusException;
 	import dub.internal.vibecompat.data.json : parseJson, parseJsonString, serializeToJson;
 	import dub.internal.vibecompat.inet.url : URL;
 	import dub.internal.logging;
@@ -22,7 +22,7 @@
 	private {
 		URL m_registryUrl;
 		struct CacheEntry { Json data; SysTime cacheTime; }
-		CacheEntry[string] m_metadataCache;
+		CacheEntry[PackageName] m_metadataCache;
 		Duration m_maxCacheTime;
 	}
 
@@ -34,10 +34,10 @@
 
 	override @property string description() { return "registry at "~m_registryUrl.toString(); }
 
-	Version[] getVersions(string package_id)
+	override Version[] getVersions(in PackageName name)
 	{
 		import std.algorithm.sorting : sort;
-		auto md = getMetadata(package_id);
+		auto md = getMetadata(name);
 		if (md.type == Json.Type.null_)
 			return null;
 		Version[] ret;
@@ -49,63 +49,66 @@
 		return ret;
 	}
 
-	auto genPackageDownloadUrl(string packageId, in VersionRange dep, bool pre_release)
+	auto genPackageDownloadUrl(in PackageName name, in VersionRange dep, bool pre_release)
 	{
 		import std.array : replace;
 		import std.format : format;
 		import std.typecons : Nullable;
-		auto md = getMetadata(packageId);
-		Json best = getBestPackage(md, packageId, dep, pre_release);
+		auto md = getMetadata(name);
+		Json best = getBestPackage(md, name, dep, pre_release);
 		Nullable!URL ret;
 		if (best.type != Json.Type.null_)
 		{
 			auto vers = best["version"].get!string;
-			ret = m_registryUrl ~ NativePath(PackagesPath~"/"~packageId~"/"~vers~".zip");
+			ret = m_registryUrl ~ NativePath(
+				"%s/%s/%s.zip".format(PackagesPath, name.main, vers));
 		}
 		return ret;
 	}
 
-	void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release)
+	override ubyte[] fetchPackage(in PackageName name,
+		in VersionRange dep, bool pre_release)
 	{
 		import std.format : format;
-		auto url = genPackageDownloadUrl(packageId, dep, pre_release);
-		if(url.isNull)
-			return;
+
+		auto url = genPackageDownloadUrl(name, dep, pre_release);
+		if(url.isNull) return null;
 		try {
-			retryDownload(url.get, path);
-			return;
+			return retryDownload(url.get);
 		}
 		catch(HTTPStatusException e) {
 			if (e.status == 404) throw e;
-			else logDebug("Failed to download package %s from %s", packageId, url);
+			else logDebug("Failed to download package %s from %s", name.main, url);
 		}
 		catch(Exception e) {
-			logDebug("Failed to download package %s from %s", packageId, url);
+			logDebug("Failed to download package %s from %s", name.main, url);
 		}
-		throw new Exception("Failed to download package %s from %s".format(packageId, url));
+		throw new Exception("Failed to download package %s from %s".format(name.main, url));
 	}
 
-	Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release)
+	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
+		bool pre_release)
 	{
-		auto md = getMetadata(packageId);
-		return getBestPackage(md, packageId, dep, pre_release);
+		auto md = getMetadata(name);
+		return getBestPackage(md, name, dep, pre_release);
 	}
 
-	private Json getMetadata(string packageId)
+	private Json getMetadata(in PackageName name)
 	{
 		auto now = Clock.currTime(UTC());
-		if (auto pentry = packageId in m_metadataCache) {
+		if (auto pentry = name.main in m_metadataCache) {
 			if (pentry.cacheTime + m_maxCacheTime > now)
 				return pentry.data;
-			m_metadataCache.remove(packageId);
+			m_metadataCache.remove(name.main);
 		}
 
 		auto url = m_registryUrl ~ NativePath("api/packages/infos");
 
 		url.queryString = "packages=" ~
-				encodeComponent(`["` ~ packageId ~ `"]`) ~ "&include_dependencies=true&minimize=true";
+			encodeComponent(`["` ~ name.main.toString() ~ `"]`) ~
+			"&include_dependencies=true&minimize=true";
 
-		logDebug("Downloading metadata for %s", packageId);
+		logDebug("Downloading metadata for %s", name.main);
 		string jsonData;
 
 		jsonData = cast(string)retryDownload(url);
@@ -114,9 +117,9 @@
 		foreach (pkg, info; json.get!(Json[string]))
 		{
 			logDebug("adding %s to metadata cache", pkg);
-			m_metadataCache[pkg] = CacheEntry(info, now);
+			m_metadataCache[PackageName(pkg)] = CacheEntry(info, now);
 		}
-		return json[packageId];
+		return json[name.main.toString()];
 	}
 
 	SearchResult[] searchPackages(string query) {
diff --git a/source/dub/platform.d b/source/dub/platform.d
index 833061a..9d2150f 100644
--- a/source/dub/platform.d
+++ b/source/dub/platform.d
@@ -15,7 +15,6 @@
 module dub.platform;
 
 import std.array;
-public import dub.data.platform;
 
 // archCheck, compilerCheck, and platformCheck are used below and in
 // generatePlatformProbeFile, so they've been extracted into these strings
@@ -99,6 +98,10 @@
 	version(Alpha) ret ~= "alpha";
 	version(Alpha_SoftFP) ret ~= "alpha_softfp";
 	version(Alpha_HardFP) ret ~= "alpha_hardfp";
+	version(LoongArch32) ret ~= "loongarch32";
+	version(LoongArch64) ret ~= "loongarch64";
+	version(LoongArch_SoftFloat) ret ~= "loongarch_softfloat";
+	version(LoongArch_HardFloat) ret ~= "loongarch_hardfloat";
 	return ret;
 };
 
@@ -111,6 +114,21 @@
 	else return null;
 };
 
+/// private
+enum string compilerCheckPragmas = q{
+	version(DigitalMars) pragma(msg, ` "dmd"`);
+	else version(GNU) pragma(msg, ` "gdc"`);
+	else version(LDC) pragma(msg, ` "ldc"`);
+	else version(SDC) pragma(msg, ` "sdc"`);
+};
+
+/// private, converts the above appender strings to pragmas
+string pragmaGen(string str) {
+	import std.string : replace;
+	return str.replace("return ret;", "").replace("string[] ret;", "").replace(`["`, `"`).replace(`", "`,`" "`).replace(`"]`, `"`).replace(`;`, "`);").replace("ret ~= ", "pragma(msg, ` ");
+}
+
+
 /** Determines the full build platform used for the current build.
 
 	Note that the `BuildPlatform.compilerBinary` field will be left empty.
@@ -167,3 +185,119 @@
 {
 	mixin(compilerCheck);
 }
+
+/** Matches a platform specification string against a build platform.
+
+	Specifications are build upon the following scheme, where each component
+	is optional (indicated by []), but the order is obligatory:
+	"[-platform][-architecture][-compiler]"
+
+	So the following strings are valid specifications: `"-windows-x86-dmd"`,
+	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
+
+	Params:
+		platform = The build platform to match against the platform specification
+	    specification = The specification being matched. It must either be an
+	    	empty string or start with a dash.
+
+	Returns:
+	    `true` if the given specification matches the build platform, `false`
+	    otherwise. Using an empty string as the platform specification will
+	    always result in a match.
+*/
+bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
+{
+	import std.string : chompPrefix, format;
+	import std.algorithm : canFind, splitter;
+	import std.exception : enforce;
+
+	if (specification.empty) return true;
+	if (platform == BuildPlatform.any) return true;
+
+	auto splitted = specification.chompPrefix("-").splitter('-');
+	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
+
+	if (platform.platform.canFind(splitted.front)) {
+		splitted.popFront();
+		if (splitted.empty)
+			return true;
+	}
+	if (platform.architecture.canFind(splitted.front)) {
+		splitted.popFront();
+		if (splitted.empty)
+			return true;
+	}
+	if (platform.compiler == splitted.front) {
+		splitted.popFront();
+		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
+		return true;
+	}
+	return false;
+}
+
+///
+unittest {
+	auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
+	assert(platform.matchesSpecification(""));
+	assert(platform.matchesSpecification("posix"));
+	assert(platform.matchesSpecification("linux"));
+	assert(platform.matchesSpecification("linux-dmd"));
+	assert(platform.matchesSpecification("linux-x86_64-dmd"));
+	assert(platform.matchesSpecification("x86_64"));
+	assert(!platform.matchesSpecification("windows"));
+	assert(!platform.matchesSpecification("ldc"));
+	assert(!platform.matchesSpecification("windows-dmd"));
+
+	// Before PR#2279, a leading '-' was required
+	assert(platform.matchesSpecification("-x86_64"));
+}
+
+/// Represents a platform a package can be build upon.
+struct BuildPlatform {
+	/// Special constant used to denote matching any build platform.
+	enum any = BuildPlatform(null, null, null, null, -1);
+
+	/// Platform identifiers, e.g. ["posix", "windows"]
+	string[] platform;
+	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
+	string[] architecture;
+	/// Canonical compiler name e.g. "dmd"
+	string compiler;
+	/// Compiler binary name e.g. "ldmd2"
+	string compilerBinary;
+	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
+	int frontendVersion;
+	/// Compiler version e.g. "1.11.0"
+	string compilerVersion;
+	/// Frontend version string from frontendVersion
+	/// e.g: 2067 => "2.067"
+	string frontendVersionString() const
+	{
+		import std.format : format;
+
+		const maj = frontendVersion / 1000;
+		const min = frontendVersion % 1000;
+		return format("%d.%03d", maj, min);
+	}
+	///
+	unittest
+	{
+		BuildPlatform bp;
+		bp.frontendVersion = 2067;
+		assert(bp.frontendVersionString == "2.067");
+	}
+
+	/// Checks to see if platform field contains windows
+	bool isWindows() const {
+		import std.algorithm : canFind;
+		return this.platform.canFind("windows");
+	}
+	///
+	unittest {
+		BuildPlatform bp;
+		bp.platform = ["windows"];
+		assert(bp.isWindows);
+		bp.platform = ["posix"];
+		assert(!bp.isWindows);
+	}
+}
diff --git a/source/dub/project.d b/source/dub/project.d
index 5826101..0f4ce82 100644
--- a/source/dub/project.d
+++ b/source/dub/project.d
@@ -57,6 +57,7 @@
 			project_path = Path of the root package to load
 			pack = An existing `Package` instance to use as the root package
 	*/
+	deprecated("Load the package using `PackageManager.getOrLoadPackage` then call the `(PackageManager, Package)` overload")
 	this(PackageManager package_manager, NativePath project_path)
 	{
 		Package pack;
@@ -103,6 +104,9 @@
 	 */
 	static package SelectedVersions loadSelections(in Package pack)
 	{
+		import dub.version_;
+		import dub.internal.dyaml.stdsumtype;
+
 		auto selverfile = (pack.path ~ SelectedVersions.defaultFile).toNativeString();
 
 		// No file exists
@@ -112,13 +116,23 @@
 		// TODO: Remove `StrictMode.Warn` after v1.40 release
 		// The default is to error, but as the previous parser wasn't
 		// complaining, we should first warn the user.
-		auto selected = parseConfigFileSimple!Selected(selverfile, StrictMode.Warn);
+		auto selected = parseConfigFileSimple!SelectionsFile(selverfile, StrictMode.Warn);
 
 		// Parsing error, it will be displayed to the user
 		if (selected.isNull())
 			return new SelectedVersions();
 
-		return new SelectedVersions(selected.get());
+		return selected.get().content.match!(
+			(Selections!0 s) {
+				logWarnTag("Unsupported version",
+					"File %s has fileVersion %s, which is not yet supported by DUB %s.",
+					selverfile, s.fileVersion, dubVersion);
+				logWarn("Ignoring selections file. Use a newer DUB version " ~
+					"and set the appropriate toolchainRequirements in your recipe file");
+				return new SelectedVersions();
+			},
+			(Selections!1 s) => new SelectedVersions(s),
+		);
 	}
 
 	/** List of all resolved dependencies.
@@ -180,12 +194,12 @@
 				if (!cfg.length) deps = p.getAllDependencies();
 				else {
 					auto depmap = p.getDependencies(cfg);
-					deps = depmap.byKey.map!(k => PackageDependency(k, depmap[k])).array;
+					deps = depmap.byKey.map!(k => PackageDependency(PackageName(k), depmap[k])).array;
 				}
-				deps.sort!((a, b) => a.name < b.name);
+				deps.sort!((a, b) => a.name.toString() < b.name.toString());
 
 				foreach (d; deps) {
-					auto dependency = getDependency(d.name, true);
+					auto dependency = getDependency(d.name.toString(), true);
 					assert(dependency || d.spec.optional,
 						format("Non-optional dependency '%s' of '%s' not found in dependency tree!?.", d.name, p.name));
 					if(dependency) perform_rec(dependency);
@@ -419,7 +433,8 @@
 					? format(`dependency "%s" repository="git+<git url>" version="<commit>"`, d.name)
 					: format(`"%s": {"repository": "git+<git url>", "version": "<commit>"}`, d.name);
 				logWarn("Dependency '%s' depends on git branch '%s', which is deprecated.",
-					d.name.color(Mode.bold), d.spec.version_.toString.color(Mode.bold));
+					d.name.toString().color(Mode.bold),
+					d.spec.version_.toString.color(Mode.bold));
 				logWarnTag("", "Specify the git repository and commit hash in your %s:",
 					(isSDL ? "dub.sdl" : "dub.json").color(Mode.bold));
 				logWarnTag("", "%s", suggestion.color(Mode.bold));
@@ -432,25 +447,25 @@
 				~ "and will have no effect.", pack.color(Mode.bold), config.color(Color.blue));
 		}
 
-		void checkSubConfig(string pack, string config) {
-			auto p = getDependency(pack, true);
+		void checkSubConfig(in PackageName name, string config) {
+			auto p = getDependency(name.toString(), true);
 			if (p && !p.configurations.canFind(config)) {
 				logWarn("The sub configuration directive \"%s\" -> [%s] "
 					~ "references a configuration that does not exist.",
-					pack.color(Mode.bold), config.color(Color.red));
+					name.toString().color(Mode.bold), config.color(Color.red));
 			}
 		}
 		auto globalbs = m_rootPackage.getBuildSettings();
 		foreach (p, c; globalbs.subConfigurations) {
 			if (p !in globalbs.dependencies) warnSubConfig(p, c);
-			else checkSubConfig(p, c);
+			else checkSubConfig(PackageName(p), c);
 		}
 		foreach (c; m_rootPackage.configurations) {
 			auto bs = m_rootPackage.getBuildSettings(c);
 			foreach (p, subConf; bs.subConfigurations) {
 				if (p !in bs.dependencies && p !in globalbs.dependencies)
 					warnSubConfig(p, subConf);
-				else checkSubConfig(p, subConf);
+				else checkSubConfig(PackageName(p), subConf);
 			}
 		}
 
@@ -461,22 +476,26 @@
 			pack.simpleLint();
 
 			foreach (d; pack.getAllDependencies()) {
-				auto basename = getBasePackageName(d.name);
+				auto basename = d.name.main;
 				d.spec.visit!(
 					(NativePath path) { /* Valid */ },
 					(Repository repo) { /* Valid */ },
 					(VersionRange vers) {
 						if (m_selections.hasSelectedVersion(basename)) {
 							auto selver = m_selections.getSelectedVersion(basename);
-							if (d.spec.merge(selver) == Dependency.invalid) {
-								logWarn(`Selected package %s %s does not match the dependency specification %s in package %s. Need to "%s"?`,
-									basename.color(Mode.bold), selver, vers, pack.name.color(Mode.bold), "dub upgrade".color(Mode.bold));
+							if (d.spec.merge(selver) == Dependency.Invalid) {
+								logWarn(`Selected package %s@%s does not match ` ~
+								   `the dependency specification %s in ` ~
+								   `package %s. Need to "%s"?`,
+									basename.toString().color(Mode.bold), selver,
+									vers, pack.name.color(Mode.bold),
+									"dub upgrade".color(Mode.bold));
 							}
 						}
 					},
 				);
 
-				auto deppack = getDependency(d.name, true);
+				auto deppack = getDependency(d.name.toString(), true);
 				if (deppack in visited) continue;
 				visited[deppack] = true;
 				if (deppack) validateDependenciesRec(deppack);
@@ -485,119 +504,132 @@
 		validateDependenciesRec(m_rootPackage);
 	}
 
-	/// Reloads dependencies.
+	/**
+	 * Reloads dependencies
+	 *
+	 * This function goes through the project and make sure that all
+	 * required packages are loaded. To do so, it uses information
+	 * both from the recipe file (`dub.json`) and from the selections
+	 * file (`dub.selections.json`).
+	 *
+	 * In the process, it populates the `dependencies`, `missingDependencies`,
+	 * and `hasAllDependencies` properties, which can only be relied on
+	 * once this has run once (the constructor always calls this).
+	 */
 	void reinit()
 	{
 		m_dependencies = null;
 		m_missingDependencies = [];
-
-		Package resolveSubPackage(Package p, string subname, bool silentFail) {
-			if (!subname.length || p is null)
-				return p;
-			return m_packageManager.getSubPackage(p, subname, silentFail);
-		}
-
-		void collectDependenciesRec(Package pack, int depth = 0)
-		{
-			auto indent = replicate("  ", depth);
-			logDebug("%sCollecting dependencies for %s", indent, pack.name);
-			indent ~= "  ";
-
-			foreach (dep; pack.getAllDependencies()) {
-				Dependency vspec = dep.spec;
-				Package p;
-
-				auto basename = getBasePackageName(dep.name);
-				auto subname = getSubPackageName(dep.name);
-
-				// non-optional and optional-default dependencies (if no selections file exists)
-				// need to be satisfied
-				bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
-
-				if (dep.name == m_rootPackage.basePackage.name) {
-					vspec = Dependency(m_rootPackage.version_);
-					p = m_rootPackage.basePackage;
-				} else if (basename == m_rootPackage.basePackage.name) {
-					vspec = Dependency(m_rootPackage.version_);
-					try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
-					catch (Exception e) {
-						logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
-						if (is_desired) m_missingDependencies ~= dep.name;
-						continue;
-					}
-				} else if (m_selections.hasSelectedVersion(basename)) {
-					vspec = m_selections.getSelectedVersion(basename);
-					p = vspec.visit!(
-						(NativePath path_) {
-							auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_;
-							auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
-							return resolveSubPackage(tmp, subname, true);
-						},
-						(Repository repo) {
-							auto tmp = m_packageManager.loadSCMPackage(basename, repo);
-							return resolveSubPackage(tmp, subname, true);
-						},
-						(VersionRange range) {
-							// See `dub.recipe.selection : SelectedDependency.fromYAML`
-							assert(range.isExactVersion());
-							return m_packageManager.getPackage(dep.name, vspec.version_);
-						},
-					);
-				} else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) {
-					auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename);
-					auto bp = m_dependencies[idx].basePackage;
-					vspec = Dependency(bp.path);
-					p = resolveSubPackage(bp, subname, false);
-				} else {
-					logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
-						indent, basename, dep.name, pack.name);
-				}
-
-				// We didn't find the package
-				if (p is null)
-				{
-					if (!vspec.repository.empty) {
-						p = m_packageManager.loadSCMPackage(basename, vspec.repository);
-						resolveSubPackage(p, subname, false);
-						enforce(p !is null,
-							"Unable to fetch '%s@%s' using git - does the repository and version exists?".format(
-								dep.name, vspec.repository));
-					} else if (!vspec.path.empty && is_desired) {
-						NativePath path = vspec.path;
-						if (!path.absolute) path = pack.path ~ path;
-						logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
-						p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
-						if (p.parentPackage !is null) {
-							logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
-							p = p.parentPackage;
-						}
-						p = resolveSubPackage(p, subname, false);
-						enforce(p.name == dep.name,
-							format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
-								path.toNativeString(), dep.name, p.name));
-					} else {
-						logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
-						if (is_desired) m_missingDependencies ~= dep.name;
-						continue;
-					}
-				}
-
-				if (!m_dependencies.canFind(p)) {
-					logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
-					m_dependencies ~= p;
-					if (basename == m_rootPackage.basePackage.name)
-						p.warnOnSpecialCompilerFlags();
-					collectDependenciesRec(p, depth+1);
-				}
-
-				m_dependees[p] ~= pack;
-				//enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
-			}
-		}
 		collectDependenciesRec(m_rootPackage);
 		m_missingDependencies.sort();
 	}
 
+	/// Implementation of `reinit`
+	private void collectDependenciesRec(Package pack, int depth = 0)
+	{
+		auto indent = replicate("  ", depth);
+		logDebug("%sCollecting dependencies for %s", indent, pack.name);
+		indent ~= "  ";
+
+		foreach (dep; pack.getAllDependencies()) {
+			Dependency vspec = dep.spec;
+			Package p;
+
+			auto basename = dep.name.main;
+			auto subname = dep.name.sub;
+
+			// non-optional and optional-default dependencies (if no selections file exists)
+			// need to be satisfied
+			bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare);
+
+			if (dep.name.toString() == m_rootPackage.basePackage.name) {
+				vspec = Dependency(m_rootPackage.version_);
+				p = m_rootPackage.basePackage;
+			} else if (basename.toString() == m_rootPackage.basePackage.name) {
+				vspec = Dependency(m_rootPackage.version_);
+				try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false);
+				catch (Exception e) {
+					logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg);
+					if (is_desired) m_missingDependencies ~= dep.name.toString();
+					continue;
+				}
+			} else if (m_selections.hasSelectedVersion(basename)) {
+				vspec = m_selections.getSelectedVersion(basename);
+				p = vspec.visit!(
+					(NativePath path_) {
+						auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_;
+						auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
+						return resolveSubPackage(tmp, subname, true);
+					},
+					(Repository repo) {
+						auto tmp = m_packageManager.loadSCMPackage(basename, repo);
+						return resolveSubPackage(tmp, subname, true);
+					},
+					(VersionRange range) {
+						// See `dub.recipe.selection : SelectedDependency.fromYAML`
+						assert(range.isExactVersion());
+						return m_packageManager.getPackage(dep.name, vspec.version_);
+					},
+				);
+			} else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) {
+				auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename);
+				auto bp = m_dependencies[idx].basePackage;
+				vspec = Dependency(bp.path);
+				p = resolveSubPackage(bp, subname, false);
+			} else {
+				logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.",
+					indent, basename, dep.name, pack.name);
+			}
+
+			// We didn't find the package
+			if (p is null)
+			{
+				if (!vspec.repository.empty) {
+					p = m_packageManager.loadSCMPackage(basename, vspec.repository);
+					resolveSubPackage(p, subname, false);
+					enforce(p !is null,
+						"Unable to fetch '%s@%s' using git - does the repository and version exists?".format(
+							dep.name, vspec.repository));
+				} else if (!vspec.path.empty && is_desired) {
+					NativePath path = vspec.path;
+					if (!path.absolute) path = pack.path ~ path;
+					logDiagnostic("%sAdding local %s in %s", indent, dep.name, path);
+					p = m_packageManager.getOrLoadPackage(path, NativePath.init, true);
+					if (p.parentPackage !is null) {
+						logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name);
+						p = p.parentPackage;
+					}
+					p = resolveSubPackage(p, subname, false);
+					enforce(p.name == dep.name.toString(),
+						format("Path based dependency %s is referenced with a wrong name: %s vs. %s",
+							path.toNativeString(), dep.name, p.name));
+				} else {
+					logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name);
+					if (is_desired) m_missingDependencies ~= dep.name.toString();
+					continue;
+				}
+			}
+
+			if (!m_dependencies.canFind(p)) {
+				logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString());
+				m_dependencies ~= p;
+				if (basename.toString() == m_rootPackage.basePackage.name)
+					p.warnOnSpecialCompilerFlags();
+				collectDependenciesRec(p, depth+1);
+			}
+
+			m_dependees[p] ~= pack;
+			//enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString());
+		}
+	}
+
+	/// Convenience function used by `reinit`
+	private Package resolveSubPackage(Package p, string subname, bool silentFail) {
+		if (!subname.length || p is null)
+			return p;
+		return m_packageManager.getSubPackage(p, subname, silentFail);
+	}
+
 	/// Returns the name of the root package.
 	@property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; }
 
@@ -620,7 +652,7 @@
 		parents[m_rootPackage.name] = null;
 		foreach (p; getTopologicalPackageList())
 			foreach (d; p.getAllDependencies())
-				parents[d.name] ~= p.name;
+				parents[d.name.toString()] ~= p.name;
 
 		size_t createConfig(string pack, string config) {
 			foreach (i, v; configs)
@@ -694,7 +726,7 @@
 		{
 			string[][string] depconfigs;
 			foreach (d; p.getAllDependencies()) {
-				auto dp = getDependency(d.name, true);
+				auto dp = getDependency(d.name.toString(), true);
 				if (!dp) continue;
 
 				string[] cfgs;
@@ -704,7 +736,7 @@
 					if (!subconf.empty) cfgs = [subconf];
 					else cfgs = dp.getPlatformConfigurations(platform);
 				}
-				cfgs = cfgs.filter!(c => haveConfig(d.name, c)).array;
+				cfgs = cfgs.filter!(c => haveConfig(d.name.toString(), c)).array;
 
 				// if no valid configuration was found for a dependency, don't include the
 				// current configuration
@@ -712,14 +744,14 @@
 					logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name);
 					return;
 				}
-				depconfigs[d.name] = cfgs;
+				depconfigs[d.name.toString()] = cfgs;
 			}
 
 			// add this configuration to the graph
 			size_t cidx = createConfig(p.name, c);
 			foreach (d; p.getAllDependencies())
-				foreach (sc; depconfigs.get(d.name, null))
-					createEdge(cidx, createConfig(d.name, sc));
+				foreach (sc; depconfigs.get(d.name.toString(), null))
+					createEdge(cidx, createConfig(d.name.toString(), sc));
 		}
 
 		// create a graph of all possible package configurations (package, config) -> (sub-package, sub-config)
@@ -732,7 +764,7 @@
 
 			// first, add all dependency configurations
 			foreach (d; p.getAllDependencies) {
-				auto dp = getDependency(d.name, true);
+				auto dp = getDependency(d.name.toString(), true);
 				if (!dp) continue;
 				determineAllConfigs(dp);
 			}
@@ -1305,12 +1337,12 @@
 	void saveSelections()
 	{
 		assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections).");
-		if (m_selections.hasSelectedVersion(m_rootPackage.basePackage.name))
-			m_selections.deselectVersion(m_rootPackage.basePackage.name);
-
-		auto path = m_rootPackage.path ~ SelectedVersions.defaultFile;
-		if (m_selections.dirty || !existsFile(path))
-			m_selections.save(path);
+		const name = PackageName(m_rootPackage.basePackage.name);
+		if (m_selections.hasSelectedVersion(name))
+			m_selections.deselectVersion(name);
+		this.m_packageManager.writeSelections(
+			this.m_rootPackage, this.m_selections.m_selections,
+			this.m_selections.dirty);
 	}
 
 	deprecated bool isUpgradeCacheUpToDate()
@@ -1735,15 +1767,20 @@
 	environment.remove("MY_ENV_VAR");
 }
 
-/** Holds and stores a set of version selections for package dependencies.
-
-	This is the runtime representation of the information contained in
-	"dub.selections.json" within a package's directory.
-*/
+/**
+ * Holds and stores a set of version selections for package dependencies.
+ *
+ * This is the runtime representation of the information contained in
+ * "dub.selections.json" within a package's directory.
+ *
+ * Note that as subpackages share the same version as their main package,
+ * this class will treat any subpackage reference as a reference to its
+ * main package.
+ */
 public class SelectedVersions {
 	protected {
 		enum FileVersion = 1;
-		Selected m_selections;
+		Selections!1 m_selections;
 		bool m_dirty = false; // has changes since last save
 		bool m_bare = true;
 	}
@@ -1752,13 +1789,14 @@
 	enum defaultFile = "dub.selections.json";
 
 	/// Constructs a new empty version selection.
-	public this(uint version_ = FileVersion) @safe pure nothrow @nogc
+	public this(uint version_ = FileVersion) @safe pure
 	{
-		this.m_selections = Selected(version_);
+		enforce(version_ == 1, "Unsupported file version");
+		this.m_selections = Selections!1(version_);
 	}
 
 	/// Constructs a new non-empty version selection.
-	public this(Selected data) @safe pure nothrow @nogc
+	public this(Selections!1 data) @safe pure nothrow @nogc
 	{
 		this.m_selections = data;
 		this.m_bare = false;
@@ -1812,36 +1850,58 @@
 	}
 
 	/// Selects a certain version for a specific package.
+	deprecated("Use the overload that accepts a `PackageName`")
 	void selectVersion(string package_id, Version version_)
 	{
-		if (auto pdep = package_id in m_selections.versions) {
-			if (*pdep == Dependency(version_))
-				return;
-		}
-		m_selections.versions[package_id] = Dependency(version_);
-		m_dirty = true;
+		const name = PackageName(package_id);
+		return this.selectVersion(name, version_);
+	}
+
+	/// Ditto
+	void selectVersion(in PackageName name, Version version_)
+	{
+		const dep = Dependency(version_);
+		this.selectVersionInternal(name, dep);
 	}
 
 	/// Selects a certain path for a specific package.
+	deprecated("Use the overload that accepts a `PackageName`")
 	void selectVersion(string package_id, NativePath path)
 	{
-		if (auto pdep = package_id in m_selections.versions) {
-			if (*pdep == Dependency(path))
-				return;
-		}
-		m_selections.versions[package_id] = Dependency(path);
-		m_dirty = true;
+		const name = PackageName(package_id);
+		return this.selectVersion(name, path);
+	}
+
+	/// Ditto
+	void selectVersion(in PackageName name, NativePath path)
+	{
+		const dep = Dependency(path);
+		this.selectVersionInternal(name, dep);
 	}
 
 	/// Selects a certain Git reference for a specific package.
+	deprecated("Use the overload that accepts a `PackageName`")
 	void selectVersion(string package_id, Repository repository)
 	{
-		const dependency = Dependency(repository);
-		if (auto pdep = package_id in m_selections.versions) {
-			if (*pdep == dependency)
+		const name = PackageName(package_id);
+		return this.selectVersion(name, repository);
+	}
+
+	/// Ditto
+	void selectVersion(in PackageName name, Repository repository)
+	{
+		const dep = Dependency(repository);
+		this.selectVersionInternal(name, dep);
+	}
+
+	/// Internal implementation of selectVersion
+	private void selectVersionInternal(in PackageName name, in Dependency dep)
+	{
+		if (auto pdep = name.main.toString() in m_selections.versions) {
+			if (*pdep == dep)
 				return;
 		}
-		m_selections.versions[package_id] = dependency;
+		m_selections.versions[name.main.toString()] = dep;
 		m_dirty = true;
 	}
 
@@ -1852,16 +1912,31 @@
 	}
 
 	/// Removes the selection for a particular package.
+	deprecated("Use the overload that accepts a `PackageName`")
 	void deselectVersion(string package_id)
 	{
-		m_selections.versions.remove(package_id);
+		const n = PackageName(package_id);
+		this.deselectVersion(n);
+	}
+
+	/// Ditto
+	void deselectVersion(in PackageName name)
+	{
+		m_selections.versions.remove(name.main.toString());
 		m_dirty = true;
 	}
 
 	/// Determines if a particular package has a selection set.
-	bool hasSelectedVersion(string packageId)
-	const {
-		return (packageId in m_selections.versions) !is null;
+	deprecated("Use the overload that accepts a `PackageName`")
+	bool hasSelectedVersion(string packageId) const {
+		const name = PackageName(packageId);
+		return this.hasSelectedVersion(name);
+	}
+
+	/// Ditto
+	bool hasSelectedVersion(in PackageName name) const
+	{
+		return (name.main.toString() in m_selections.versions) !is null;
 	}
 
 	/** Returns the selection for a particular package.
@@ -1871,10 +1946,18 @@
 		is a path based selection, or its `Dependency.version_` property is
 		valid and it is a version selection.
 	*/
-	Dependency getSelectedVersion(string packageId)
-	const {
-		enforce(hasSelectedVersion(packageId));
-		return m_selections.versions[packageId];
+	deprecated("Use the overload that accepts a `PackageName`")
+	Dependency getSelectedVersion(string packageId) const
+	{
+		const name = PackageName(packageId);
+		return this.getSelectedVersion(name);
+	}
+
+	/// Ditto
+	Dependency getSelectedVersion(in PackageName name) const
+	{
+		enforce(hasSelectedVersion(name));
+		return m_selections.versions[name.main.toString()];
 	}
 
 	/** Stores the selections to disk.
@@ -1883,30 +1966,10 @@
 		should be used as the file name and the directory should be the root
 		directory of the project's root package.
 	*/
+	deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`")
 	void save(NativePath path)
 	{
-		Json json = serialize();
-		auto result = appender!string();
-
-		assert(json.type == Json.Type.object);
-		assert(json.length == 2);
-		assert(json["versions"].type != Json.Type.undefined);
-
-		result.put("{\n\t\"fileVersion\": ");
-		result.writeJsonString(json["fileVersion"]);
-		result.put(",\n\t\"versions\": {");
-		auto vers = json["versions"].get!(Json[string]);
-		bool first = true;
-		foreach (k; vers.byKey.array.sort()) {
-			if (!first) result.put(",");
-			else first = false;
-			result.put("\n\t\t");
-			result.writeJsonString(Json(k));
-			result.put(": ");
-			result.writeJsonString(vers[k]);
-		}
-		result.put("\n\t}\n}\n");
-		path.writeFile(result.data);
+		path.writeFile(PackageManager.selectionsToString(this.m_selections));
 		m_dirty = false;
 		m_bare = false;
 	}
@@ -1930,15 +1993,9 @@
 		else throw new Exception(format("Unexpected type for dependency: %s", j));
 	}
 
-	Json serialize()
-	const {
-		Json json = serializeToJson(m_selections);
-		Json serialized = Json.emptyObject;
-		serialized["fileVersion"] = m_selections.fileVersion;
-		serialized["versions"] = Json.emptyObject;
-		foreach (p, dep; m_selections.versions)
-			serialized["versions"][p] = dep.toJson(true);
-		return serialized;
+	deprecated("JSON serialization is deprecated")
+	Json serialize() const {
+		return PackageManager.selectionsToJSON(this.m_selections);
 	}
 
 	deprecated("JSON deserialization is deprecated")
@@ -1956,6 +2013,7 @@
 
 /// The template code from which the test runner is generated
 private immutable TestRunnerTemplate = q{
+deprecated // allow silently using deprecated symbols
 module dub_test_root;
 
 import std.typetuple;
diff --git a/source/dub/recipe/io.d b/source/dub/recipe/io.d
index 5fedf46..f12fdb5 100644
--- a/source/dub/recipe/io.d
+++ b/source/dub/recipe/io.d
@@ -7,6 +7,7 @@
 */
 module dub.recipe.io;
 
+import dub.dependency : PackageName;
 import dub.recipe.packagerecipe;
 import dub.internal.logging;
 import dub.internal.vibecompat.core.file;
@@ -19,26 +20,34 @@
 
 	Params:
 		filename = NativePath of the package recipe file
-		parent_name = Optional name of the parent package (if this is a sub package)
+		parent = Optional name of the parent package (if this is a sub package)
 		mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
 
 	Returns: Returns the package recipe contents
 	Throws: Throws an exception if an I/O or syntax error occurs
 */
+deprecated("Use the overload that accepts a `NativePath` as first argument")
 PackageRecipe readPackageRecipe(
-	string filename, string parent_name = null, StrictMode mode = StrictMode.Ignore)
+	string filename, string parent = null, StrictMode mode = StrictMode.Ignore)
 {
-	return readPackageRecipe(NativePath(filename), parent_name, mode);
+	return readPackageRecipe(NativePath(filename), parent, mode);
 }
 
 /// ditto
+deprecated("Use the overload that accepts a `PackageName` as second argument")
 PackageRecipe readPackageRecipe(
-	NativePath filename, string parent_name = null, StrictMode mode = StrictMode.Ignore)
+	NativePath filename, string parent, StrictMode mode = StrictMode.Ignore)
 {
-	import dub.internal.utils : stripUTF8Bom;
+	return readPackageRecipe(filename, parent.length ? PackageName(parent) : PackageName.init, mode);
+}
 
-	string text = stripUTF8Bom(cast(string)readFile(filename));
-	return parsePackageRecipe(text, filename.toNativeString(), parent_name, null, mode);
+
+/// ditto
+PackageRecipe readPackageRecipe(NativePath filename,
+	in PackageName parent = PackageName.init, StrictMode mode = StrictMode.Ignore)
+{
+	string text = readText(filename);
+	return parsePackageRecipe(text, filename.toNativeString(), parent, null, mode);
 }
 
 /** Parses an in-memory package recipe.
@@ -49,7 +58,7 @@
 		contents = The contents of the recipe file
 		filename = Name associated with the package recipe - this is only used
 			to determine the file format from the file extension
-		parent_name = Optional name of the parent package (if this is a sub
+		parent = Optional name of the parent package (if this is a sub
 		package)
 		default_package_name = Optional default package name (if no package name
 		is found in the recipe this value will be used)
@@ -58,7 +67,18 @@
 	Returns: Returns the package recipe contents
 	Throws: Throws an exception if an I/O or syntax error occurs
 */
-PackageRecipe parsePackageRecipe(string contents, string filename, string parent_name = null,
+deprecated("Use the overload that accepts a `PackageName` as 3rd argument")
+PackageRecipe parsePackageRecipe(string contents, string filename, string parent,
+	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
+{
+    return parsePackageRecipe(contents, filename, parent.length ?
+        PackageName(parent) : PackageName.init,
+        default_package_name, mode);
+}
+
+/// Ditto
+PackageRecipe parsePackageRecipe(string contents, string filename,
+    in PackageName parent = PackageName.init,
 	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
 {
 	import std.algorithm : endsWith;
@@ -82,7 +102,7 @@
 			logWarn("Error was: %s", exc);
 			// Fallback to JSON parser
 			ret = PackageRecipe.init;
-			parseJson(ret, parseJsonString(contents, filename), parent_name);
+			parseJson(ret, parseJsonString(contents, filename), parent);
 		} catch (Exception exc) {
 			logWarn("Your `dub.json` file use non-conventional features that are deprecated");
 			logWarn("This is most likely due to duplicated keys.");
@@ -90,7 +110,7 @@
 			logWarn("Error was: %s", exc);
 			// Fallback to JSON parser
 			ret = PackageRecipe.init;
-			parseJson(ret, parseJsonString(contents, filename), parent_name);
+			parseJson(ret, parseJsonString(contents, filename), parent);
 		}
 		// `debug = ConfigFillerDebug` also enables verbose parser output
 		debug (ConfigFillerDebug)
@@ -111,7 +131,7 @@
 			}
 		}
 	}
-	else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent_name, filename);
+	else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent, filename);
 	else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename);
 
 	// Fix for issue #711: `targetType` should be inherited, or default to library
diff --git a/source/dub/recipe/json.d b/source/dub/recipe/json.d
index 505baff..bb48990 100644
--- a/source/dub/recipe/json.d
+++ b/source/dub/recipe/json.d
@@ -20,8 +20,14 @@
 import std.string : format, indexOf;
 import std.traits : EnumMembers;
 
+deprecated("Use the overload that takes a `PackageName` as 3rd argument")
+void parseJson(ref PackageRecipe recipe, Json json, string parent)
+{
+    const PackageName pname = parent ? PackageName(parent) : PackageName.init;
+	parseJson(recipe, json, pname);
+}
 
-void parseJson(ref PackageRecipe recipe, Json json, string parent_name)
+void parseJson(ref PackageRecipe recipe, Json json, in PackageName parent = PackageName.init)
 {
 	foreach (string field, value; json) {
 		switch (field) {
@@ -37,7 +43,7 @@
 			case "buildTypes":
 				foreach (string name, settings; value) {
 					BuildSettingsTemplate bs;
-					bs.parseJson(settings, null);
+					bs.parseJson(settings, PackageName.init);
 					recipe.buildTypes[name] = bs;
 				}
 				break;
@@ -51,7 +57,9 @@
 
 	enforce(recipe.name.length > 0, "The package \"name\" field is missing or empty.");
 
-	auto fullname = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name;
+	const fullname = parent.toString().length
+		? PackageName(parent.toString() ~ ":" ~ recipe.name)
+		: PackageName(recipe.name);
 
 	// parse build settings
 	recipe.buildSettings.parseJson(json, fullname);
@@ -59,7 +67,7 @@
 	if (auto pv = "configurations" in json) {
 		foreach (settings; *pv) {
 			ConfigurationInfo ci;
-			ci.parseJson(settings, recipe.name);
+			ci.parseJson(settings, fullname);
 			recipe.configurations ~= ci;
 		}
 	}
@@ -110,10 +118,10 @@
 	return ret;
 }
 
-private void parseSubPackages(ref PackageRecipe recipe, string parent_package_name, Json[] subPackagesJson)
+private void parseSubPackages(ref PackageRecipe recipe, in PackageName parent, Json[] subPackagesJson)
 {
-	enforce(!parent_package_name.canFind(":"), format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.",
-		parent_package_name, getBasePackageName(parent_package_name)));
+	enforce(!parent.sub, format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.",
+		parent, parent.main));
 
 	recipe.subPackages = new SubPackage[subPackagesJson.length];
 	foreach (i, subPackageJson; subPackagesJson) {
@@ -123,13 +131,13 @@
 			recipe.subPackages[i] = SubPackage(subpath, PackageRecipe.init);
 		} else {
 			PackageRecipe subinfo;
-			subinfo.parseJson(subPackageJson, parent_package_name);
+			subinfo.parseJson(subPackageJson, parent);
 			recipe.subPackages[i] = SubPackage(null, subinfo);
 		}
 	}
 }
 
-private void parseJson(ref ConfigurationInfo config, Json json, string package_name)
+private void parseJson(ref ConfigurationInfo config, Json json, in PackageName pname)
 {
 	foreach (string name, value; json) {
 		switch (name) {
@@ -143,7 +151,7 @@
 	}
 
 	enforce(!config.name.empty, "Configuration is missing a name.");
-	config.buildSettings.parseJson(json, package_name);
+	config.buildSettings.parseJson(json, pname);
 }
 
 private Json toJson(const scope ref ConfigurationInfo config)
@@ -154,7 +162,7 @@
 	return ret;
 }
 
-private void parseJson(ref BuildSettingsTemplate bs, Json json, string package_name)
+private void parseJson(ref BuildSettingsTemplate bs, Json json, in PackageName pname)
 {
 	foreach(string name, value; json)
 	{
@@ -167,13 +175,15 @@
 			case "dependencies":
 				foreach (string pkg, verspec; value) {
 					if (pkg.startsWith(":")) {
-						enforce(!package_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", package_name, pkg));
-						pkg = package_name ~ pkg;
+						enforce(!pname.sub.length,
+							"Short-hand packages syntax not allowed within " ~
+							"sub packages: %s -> %s".format(pname, pkg));
+						pkg = pname.toString() ~ pkg;
 					}
 					enforce(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once." );
 					bs.dependencies[pkg] = Dependency.fromJson(verspec);
 					if (verspec.type == Json.Type.object)
-						bs.dependencies[pkg].settings.parseJson(verspec, package_name);
+						bs.dependencies[pkg].settings.parseJson(verspec, pname);
 				}
 				break;
 			case "systemDependencies":
@@ -376,9 +386,10 @@
 	`.strip.outdent;
 	auto jsonValue = parseJsonString(json);
 	PackageRecipe rec1;
-	parseJson(rec1, jsonValue, null);
+	parseJson(rec1, jsonValue);
 	PackageRecipe rec;
-	parseJson(rec, rec1.toJson(), null); // verify that all fields are serialized properly
+	// verify that all fields are serialized properly
+	parseJson(rec, rec1.toJson());
 
 	assert(rec.name == "projectname");
 	assert(rec.buildSettings.environments == ["": ["Var1": "env"]]);
diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d
index 5573018..44f34b0 100644
--- a/source/dub/recipe/packagerecipe.d
+++ b/source/dub/recipe/packagerecipe.d
@@ -32,17 +32,25 @@
 	example, "packa:packb:packc" references a package named "packc" that is a
 	sub package of "packb", which in turn is a sub package of "packa".
 */
+deprecated("This function is not supported as subpackages cannot be nested")
 string[] getSubPackagePath(string package_name) @safe pure
 {
 	return package_name.split(":");
 }
 
+deprecated @safe unittest
+{
+	assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]);
+	assert(getSubPackagePath("pack") == ["pack"]);
+}
+
 /**
 	Returns the name of the top level package for a given (sub) package name of
 	format `"basePackageName"` or `"basePackageName:subPackageName"`.
 
 	In case of a top level package, the qualified name is returned unmodified.
 */
+deprecated("Use `dub.dependency : PackageName(arg).main` instead")
 string getBasePackageName(string package_name) @safe pure
 {
 	return package_name.findSplit(":")[0];
@@ -55,15 +63,14 @@
 	This is the part of the package name excluding the base package
 	name. See also $(D getBasePackageName).
 */
+deprecated("Use `dub.dependency : PackageName(arg).sub` instead")
 string getSubPackageName(string package_name) @safe pure
 {
 	return package_name.findSplit(":")[2];
 }
 
-@safe unittest
+deprecated @safe unittest
 {
-	assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]);
-	assert(getSubPackagePath("pack") == ["pack"]);
 	assert(getBasePackageName("packa:packb:packc") == "packa");
 	assert(getBasePackageName("pack") == "pack");
 	assert(getSubPackageName("packa:packb:packc") == "packb:packc");
diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d
index 5d4d909..e6872c2 100644
--- a/source/dub/recipe/sdl.d
+++ b/source/dub/recipe/sdl.d
@@ -20,13 +20,26 @@
 import std.conv;
 import std.string : startsWith, format;
 
+deprecated("Use `parseSDL(PackageRecipe, string, PackageName, string)` instead")
 void parseSDL(ref PackageRecipe recipe, string sdl, string parent_name, string filename)
 {
-	parseSDL(recipe, parseSource(sdl, filename), parent_name);
+	parseSDL(recipe, parseSource(sdl, filename), PackageName(parent_name));
 }
 
+deprecated("Use `parseSDL(PackageRecipe, Tag, PackageName)` instead")
 void parseSDL(ref PackageRecipe recipe, Tag sdl, string parent_name)
 {
+	parseSDL(recipe, sdl, PackageName(parent_name));
+}
+
+void parseSDL(ref PackageRecipe recipe, string sdl, in PackageName parent,
+	string filename)
+{
+	parseSDL(recipe, parseSource(sdl, filename), parent);
+}
+
+void parseSDL(ref PackageRecipe recipe, Tag sdl, in PackageName parent = PackageName.init)
+{
 	Tag[] subpacks;
 	Tag[] configs;
 
@@ -47,7 +60,7 @@
 			case "buildType":
 				auto name = n.stringTagValue(true);
 				BuildSettingsTemplate bt;
-				parseBuildSettings(n, bt, parent_name);
+				parseBuildSettings(n, bt, parent);
 				recipe.buildTypes[name] = bt;
 				break;
 			case "toolchainRequirements":
@@ -59,7 +72,9 @@
 	}
 
 	enforceSDL(recipe.name.length > 0, "The package \"name\" field is missing or empty.", sdl);
-	string full_name = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name;
+	const full_name = parent.toString().length
+		? PackageName(parent.toString() ~ ":" ~ recipe.name)
+		: PackageName(recipe.name);
 
 	// parse general build settings
 	parseBuildSettings(sdl, recipe.buildSettings, full_name);
@@ -119,17 +134,19 @@
 	return ret;
 }
 
-private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs, string package_name)
+private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs,
+	in PackageName name)
 {
 	foreach (setting; settings.all.tags)
-		parseBuildSetting(setting, bs, package_name);
+		parseBuildSetting(setting, bs, name);
 }
 
-private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs, string package_name)
+private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs,
+	in PackageName name)
 {
 	switch (setting.fullName) {
 		default: break;
-		case "dependency": parseDependency(setting, bs, package_name); break;
+		case "dependency": parseDependency(setting, bs, name); break;
 		case "systemDependencies": bs.systemDependencies = setting.stringTagValue; break;
 		case "targetType": bs.targetType = setting.stringTagValue.to!TargetType; break;
 		case "targetName": bs.targetName = setting.stringTagValue; break;
@@ -138,7 +155,7 @@
 		case "subConfiguration":
 			auto args = setting.stringArrayTagValue;
 			enforceSDL(args.length == 2, "Expecting package and configuration names as arguments.", setting);
-			bs.subConfigurations[expandPackageName(args[0], package_name, setting)] = args[1];
+			bs.subConfigurations[expandPackageName(args[0], name, setting)] = args[1];
 			break;
 		case "dflags": setting.parsePlatformStringArray(bs.dflags); break;
 		case "lflags": setting.parsePlatformStringArray(bs.lflags); break;
@@ -178,14 +195,14 @@
 	}
 }
 
-private void parseDependency(Tag t, ref BuildSettingsTemplate bs, string package_name)
+private void parseDependency(Tag t, ref BuildSettingsTemplate bs, in PackageName name)
 {
 	enforceSDL(t.values.length != 0, "Missing dependency name.", t);
 	enforceSDL(t.values.length == 1, "Multiple dependency names.", t);
-	auto pkg = expandPackageName(t.values[0].expect!string(t), package_name, t);
+	auto pkg = expandPackageName(t.values[0].expect!string(t), name, t);
 	enforceSDL(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once.", t);
 
-	Dependency dep = Dependency.any;
+	Dependency dep = Dependency.Any;
 	auto attrs = t.attributes;
 
 	if ("path" in attrs) {
@@ -209,15 +226,15 @@
 	bs.dependencies[pkg] = dep;
 
 	BuildSettingsTemplate dbs;
-	parseBuildSettings(t, bs.dependencies[pkg].settings, package_name);
+	parseBuildSettings(t, bs.dependencies[pkg].settings, name);
 }
 
-private void parseConfiguration(Tag t, ref ConfigurationInfo ret, string package_name)
+private void parseConfiguration(Tag t, ref ConfigurationInfo ret, in PackageName name)
 {
 	ret.name = t.stringTagValue(true);
 	foreach (f; t.tags) {
 		switch (f.fullName) {
-			default: parseBuildSetting(f, ret.buildSettings, package_name); break;
+			default: parseBuildSetting(f, ret.buildSettings, name); break;
 			case "platforms": ret.platforms ~= f.stringArrayTagValue; break;
 		}
 	}
@@ -335,13 +352,14 @@
 	return new Tag(null, "toolchainRequirements", null, attrs);
 }
 
-private string expandPackageName(string name, string parent_name, Tag tag)
+private string expandPackageName(string name, in PackageName parent, Tag tag)
 {
 	import std.algorithm : canFind;
-	if (name.startsWith(":")) {
-		enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag);
-		return parent_name ~ name;
-	} else return name;
+	if (!name.startsWith(":"))
+		return name;
+	enforceSDL(!parent.sub.length, "Short-hand packages syntax not " ~
+		"allowed within sub packages: %s -> %s".format(parent, name), tag);
+	return parent.toString() ~ name;
 }
 
 private string stringTagValue(Tag t, bool allow_child_tags = false)
@@ -424,6 +442,11 @@
 	}
 }
 
+// Just a wrapper around `parseSDL` for easier testing
+version (unittest) private void parseSDLTest(ref PackageRecipe recipe, string sdl)
+{
+	parseSDL(recipe, parseSource(sdl, "testfile"), PackageName.init);
+}
 
 unittest { // test all possible fields
 	auto sdl =
@@ -533,9 +556,9 @@
 lflags "lf3"
 `;
 	PackageRecipe rec1;
-	parseSDL(rec1, sdl, null, "testfile");
+	parseSDLTest(rec1, sdl);
 	PackageRecipe rec;
-	parseSDL(rec, rec1.toSDL(), null); // verify that all fields are serialized properly
+	parseSDL(rec, rec1.toSDL()); // verify that all fields are serialized properly
 
 	assert(rec.name == "projectname");
 	assert(rec.description == "project description");
@@ -630,7 +653,7 @@
 dflags "-j" platform="linux"
 `;
 	PackageRecipe rec;
-	parseSDL(rec, sdl, null, "testfile");
+	parseSDLTest(rec, sdl);
 	assert(rec.buildSettings.dflags.length == 3);
 	assert(rec.buildSettings.dflags["windows-x86"] == ["-a", "-b", "-c"]);
 	assert(rec.buildSettings.dflags[""] == ["-e", "-f", "-g"]);
@@ -641,23 +664,22 @@
 	import std.exception;
 	auto sdl = `description "missing name"`;
 	PackageRecipe rec;
-	assertThrown(parseSDL(rec, sdl, null, "testfile"));
+	assertThrown(parseSDLTest(rec, sdl));
 }
 
 unittest { // test single value fields
 	import std.exception;
 	PackageRecipe rec;
-	assertThrown!Exception(parseSDL(rec, `name "hello" "world"`, null, "testfile"));
-	assertThrown!Exception(parseSDL(rec, `name`, null, "testfile"));
-	assertThrown!Exception(parseSDL(rec, `name 10`, null, "testfile"));
-	assertThrown!Exception(parseSDL(rec,
+	assertThrown!Exception(parseSDLTest(rec, `name "hello" "world"`));
+	assertThrown!Exception(parseSDLTest(rec, `name`));
+	assertThrown!Exception(parseSDLTest(rec, `name 10`));
+	assertThrown!Exception(parseSDLTest(rec,
 		`name "hello" {
 			world
-		}`, null, "testfile"));
-	assertThrown!Exception(parseSDL(rec,
+		}`));
+	assertThrown!Exception(parseSDLTest(rec,
 		`name ""
-		versions "hello" 10`
-		, null, "testfile"));
+		versions "hello" 10`));
 }
 
 unittest { // test basic serialization
@@ -678,14 +700,14 @@
 unittest {
 	auto sdl = "name \"test\"\nsourcePaths";
 	PackageRecipe rec;
-	parseSDL(rec, sdl, null, "testfile");
+	parseSDLTest(rec, sdl);
 	assert("" in rec.buildSettings.sourcePaths);
 }
 
 unittest {
 	auto sdl = "name \"test\"\ncSourcePaths";
 	PackageRecipe rec;
-	parseSDL(rec, sdl, null, "testfile");
+	parseSDLTest(rec, sdl);
 	assert("" in rec.buildSettings.cSourcePaths);
 }
 
@@ -695,7 +717,7 @@
 dependency "package" repository="git+https://some.url" version="12345678"
 `;
 	PackageRecipe rec;
-	parseSDL(rec, sdl, null, "testfile");
+	parseSDLTest(rec, sdl);
 	auto dependency = rec.buildSettings.dependencies["package"];
 	assert(!dependency.repository.empty);
 	assert(dependency.repository.ref_ == "12345678");
diff --git a/source/dub/recipe/selection.d b/source/dub/recipe/selection.d
index 1e127e3..11758e8 100644
--- a/source/dub/recipe/selection.d
+++ b/source/dub/recipe/selection.d
@@ -1,5 +1,10 @@
 /**
- * Contains type definition for `dub.selections.json`
+ * Contains type definition for the selections file
+ *
+ * The selections file, commonly known by its file name
+ * `dub.selections.json`, is used by Dub to store resolved
+ * dependencies. Its purpose is identical to other package
+ * managers' lock file.
  */
 module dub.recipe.selection;
 
@@ -7,16 +12,99 @@
 import dub.internal.vibecompat.inet.path : NativePath;
 
 import dub.internal.configy.Attributes;
+import dub.internal.dyaml.stdsumtype;
 
 import std.exception;
 
-public struct Selected
-{
-    /// The current version of the file format
-    public uint fileVersion;
+deprecated("Use either `Selections!1` or `SelectionsFile` instead")
+public alias Selected = Selections!1;
 
-    /// The selected package and their matching versions
-    public SelectedDependency[string] versions;
+/**
+ * Top level type for `dub.selections.json`
+ *
+ * To support multiple version, we expose a `SumType` which
+ * contains the "real" version being parsed.
+ */
+public struct SelectionsFile
+{
+    /// Private alias to avoid repetition
+    private alias DataType = SumType!(Selections!0, Selections!1);
+
+    /**
+     * Get the `fileVersion` of this selection file
+     *
+     * The `fileVersion` is always present, no matter the version.
+     * This is a convenience function that matches any version and allows
+     * one to retrieve it.
+     *
+     * Note that the `fileVersion` can be an unsupported version.
+     */
+    public uint fileVersion () const @safe pure nothrow @nogc
+    {
+        return this.content.match!((s) => s.fileVersion);
+    }
+
+    /**
+     * The content of this selections file
+     *
+     * The underlying content can be accessed using
+     * `dub.internal.yaml.stdsumtype : match`, for example:
+     * ---
+     * SelectionsFile file = readSelectionsFile();
+     * file.content.match!(
+     *     (Selections!0 s) => logWarn("Unsupported version: %s", s.fileVersion),
+     *     (Selections!1 s) => logWarn("Old version (1), please upgrade!"),
+     *     (Selections!2 s) => logInfo("You are up to date"),
+     * );
+     * ---
+     */
+    public DataType content;
+
+    /**
+     * Deserialize the selections file according to its version
+     *
+     * This will first deserialize the `fileVersion` only, and then
+     * the expected version if it is supported. Unsupported versions
+     * will be returned inside a `Selections!0` struct,
+     * which only contains a `fileVersion`.
+     */
+    public static SelectionsFile fromYAML (scope ConfigParser!SelectionsFile parser)
+    {
+        import dub.internal.configy.Read;
+
+        static struct OnlyVersion { uint fileVersion; }
+
+        auto vers = parseConfig!OnlyVersion(
+            CLIArgs.init, parser.node, StrictMode.Ignore);
+
+        switch (vers.fileVersion) {
+        case 1:
+            return SelectionsFile(DataType(parser.parseAs!(Selections!1)));
+        default:
+            return SelectionsFile(DataType(Selections!0(vers.fileVersion)));
+        }
+    }
+}
+
+/**
+ * A specific version of the selections file
+ *
+ * Currently, only two instantiations of this struct are possible:
+ * - `Selections!0` is an invalid/unsupported version;
+ * - `Selections!1` is the most widespread version;
+ */
+public struct Selections (ushort Version)
+{
+    ///
+    public uint fileVersion = Version;
+
+    static if (Version == 0) { /* Invalid version */ }
+    else static if (Version == 1) {
+        /// The selected package and their matching versions
+        public SelectedDependency[string] versions;
+    }
+    else
+        static assert(false, "This version is not supported");
 }
 
 
@@ -97,8 +185,12 @@
     }
 }`;
 
-    auto s = parseConfigString!Selected(content, "/dev/null");
-    assert(s.fileVersion == 1);
+    auto file = parseConfigString!SelectionsFile(content, "/dev/null");
+    assert(file.fileVersion == 1);
+    auto s = file.content.match!(
+        (Selections!1 s) => s,
+        (s) { assert(0); return Selections!(1).init; },
+    );
     assert(s.versions.length == 5);
     assert(s.versions["simple"]     == Dependency(Version("1.5.6")));
     assert(s.versions["branch"]     == Dependency(Version("~master")));
@@ -106,3 +198,13 @@
     assert(s.versions["path"]       == Dependency(NativePath("../some/where")));
     assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456")));
 }
+
+// Test reading an unsupported version
+unittest
+{
+    import dub.internal.configy.Read : parseConfigString;
+
+    immutable string content = `{"fileVersion": 9999, "thisis": "notrecognized"}`;
+    auto s = parseConfigString!SelectionsFile(content, "/dev/null");
+    assert(s.fileVersion == 9999);
+}
diff --git a/source/dub/test/base.d b/source/dub/test/base.d
index e5cd8e4..51803a6 100644
--- a/source/dub/test/base.d
+++ b/source/dub/test/base.d
@@ -51,24 +51,25 @@
 
 import std.array;
 public import std.algorithm;
+import std.exception;
+import std.format;
+import std.string;
 
 import dub.data.settings;
 public import dub.dependency;
 public import dub.dub;
 public import dub.package_;
+import dub.internal.vibecompat.core.file : FileInfo;
+public import dub.internal.vibecompat.inet.path;
 import dub.packagemanager;
 import dub.packagesuppliers.packagesupplier;
 import dub.project;
+import dub.recipe.io : parsePackageRecipe;
+import dub.recipe.selection;
 
 /// Example of a simple unittest for a project with a single dependency
 unittest
 {
-    // `a` will be loaded as the project while `b` will be loaded
-    // as a simple package. The recipe files can be in JSON or SDL format,
-    // here we use both to demonstrate this.
-    const a = `{ "name": "a", "dependencies": { "b": "~>1.0" } }`;
-    const b = `name "b"`;
-
     // Enabling this would provide some more verbose output, which makes
     // debugging a failing unittest much easier.
     version (none) {
@@ -76,24 +77,35 @@
         scope(exit) disableLogging();
     }
 
-    scope dub = new TestDub();
-    // Let the `PackageManager` know about the `b` package
-    dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl);
-    // And about our main package
-    auto mainPackage = dub.addTestPackage(a, Version("1.0.0"));
+    // Initialization is best done as a delegate passed to `TestDub` constructor,
+    // which receives an `FSEntry` representing the root of the filesystem.
+    // Various low-level functions are exposed (mkdir, writeFile, ...),
+    // as well as higher-level functions (`writePackageFile`).
+    scope dub = new TestDub((scope FSEntry root) {
+            // `a` will be loaded as the project while `b` will be loaded
+            // as a simple package. The recipe files can be in JSON or SDL format,
+            // here we use both to demonstrate this.
+            root.writeFile(TestDub.ProjectPath ~ "dub.json",
+                `{ "name": "a", "dependencies": { "b": "~>1.0" } }`);
+            root.writeFile(TestDub.ProjectPath ~ "dub.selections.json",
+                           `{"fileVersion": 1, "versions": {"b": "1.1.0"}}`);
+            // Note that you currently need to add the `version` to the package
+            root.writePackageFile("b", "1.0.0", `name "b"
+version "1.0.0"`, PackageFormat.sdl);
+            root.writePackageFile("b", "1.1.0", `name "b"
+version "1.1.0"`, PackageFormat.sdl);
+            root.writePackageFile("b", "1.2.0", `name "b"
+version "1.2.0"`, PackageFormat.sdl);
+    });
+
     // `Dub.loadPackage` will set this package as the project
     // While not required, it follows the common Dub use case.
-    dub.loadPackage(mainPackage);
-    // This triggers the dependency resolution process that happens
-    // when one does not have a selection file in the project.
-    // Dub will resolve dependencies and generate the selection file
-    // (in memory). If your test has set dependencies / no dependencies,
-    // this will not be needed.
-    dub.upgrade(UpgradeOptions.select);
+    dub.loadPackage();
 
     // Simple tests can be performed using the public API
     assert(dub.project.hasAllDependencies(), "project has missing dependencies");
     assert(dub.project.getDependency("b", true), "Missing 'b' dependency");
+    assert(dub.project.getDependency("b", true).version_ == Version("1.1.0"));
     // While it is important to make your tests fail before you make them pass,
     // as is common with TDD, it can also be useful to test simple assumptions
     // as part of your basic tests. Here we want to make sure `getDependency`
@@ -102,6 +114,18 @@
     // and tests are run serially in a module, so one may rely on previous tests
     // having passed to avoid repeating some assumptions.
     assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency");
+
+    // This triggers the dependency resolution process that happens
+    // when one does not have a selection file in the project.
+    // Dub will resolve dependencies and generate the selection file
+    // (in memory). If your test has set dependencies / no dependencies,
+    // this will not be needed.
+    dub.upgrade(UpgradeOptions.select);
+    assert(dub.project.getDependency("b", true).version_ == Version("1.1.0"));
+
+    /// Now actually upgrade dependencies in memory
+    dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade);
+    assert(dub.project.getDependency("b", true).version_ == Version("1.2.0"));
 }
 
 // TODO: Remove and handle logging the same way we handle other IO
@@ -123,27 +147,132 @@
  * This instance of dub should not read any environment variables,
  * nor should it do any file IO, to make it usable and reliable in unittests.
  * Currently it reads environment variables but does not read the configuration.
+ *
+ * Note that since the design of Dub was centered on the file system for so long,
+ * `NativePath` is still a core part of how one interacts with this class.
+ * In order to be as close to the production code as possible, this class
+ * use the following conventions:
+ * - The project is located under `/dub/project/`;
+ * - The user and system packages are under `/dub/user/packages/` and
+ *   `/dub/system/packages/`, respectively;
+ * Those paths don't need to exists, but they are what one might see
+ * when writing and debugging unittests.
  */
 public class TestDub : Dub
 {
-    /// Forward to base constructor
-    public this (string root = ".", PackageSupplier[] extras = null,
-                 SkipPackageSuppliers skip = SkipPackageSuppliers.none)
+    /// The virtual filesystem that this instance acts on
+    public FSEntry fs;
+
+    /// Convenience constants for use in unittests
+    version (Windows)
+        public static immutable Root = NativePath("T:\\dub\\");
+    else
+        public static immutable Root = NativePath("/dub/");
+
+    /// Ditto
+    public static immutable ProjectPath = Root ~ "project";
+
+    /// Ditto
+    public static immutable SpecialDirs Paths = {
+        temp: Root ~ "temp/",
+        systemSettings: Root ~ "system/",
+        userSettings: Root ~ "user/",
+        userPackages: Root ~ "user/",
+        cache: Root ~ "user/" ~ "cache/",
+    };
+
+    /***************************************************************************
+
+        Instantiate a new `TestDub` instance with the provided filesystem state
+
+        This exposes the raw virtual filesystem to the user, allowing any kind
+        of customization to happen: Empty directory, non-writeable ones, etc...
+
+        Params:
+          dg = Delegate to be called with the filesystem, before `TestDub`
+               instantiation is performed;
+          root = The root path for this instance (forwarded to Dub)
+          extras = Extras `PackageSupplier`s (forwarded to Dub)
+          skip = What `PackageSupplier`s to skip (forwarded to Dub)
+
+    ***************************************************************************/
+
+    public this (scope void delegate(scope FSEntry root) dg = null,
+        string root = ProjectPath.toNativeString(),
+        PackageSupplier[] extras = null,
+        SkipPackageSuppliers skip = SkipPackageSuppliers.none)
     {
+        /// Create the fs & its base structure
+        auto fs_ = new FSEntry();
+        fs_.mkdir(Paths.temp);
+        fs_.mkdir(Paths.systemSettings);
+        fs_.mkdir(Paths.userSettings);
+        fs_.mkdir(Paths.userPackages);
+        fs_.mkdir(Paths.cache);
+        fs_.mkdir(ProjectPath);
+        if (dg !is null) dg(fs_);
+        this(fs_, root, extras, skip);
+    }
+
+    /// Workaround https://issues.dlang.org/show_bug.cgi?id=24388 when called
+    /// when called with (null, ...).
+    public this (typeof(null) _,
+        string root = ProjectPath.toNativeString(),
+        PackageSupplier[] extras = null,
+        SkipPackageSuppliers skip = SkipPackageSuppliers.none)
+    {
+        alias TType = void delegate(scope FSEntry);
+        this(TType.init, root, extras, skip);
+    }
+
+    /// Internal constructor
+    private this(FSEntry fs_, string root, PackageSupplier[] extras,
+        SkipPackageSuppliers skip)
+    {
+        this.fs = fs_;
         super(root, extras, skip);
     }
 
+    /***************************************************************************
+
+        Get a new `Dub` instance with the same filesystem
+
+        This creates a new `TestDub` instance with the existing filesystem,
+        allowing one to write tests that would normally require multiple Dub
+        instantiation (e.g. test that `fetch` is idempotent).
+        Like the main `TestDub` constructor, it allows to do modifications to
+        the filesystem before the new instantiation is made.
+
+        Params:
+          dg = Delegate to be called with the filesystem, before `TestDub`
+               instantiation is performed;
+
+        Returns:
+          A new `TestDub` instance referencing the same filesystem as `this`.
+
+    ***************************************************************************/
+
+    public TestDub newTest (scope void delegate(scope FSEntry root) dg = null,
+        string root = ProjectPath.toNativeString(),
+        PackageSupplier[] extras = null,
+        SkipPackageSuppliers skip = SkipPackageSuppliers.none)
+    {
+        if (dg !is null) dg(this.fs);
+        return new TestDub(this.fs, root, extras, skip);
+    }
+
     /// Avoid loading user configuration
     protected override Settings loadConfig(ref SpecialDirs dirs) const
     {
-        // No-op
+        dirs = Paths;
         return Settings.init;
     }
 
 	///
-	protected override PackageManager makePackageManager() const
+	protected override PackageManager makePackageManager()
 	{
-		return new TestPackageManager();
+		assert(this.fs !is null);
+		return new TestPackageManager(this.fs);
 	}
 
     /// See `MockPackageSupplier` documentation for this class' implementation
@@ -152,16 +281,11 @@
         return new MockPackageSupplier(url);
     }
 
-	/// Loads the package from the specified path as the main project package.
-	public override void loadPackage(NativePath path)
-	{
-		assert(0, "Not implemented");
-	}
-
 	/// Loads a specific package as the main project package (can be a sub package)
 	public override void loadPackage(Package pack)
 	{
-		m_project = new Project(m_packageManager, pack, new TestSelectedVersions());
+        auto selections = this.packageManager.loadSelections(pack);
+		m_project = new Project(m_packageManager, pack, selections);
 	}
 
 	/// Reintroduce parent overloads
@@ -177,71 +301,6 @@
 	{
 		return cast(inout(TestPackageManager)) this.m_packageManager;
 	}
-
-	/**
-	 * Creates a package with the provided recipe
-	 *
-	 * This is a convenience function provided to create a package based on
-	 * a given recipe. This is to allow test-cases to be written based off
-	 * issues more easily.
-     *
-     * In order for the `Package` to be visible to `Dub`, use `addTestPackage`,
-     * as `makeTestPackage` simply creates the `Package` without adding it.
-	 *
-	 * Params:
-	 *	 str = The string representation of the `PackageRecipe`
-	 *	 recipe = The `PackageRecipe` to use
-	 *	 vers = The version the package is at, e.g. `Version("1.0.0")`
-	 *	 fmt = The format `str` is in, either JSON or SDL
-	 *
-	 * Returns:
-	 *	 The created `Package` instance
-	 */
-	public Package makeTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json)
-	{
-		import dub.recipe.io;
-		final switch (fmt) {
-			case PackageFormat.json:
-				auto recipe = parsePackageRecipe(str, "dub.json");
-                recipe.version_ = vers.toString();
-                return new Package(recipe);
-			case PackageFormat.sdl:
-				auto recipe = parsePackageRecipe(str, "dub.sdl");
-                recipe.version_ = vers.toString();
-                return new Package(recipe);
-		}
-	}
-
-    /// Ditto
-	public Package addTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json)
-    {
-        return this.packageManager.add(this.makeTestPackage(str, vers, fmt));
-    }
-}
-
-/**
- *
- */
-public class TestSelectedVersions : SelectedVersions {
-	import dub.recipe.selection;
-
-	/// Forward to parent's constructor
-	public this(uint version_ = FileVersion) @safe pure nothrow @nogc
-	{
-		super(version_);
-	}
-
-	/// Ditto
-	public this(Selected data) @safe pure nothrow @nogc
-	{
-		super(data);
-	}
-
-	/// Do not do IO
-	public override void save(NativePath path)
-	{
-		// No-op
-	}
 }
 
 /**
@@ -253,121 +312,162 @@
  */
 package class TestPackageManager : PackageManager
 {
+    /// `loadSCMPackage` will strip some part of the remote / repository,
+    /// which we need to mimic to provide a usable API.
+    private struct GitReference {
+        ///
+        this (in Repository repo) {
+            this.remote = repo.remote.chompPrefix("git+");
+            this.ref_ = repo.ref_.chompPrefix("~");
+        }
+
+        ///
+        this (in string remote, in string gitref) {
+            this.remote = remote;
+            this.ref_ = gitref;
+        }
+
+        string remote;
+        string ref_;
+    }
+
+
     /// List of all SCM packages that can be fetched by this instance
-    protected Package[Repository] scm;
+    protected string[GitReference] scm;
+    /// The virtual filesystem that this PackageManager acts on
+    protected FSEntry fs;
 
-    this()
+    this(FSEntry filesystem)
     {
-        NativePath pkg = NativePath("/tmp/dub-testsuite-nonexistant/packages/");
-        NativePath user = NativePath("/tmp/dub-testsuite-nonexistant/user/");
-        NativePath system = NativePath("/tmp/dub-testsuite-nonexistant/system/");
-        super(pkg, user, system, false);
+        NativePath local = TestDub.ProjectPath;
+        NativePath user = TestDub.Paths.userSettings;
+        NativePath system = TestDub.Paths.systemSettings;
+        this.fs = filesystem;
+        super(local, user, system, false);
     }
 
-    /// Disabled as semantic are not implementable unless a virtual FS is created
-	public override @property void customCachePaths(NativePath[] custom_cache_paths)
-    {
-        assert(0, "Function not implemented");
-    }
+    /// Port of `Project.loadSelections`
+    SelectedVersions loadSelections(in Package pack)
+	{
+		import dub.version_;
+        import dub.internal.configy.Read;
+		import dub.internal.dyaml.stdsumtype;
+
+		auto selverfile = (pack.path ~ SelectedVersions.defaultFile);
+		// No file exists
+		if (!this.fs.existsFile(selverfile))
+			return new SelectedVersions();
+
+        SelectionsFile selected;
+        try
+        {
+            const content = this.fs.readText(selverfile);
+            selected = parseConfigString!SelectionsFile(
+                content, selverfile.toNativeString());
+        }
+        catch (Exception exc) {
+            logError("Error loading %s: %s", selverfile, exc.message());
+			return new SelectedVersions();
+        }
+
+		return selected.content.match!(
+			(Selections!0 s) {
+				logWarnTag("Unsupported version",
+					"File %s has fileVersion %s, which is not yet supported by DUB %s.",
+					selverfile, s.fileVersion, dubVersion);
+				logWarn("Ignoring selections file. Use a newer DUB version " ~
+					"and set the appropriate toolchainRequirements in your recipe file");
+				return new SelectedVersions();
+			},
+			(Selections!1 s) => new SelectedVersions(s),
+		);
+	}
+
+    // Re-introduce hidden/deprecated overloads
+    public alias store = PackageManager.store;
 
     /// Ditto
-    public override Package store(NativePath src, PlacementLocation dest, string name, Version vers)
+    public override Package store(NativePath src, PlacementLocation dest, in PackageName name, in Version vers)
     {
         assert(0, "Function not implemented");
     }
 
-    /**
-     * This function usually scans the filesystem for packages.
-     *
-     * We don't want to do IO access and rely on users adding the packages
-     * before the test starts instead.
-     *
-     * Note: Deprecated `refresh(bool)` does IO, but it's deprecated
-     */
-    public override void refresh()
-    {
-        // Do nothing
-    }
-
-    /**
-     * Looks up a specific package
-     *
-     * Unlike its parent class, no lazy loading is performed.
-     * Additionally, as they are already deprecated, overrides are
-     * disabled and not available.
-     */
-	public override Package getPackage(string name, Version vers, bool enable_overrides = false)
-    {
-        //assert(!enable_overrides, "Overrides are not implemented for TestPackageManager");
-
-        // Implementation inspired from `PackageManager.lookup`,
-        // except we replaced `load` with `lookup`.
-        if (auto pkg = this.m_internal.lookup(name, vers, this))
-			return pkg;
-
-		foreach (ref location; this.m_repositories)
-			if (auto p = location.lookup(name, vers, this))
-				return p;
-
-		return null;
-    }
-
 	/**
-	 * Re-Implementation of `loadSCMPackage`.
+	 * Re-Implementation of `gitClone`.
 	 *
-	 * The base implementation will do a `git` clone, which we would like to avoid.
-	 * Instead, we allow unittests to explicitly define what packages should be
-	 * reachable in a given test.
+	 * The base implementation will do a `git` clone, to the file-system.
+	 * We need to mock both the `git` part and the write to the file system.
 	 */
-	public override Package loadSCMPackage(string name, Repository repo)
+	protected override bool gitClone(string remote, string gitref, in NativePath dest)
 	{
-        import std.string : chompPrefix;
-
-		// We're trying to match `loadGitPackage` as much as possible
-		if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash)
-			return null;
-
-		string gitReference = repo.ref_.chompPrefix("~");
-		NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_);
-		destination ~= name;
-		destination.endsWithSlash = true;
-
-		foreach (p; getPackageIterator(name))
-			if (p.path == destination)
-				return p;
-
-		return this.loadSCMRepository(name, repo);
-	}
-
-	/// The private part of `loadSCMPackage`
-	protected Package loadSCMRepository(string name, Repository repo)
-	{
-		if (auto prepo = repo in this.scm) {
-            this.add(*prepo);
-            return *prepo;
+        if (auto pstr = GitReference(remote, gitref) in this.scm) {
+            this.fs.mkdir(dest);
+            this.fs.writeFile(dest ~ "dub.json", *pstr);
+            return true;
         }
-		return null;
-	}
-
-    /**
-     * Adds a `Package` to this `PackageManager`
-     *
-     * This is currently only available in unittests as it is a convenience
-     * function used by `TestDub`, but could be generalized once IO has been
-     * abstracted away from this class.
-     */
-	public Package add(Package pkg)
-	{
-		// See `PackageManager.addPackages` for inspiration.
-		assert(!pkg.subPackages.length, "Subpackages are not yet supported");
-		this.m_internal.fromPath ~= pkg;
-		return pkg;
+        return false;
 	}
 
     /// Add a reachable SCM package to this `PackageManager`
-    public void addTestSCMPackage(Repository repo, Package pkg)
+    public void addTestSCMPackage(in Repository repo, string dub_json)
     {
-        this.scm[repo] = pkg;
+        this.scm[GitReference(repo)] = dub_json;
+    }
+
+    ///
+    protected override bool existsDirectory(NativePath path)
+    {
+        return this.fs.existsDirectory(path);
+    }
+
+    ///
+    protected override void ensureDirectory(NativePath path)
+    {
+        this.fs.mkdir(path);
+    }
+
+    ///
+    protected override bool existsFile(NativePath path)
+    {
+        return this.fs.existsFile(path);
+    }
+
+    ///
+    protected override void writeFile(NativePath path, const(ubyte)[] data)
+    {
+        return this.fs.writeFile(path, data);
+    }
+
+    ///
+    protected override void writeFile(NativePath path, const(char)[] data)
+    {
+        return this.fs.writeFile(path, data);
+    }
+
+    ///
+    protected override string readText(NativePath path)
+    {
+        return this.fs.readText(path);
+    }
+
+    ///
+    protected override IterateDirDg iterateDirectory(NativePath path)
+    {
+        enforce(this.fs.existsDirectory(path),
+            path.toNativeString() ~ " does not exists or is not a directory");
+        auto dir = this.fs.lookup(path);
+        int iterator(scope int delegate(ref FileInfo) del) {
+            foreach (c; dir.children) {
+                FileInfo fi;
+                fi.name = c.name;
+                fi.size = (c.type == FSEntry.Type.Directory) ? 0 : c.content.length;
+                fi.isDirectory = (c.type == FSEntry.Type.Directory);
+                if (auto res = del(fi))
+                    return res;
+            }
+            return 0;
+        }
+        return &iterator;
     }
 }
 
@@ -380,7 +480,7 @@
 public class MockPackageSupplier : PackageSupplier
 {
     /// Mapping of package name to packages, ordered by `Version`
-    protected Package[][string] pkgs;
+    protected Package[Version][PackageName] pkgs;
 
     /// URL this was instantiated with
     protected string url;
@@ -398,32 +498,35 @@
     }
 
     ///
-    public override Version[] getVersions(string package_id)
+    public override Version[] getVersions(in PackageName name)
     {
-        if (auto ppkgs = package_id in this.pkgs)
-            return (*ppkgs).map!(pkg => pkg.version_).array;
+        if (auto ppkgs = name.main in this.pkgs)
+            return (*ppkgs).keys;
         return null;
     }
 
     ///
-    public override void fetchPackage(
-        NativePath path, string package_id, in VersionRange dep, bool pre_release)
+    public override ubyte[] fetchPackage(in PackageName name,
+        in VersionRange dep, bool pre_release)
     {
-        assert(0, this.url ~ " - fetchPackage not implemented for: " ~ package_id);
+        assert(0, "%s - fetchPackage not implemented for: %s"
+            .format(this.url, name.main));
     }
 
     ///
-    public override Json fetchPackageRecipe(
-        string package_id, in VersionRange dep, bool pre_release)
+    public override Json fetchPackageRecipe(in PackageName name,
+        in VersionRange dep, bool pre_release)
     {
         import dub.recipe.json;
 
-        if (auto ppkgs = package_id in this.pkgs)
-            foreach_reverse (pkg; *ppkgs)
-                if ((!pkg.version_.isPreRelease || pre_release) &&
-                    dep.matches(pkg.version_))
-                    return toJson(pkg.recipe);
-        return Json.init;
+        Package match;
+        if (auto ppkgs = name.main in this.pkgs)
+            foreach (vers, pkg; *ppkgs)
+                if ((!vers.isPreRelease || pre_release) &&
+                    dep.matches(vers) &&
+                    (match is null || match.version_ < vers))
+                    match = pkg;
+        return match is null ? Json.init : toJson(match.recipe);
     }
 
     ///
@@ -432,3 +535,345 @@
         assert(0, this.url ~ " - searchPackages not implemented for: " ~ query);
     }
 }
+
+/// An abstract filesystem representation
+public class FSEntry
+{
+    /// Type of file system entry
+    public enum Type {
+        Directory,
+        File,
+    }
+
+    /// Ditto
+    protected Type type;
+    /// The name of this node
+    protected string name;
+    /// The parent of this entry (can be null for the root)
+    protected FSEntry parent;
+    union {
+        /// Children for this FSEntry (with type == Directory)
+        protected FSEntry[] children;
+        /// Content for this FDEntry (with type == File)
+        protected ubyte[] content;
+    }
+
+    /// Creates a new FSEntry
+    private this (FSEntry p, Type t, string n)
+    {
+        this.type = t;
+        this.parent = p;
+        this.name = n;
+    }
+
+    /// Create the root of the filesystem, only usable from this module
+    private this ()
+    {
+        this.type = Type.Directory;
+    }
+
+    /// Get a direct children node, returns `null` if it can't be found
+    protected inout(FSEntry) lookup(string name) inout return scope
+    {
+        assert(!name.canFind('/'));
+        foreach (c; this.children)
+            if (c.name == name)
+                return c;
+        return null;
+    }
+
+    /// Get an arbitrarily nested children node
+    protected inout(FSEntry) lookup(NativePath path) inout return scope
+    {
+        auto relp = this.relativePath(path);
+        if (relp.empty)
+            return this;
+        auto segments = relp.bySegment;
+        if (auto c = this.lookup(segments.front.name)) {
+            segments.popFront();
+            return !segments.empty ? c.lookup(NativePath(segments)) : c;
+        }
+        return null;
+    }
+
+    /** Get the parent `FSEntry` of a `NativePath`
+     *
+     * If the parent doesn't exist, an `Exception` will be thrown
+     * unless `silent` is provided. If the parent path is a file,
+     * an `Exception` will be thrown regardless of `silent`.
+     *
+     * Params:
+     *   path = The path to look up the parent for
+     *   silent = Whether to error on non-existing parent,
+     *            default to `false`.
+     */
+    protected inout(FSEntry) getParent(NativePath path, bool silent = false)
+        inout return scope
+    {
+        // Relative path in the current directory
+        if (!path.hasParentPath())
+            return this;
+
+        // If we're not in the right `FSEntry`, recurse
+        const parentPath = path.parentPath();
+        auto p = this.lookup(parentPath);
+        enforce(silent || p !is null,
+            "No such directory: " ~ parentPath.toNativeString());
+        enforce(p is null || p.type == Type.Directory,
+            "Parent path is not a directory: " ~ parentPath.toNativeString());
+        return p;
+    }
+
+    /// Returns: A path relative to `this.path`
+    protected NativePath relativePath(NativePath path) const scope
+    {
+        assert(!path.absolute() || path.startsWith(this.path),
+               "Calling relativePath with a differently rooted path");
+        return path.absolute() ? path.relativeTo(this.path) : path;
+    }
+
+    /*+*************************************************************************
+
+        Utility function
+
+        Below this banners are functions that are provided for the convenience
+        of writing tests for `Dub`.
+
+    ***************************************************************************/
+
+    /// Prints a visual representation of the filesystem to stdout for debugging
+    public void print(bool content = false) const scope
+    {
+        import std.range : repeat;
+        static import std.stdio;
+
+        size_t indent;
+        for (auto p = &this.parent; (*p) !is null; p = &p.parent)
+            indent++;
+        // Don't print anything (even a newline) for root
+        if (this.parent is null)
+            std.stdio.write('/');
+        else
+            std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' ');
+
+        final switch (this.type) {
+        case Type.Directory:
+            std.stdio.writeln('(', this.children.length, " entries):");
+            foreach (c; this.children)
+                c.print(content);
+            break;
+        case Type.File:
+            if (!content)
+                std.stdio.writeln('(', this.content.length, " bytes)");
+            else if (this.name.endsWith(".json") || this.name.endsWith(".sdl"))
+                std.stdio.writeln('(', this.content.length, " bytes): ",
+                    cast(string) this.content);
+            else
+                std.stdio.writeln('(', this.content.length, " bytes): ",
+                    this.content);
+            break;
+        }
+    }
+
+    /// Returns: The final destination a specific package needs to be stored in
+    public static NativePath getPackagePath(in string name_, string vers,
+        PlacementLocation location = PlacementLocation.user)
+    {
+        PackageName name = PackageName(name_);
+        // Keep in sync with `dub.packagemanager: PackageManager.getPackagePath`
+        // and `Location.getPackagePath`
+        NativePath result (in NativePath base)
+        {
+            NativePath res = base ~ name.main.toString() ~ vers ~
+                name.main.toString();
+            res.endsWithSlash = true;
+            return res;
+        }
+
+        final switch (location) {
+        case PlacementLocation.user:
+            return result(TestDub.Paths.userSettings ~ "packages/");
+        case PlacementLocation.system:
+            return result(TestDub.Paths.systemSettings ~ "packages/");
+        case PlacementLocation.local:
+            return result(TestDub.ProjectPath ~ "/.dub/packages/");
+        }
+    }
+
+    /*+*************************************************************************
+
+        Public filesystem functions
+
+        Below this banners are functions which mimic the behavior of a file
+        system.
+
+    ***************************************************************************/
+
+    /// Returns: The `path` of this FSEntry
+    public NativePath path() const scope
+    {
+        if (this.parent is null)
+            return NativePath("/");
+        auto thisPath = this.parent.path ~ this.name;
+        thisPath.endsWithSlash = (this.type == Type.Directory);
+        return thisPath;
+    }
+
+    /// Implements `mkdir -p`, returns the created directory
+    public FSEntry mkdir (NativePath path) scope
+    {
+        auto relp = this.relativePath(path);
+        // Check if the child already exists
+        auto segments = relp.bySegment;
+        auto child = this.lookup(segments.front.name);
+        if (child is null) {
+            child = new FSEntry(this, Type.Directory, segments.front.name);
+            this.children ~= child;
+        }
+        // Recurse if needed
+        segments.popFront();
+        return !segments.empty ? child.mkdir(NativePath(segments)) : child;
+    }
+
+    /// Checks the existence of a file
+    public bool existsFile (NativePath path) const scope
+    {
+        auto entry = this.lookup(path);
+        return entry !is null && entry.type == Type.File;
+    }
+
+    /// Checks the existence of a directory
+    public bool existsDirectory (NativePath path) const scope
+    {
+        auto entry = this.lookup(path);
+        return entry !is null && entry.type == Type.Directory;
+    }
+
+    /// Reads a file, returns the content as `ubyte[]`
+    public ubyte[] readFile (NativePath path) const scope
+    {
+        auto entry = this.lookup(path);
+        enforce(entry.type == Type.File, "Trying to read a directory");
+        return entry.content.dup;
+    }
+
+    /// Reads a file, returns the content as text
+    public string readText (NativePath path) const scope
+    {
+        import std.utf : validate;
+
+        auto entry = this.lookup(path);
+        enforce(entry.type == Type.File, "Trying to read a directory");
+        // Ignore BOM: If it's needed for a test, add support for it.
+        validate(cast(const(char[])) entry.content);
+        return cast(string) entry.content.idup();
+    }
+
+    /// Write to this file
+    public void writeFile (NativePath path, const(char)[] data) scope
+    {
+        this.writeFile(path, data.representation);
+    }
+
+    /// Ditto
+    public void writeFile (NativePath path, const(ubyte)[] data) scope
+    {
+        enforce(!path.endsWithSlash(),
+            "Cannot write to directory: " ~ path.toNativeString());
+        if (auto file = this.lookup(path)) {
+            // If the file already exists, override it
+            enforce(file.type == Type.File,
+                "Trying to write to directory: " ~ path.toNativeString());
+            file.content = data.dup;
+        } else {
+            auto p = this.getParent(path);
+            auto file = new FSEntry(p, Type.File, path.head.name());
+            file.content = data.dup;
+            p.children ~= file;
+        }
+    }
+
+    /** Remove a file
+     *
+     * Always error if the target is a directory.
+     * Does not error if the target does not exists
+     * and `force` is set to `true`.
+     *
+     * Params:
+     *   path = Path to the file to remove
+     *   force = Whether to ignore non-existing file,
+     *           default to `false`.
+     */
+    public void removeFile (NativePath path, bool force = false)
+    {
+        import std.algorithm.searching : countUntil;
+
+        assert(!path.empty, "Empty path provided to `removeFile`");
+        enforce(!path.endsWithSlash(),
+            "Cannot remove file with directory path: " ~ path.toNativeString());
+        auto p = this.getParent(path, force);
+        const idx = p.children.countUntil!(e => e.name == path.head.name());
+        if (idx < 0) {
+            enforce(force,
+                "removeFile: No such file: " ~ path.toNativeString());
+        } else {
+            enforce(p.children[idx].type == Type.File,
+                "removeFile called on a directory: " ~ path.toNativeString());
+            p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
+        }
+    }
+
+    /** Remove a directory
+     *
+     * Remove an existing empty directory.
+     * If `force` is set to `true`, no error will be thrown
+     * if the directory is empty or non-existing.
+     *
+     * Params:
+     *   path = Path to the directory to remove
+     *   force = Whether to ignore non-existing / non-empty directories,
+     *           default to `false`.
+     */
+    public void removeDir (NativePath path, bool force = false)
+    {
+        import std.algorithm.searching : countUntil;
+
+        assert(!path.empty, "Empty path provided to `removeFile`");
+        auto p = this.getParent(path, force);
+        const idx = p.children.countUntil!(e => e.name == path.head.name());
+        if (idx < 0) {
+            enforce(force,
+                "removeDir: No such directory: " ~ path.toNativeString());
+        } else {
+            enforce(p.children[idx].type == Type.Directory,
+                "removeDir called on a file: " ~ path.toNativeString());
+            enforce(force || p.children[idx].children.length == 0,
+                "removeDir called on non-empty directory: " ~ path.toNativeString());
+            p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
+        }
+    }
+}
+
+/**
+ * Convenience function to write a package file
+ *
+ * Allows to write a package file (and only a package file) for a certain
+ * package name and version.
+ *
+ * Params:
+ *   root = The root FSEntry
+ *   name = The package name (typed as string for convenience)
+ *   vers = The package version
+ *   recipe = The text of the package recipe
+ *   fmt = The format used for `recipe` (default to JSON)
+ *   location = Where to place the package (default to user location)
+ */
+public void writePackageFile (FSEntry root, in string name, in string vers,
+    in string recipe, in PackageFormat fmt = PackageFormat.json,
+    in PlacementLocation location = PlacementLocation.user)
+{
+    const path = FSEntry.getPackagePath(name, vers, location);
+    root.mkdir(path).writeFile(
+        NativePath(fmt == PackageFormat.json ? "dub.json" : "dub.sdl"),
+        recipe);
+}
diff --git a/source/dub/test/dependencies.d b/source/dub/test/dependencies.d
index d9f78ed..180f31d 100644
--- a/source/dub/test/dependencies.d
+++ b/source/dub/test/dependencies.d
@@ -31,17 +31,18 @@
 // Ensure that simple dependencies get resolved correctly
 unittest
 {
-    const a = `name "a"
+    scope dub = new TestDub((scope FSEntry root) {
+            root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
+version "1.0.0"
 dependency "b" version="*"
 dependency "c" version="*"
-`;
-    const b = `name "b"`;
-    const c = `name "c"`;
-
-    scope dub = new TestDub();
-    dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl);
-    dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl);
-    dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl));
+`);
+            root.writePackageFile("b", "1.0.0", `name "b"
+version "1.0.0"`, PackageFormat.sdl);
+            root.writePackageFile("c", "1.0.0", `name "c"
+version "1.0.0"`, PackageFormat.sdl);
+        });
+    dub.loadPackage();
 
     dub.upgrade(UpgradeOptions.select);
 
@@ -54,18 +55,16 @@
 // Test that indirect dependencies get resolved correctly
 unittest
 {
-    const a = `name "a"
-dependency "b" version="*"
-`;
-    const b = `name "b"
-dependency "c" version="*"
-`;
-    const c = `name "c"`;
-
-    scope dub = new TestDub();
-    dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl);
-    dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl);
-    dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl));
+    scope dub = new TestDub((scope FSEntry root) {
+            root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
+dependency "b" version="*"`);
+            root.writePackageFile("b", "1.0.0", `name "b"
+version "1.0.0"
+dependency "c" version="*"`, PackageFormat.sdl);
+            root.writePackageFile("c", "1.0.0", `name "c"
+version "1.0.0"`, PackageFormat.sdl);
+    });
+    dub.loadPackage();
 
     dub.upgrade(UpgradeOptions.select);
 
@@ -78,23 +77,21 @@
 // Simple diamond dependency
 unittest
 {
-    const a = `name "a"
+    scope dub = new TestDub((scope FSEntry root) {
+            root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
 dependency "b" version="*"
-dependency "c" version="*"
-`;
-    const b = `name "b"
-dependency "d" version="*"
-`;
-    const c = `name "c"
-dependency "d" version="*"
-`;
-    const d = `name "d"`;
+dependency "c" version="*"`);
+            root.writePackageFile("b", "1.0.0", `name "b"
+version "1.0.0"
+dependency "d" version="*"`, PackageFormat.sdl);
+            root.writePackageFile("c", "1.0.0", `name "c"
+version "1.0.0"
+dependency "d" version="*"`, PackageFormat.sdl);
+            root.writePackageFile("d", "1.0.0", `name "d"
+version "1.0.0"`, PackageFormat.sdl);
 
-    scope dub = new TestDub();
-    dub.addTestPackage(d, Version("1.0.0"), PackageFormat.sdl);
-    dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl);
-    dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl);
-    dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl));
+    });
+    dub.loadPackage();
 
     dub.upgrade(UpgradeOptions.select);
 
@@ -108,24 +105,25 @@
 // Missing dependencies trigger an error
 unittest
 {
-    const a = `name "a"
-dependency "b" version="*"
-`;
-
-    scope dub = new TestDub();
-    dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl));
+    scope dub = new TestDub((scope FSEntry root) {
+            root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
+dependency "b" version="*"`);
+    });
+    dub.loadPackage();
 
     try
         dub.upgrade(UpgradeOptions.select);
     catch (Exception exc)
-        assert(exc.message() == `Failed to find any versions for package b, referenced by a 1.0.0`);
+        assert(exc.message() == `Failed to find any versions for package b, referenced by a ~master`);
 
     assert(!dub.project.hasAllDependencies(), "project should have missing dependencies");
     assert(dub.project.getDependency("b", true) is null, "Found 'b' dependency");
     assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency");
 
     // Add the missing dependency to our PackageManager
-    dub.addTestPackage(`name "b"`, Version("1.0.0"), PackageFormat.sdl);
+    dub.fs.writePackageFile(`b`, "1.0.0", `name "b"
+version "1.0.0"`, PackageFormat.sdl);
+    dub.packageManager.refresh();
     dub.upgrade(UpgradeOptions.select);
     assert(dub.project.hasAllDependencies(), "project have missing dependencies");
     assert(dub.project.getDependency("b", true), "Missing 'b' dependency");
diff --git a/source/dub/test/other.d b/source/dub/test/other.d
index 9407fb2..241fb0f 100644
--- a/source/dub/test/other.d
+++ b/source/dub/test/other.d
@@ -18,33 +18,64 @@
     const ValidURL = `git+https://example.com/dlang/dub`;
     // Taken from a commit in the dub repository
     const ValidHash = "54339dff7ce9ec24eda550f8055354f712f15800";
-    const Template = `{"name": "%s", "dependencies": {
+    const Template = `{"name": "%s", "version": "1.0.0", "dependencies": {
 "dep1": { "repository": "%s", "version": "%s" }}}`;
 
-    scope dub = new TestDub();
+    scope dub = new TestDub((scope FSEntry fs) {
+        // Invalid URL, valid hash
+        fs.writePackageFile("a", "1.0.0", Template.format("a", "git+https://nope.nope", ValidHash));
+        // Valid URL, invalid hash
+        fs.writePackageFile("b", "1.0.0", Template.format("b", ValidURL, "invalid"));
+        // Valid URL, valid hash
+        fs.writePackageFile("c", "1.0.0", Template.format("c", ValidURL, ValidHash));
+    });
     dub.packageManager.addTestSCMPackage(
-        Repository(ValidURL, ValidHash),
-        // Note: SCM package are always marked as using `~master`
-        dub.makeTestPackage(`{ "name": "dep1" }`, Version(`~master`)),
-    );
+        Repository(ValidURL, ValidHash), `{ "name": "dep1" }`);
 
-    // Invalid URL, valid hash
-    const a = Template.format("a", "git+https://nope.nope", ValidHash);
     try
-        dub.loadPackage(dub.addTestPackage(a, Version("1.0.0")));
+        dub.loadPackage(dub.packageManager.getPackage(PackageName("a"), Version("1.0.0")));
+    catch (Exception exc)
+         assert(exc.message.canFind("Unable to fetch"));
+
+    try
+        dub.loadPackage(dub.packageManager.getPackage(PackageName("b"), Version("1.0.0")));
     catch (Exception exc)
         assert(exc.message.canFind("Unable to fetch"));
 
-    // Valid URL, invalid hash
-    const b = Template.format("b", ValidURL, "invalid");
-    try
-        dub.loadPackage(dub.addTestPackage(b, Version("1.0.0")));
-    catch (Exception exc)
-        assert(exc.message.canFind("Unable to fetch"));
-
-    // Valid URL, valid hash
-    const c = Template.format("c", ValidURL, ValidHash);
-    dub.loadPackage(dub.addTestPackage(c, Version("1.0.0")));
+    dub.loadPackage(dub.packageManager.getPackage(PackageName("c"), Version("1.0.0")));
     assert(dub.project.hasAllDependencies());
     assert(dub.project.getDependency("dep1", true), "Missing 'dep1' dependency");
 }
+
+// Test for https://github.com/dlang/dub/pull/2481
+// Make sure packages found with `add-path` take priority.
+unittest
+{
+    const AddPathDir = TestDub.Paths.temp ~ "addpath/";
+    const BDir = AddPathDir ~ "b/";
+    scope dub = new TestDub((scope FSEntry root) {
+            root.writeFile(TestDub.ProjectPath ~ "dub.json",
+                `{ "name": "a", "dependencies": { "b": "~>1.0" } }`);
+
+            root.writePackageFile("b", "1.0.0", `name "b"
+version "1.0.0"`, PackageFormat.sdl);
+            root.mkdir(BDir);
+            root.writeFile(BDir ~ "dub.json", `{"name": "b", "version": "1.0.0" }`);
+    });
+
+    dub.loadPackage();
+    assert(!dub.project.hasAllDependencies());
+    dub.upgrade(UpgradeOptions.select);
+    // Test that without add-path, we get a package in the userPackage
+    const oldDir = dub.project.getDependency("b", true).path();
+    assert(oldDir == TestDub.Paths.userPackages ~ "packages/b/1.0.0/b/",
+           oldDir.toNativeString());
+    // Now run `add-path`
+    dub.addSearchPath(AddPathDir.toNativeString(), dub.defaultPlacementLocation);
+    // We need a new instance to test
+    scope newDub = dub.newTest();
+    newDub.loadPackage();
+    assert(newDub.project.hasAllDependencies());
+    const actualDir = newDub.project.getDependency("b", true).path();
+    assert(actualDir == BDir, actualDir.toNativeString());
+}
diff --git a/source/dub/test/subpackages.d b/source/dub/test/subpackages.d
new file mode 100644
index 0000000..a84ae83
--- /dev/null
+++ b/source/dub/test/subpackages.d
@@ -0,0 +1,40 @@
+/*******************************************************************************
+
+    Test for subpackages
+
+    Subpackages are packages that are part of a 'main' packages. Their version
+    is that of their main (parent) package. They are referenced using a column,
+    e.g. `mainpkg:subpkg`. Nested subpackages are disallowed.
+
+*******************************************************************************/
+
+module dub.test.subpackages;
+
+version(unittest):
+
+import dub.test.base;
+
+/// Test of the PackageManager APIs
+unittest
+{
+    scope dub = new TestDub((scope FSEntry root) {
+        root.writeFile(TestDub.ProjectPath ~ "dub.json",
+            `{ "name": "a", "dependencies": { "b:a": "~>1.0", "b:b": "~>1.0" } }`);
+        root.writePackageFile("b", "1.0.0",
+            `{ "name": "b", "version": "1.0.0", "subPackages": [ { "name": "a" }, { "name": "b" } ] }`);
+    });
+    dub.loadPackage();
+
+    dub.upgrade(UpgradeOptions.select);
+
+    assert(dub.project.hasAllDependencies(), "project has missing dependencies");
+    assert(dub.project.getDependency("b:b", true), "Missing 'b:b' dependency");
+    assert(dub.project.getDependency("b:a", true), "Missing 'b:a' dependency");
+    assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency");
+
+    assert(dub.packageManager().getPackage(PackageName("b:a"), Version("1.0.0")).name == "b:a");
+    assert(dub.packageManager().getPackage(PackageName("b:b"), Version("1.0.0")).name == "b:b");
+    assert(dub.packageManager().getPackage(PackageName("b"), Version("1.0.0")).name == "b");
+
+    assert(!dub.packageManager().getPackage(PackageName("b:b"), Version("1.1.0")));
+}
diff --git a/test/environment-variables.script.d b/test/environment-variables.script.d
index c96c2c4..347f696 100644
--- a/test/environment-variables.script.d
+++ b/test/environment-variables.script.d
@@ -15,7 +15,7 @@
 	// preRunCommands       uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments < deppkg.preRunEnvironments < root.preRunEnvironments
 	// User application     uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments
 	// postRunCommands      uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments < deppkg.postRunEnvironments < root.postRunEnvironments
-	
+
 	// Test cases covers:
 	// preGenerateCommands [in root]
 	//      priority check: system.environments < settings.environments
@@ -50,30 +50,30 @@
 	], Config.none, size_t.max, currDir.buildPath("environment-variables"));
 	scope (failure)
 		writeln("environment-variables test failed... Testing stdout is:\n-----\n", res.output);
-	
+
 	// preGenerateCommands [in root]
 	assert(res.output.canFind("root.preGenerate: setting.PRIORITYCHECK_SYS_SET"),       "preGenerate environment variables priority check is failed.");
 	assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_SET_DEP"),        "preGenerate environment variables priority check is failed.");
 	assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_DEP_ROOT"),       "preGenerate environment variables priority check is failed.");
 	assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_ROOT_DEPSPEC"),   "preGenerate environment variables priority check is failed.");
 	assert(res.output.canFind("root.preGenerate: root.PRIORITYCHECK_DEPSPEC_ROOTSPEC"), "preGenerate environment variables priority check is failed.");
-	
+
 	// postGenerateCommands [in root]
 	assert(res.output.canFind("root.postGenerate: deppkg.VAR4", "postGenerate environment variables expantion check is failed."));
-	
+
 	// preBuildCommands [in deppkg]
 	assert(res.output.canFind("deppkg.preBuild: deppkg.PRIORITYCHECK_ROOT_DEPBLDSPEC"),      "preBuild environment variables priority check is failed.");
 	assert(res.output.canFind("deppkg.preBuild: root.PRIORITYCHECK_DEPBLDSPEC_ROOTBLDSPEC"), "preBuild environment variables priority check is failed.");
 	assert(res.output.canFind("deppkg.preBuild: deppkg.PRIORITYCHECK_ROOTBLDSPEC_DEPSPEC"),  "preBuild environment variables priority check is failed.");
 	assert(res.output.canFind("deppkg.preBuild: root.PRIORITYCHECK_DEPSPEC_ROOTSPEC"),       "preBuild environment variables priority check is failed.");
-	
+
 	// postBuildCommands [in deppkg]
 	assert(res.output.canFind("deppkg.postBuild: deppkg.VAR4"), "postBuild environment variables expantion check is failed.");
-	
+
 	// preRunCommands [in deppkg][in root]
 	assert(!res.output.canFind("deppkg.preRun: deppkg.VAR4"),   "preRun that is defined dependent library does not call.");
 	assert(res.output.canFind("root.preRun: deppkg.VAR4"),      "preRun environment variables expantion check is failed.");
-	
+
 	// Application run
 	assert(res.output.canFind("app.run: root.VAR1"),                "run environment variables expantion check is failed.");
 	assert(res.output.canFind("app.run: settings.VAR2"),            "run environment variables expantion check is failed.");
@@ -81,7 +81,7 @@
 	assert(res.output.canFind("app.run: deppkg.VAR4"),              "run environment variables expantion check is failed.");
 	assert(res.output.canFind("app.run: system.VAR5"),              "run environment variables expantion check is failed.");
 	assert(res.output.canFind("app.run: system.SYSENVVAREXPCHECK"), "run environment variables expantion check is failed.");
-	
+
 	// postRunCommands [in deppkg][in root]
 	assert(!res.output.canFind("deppkg.postRun: deppkg.VAR4"),  "postRunCommands that is defined dependent library does not call.");
 	assert(res.output.canFind("root.postRun: deppkg.VAR4"),     "postRun environment variables expantion check is failed.");
diff --git a/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl b/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl
deleted file mode 100644
index d19952b..0000000
--- a/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl
+++ /dev/null
@@ -1,10 +0,0 @@
-name "a"
-dependency "b" version="*"
-
-configuration "x" {
-	subConfiguration "b" "x"
-}
-
-configuration "y" {
-	subConfiguration "b" "y"
-}
diff --git a/test/issue1005-configuration-resolution/a/dub.sdl b/test/issue1005-configuration-resolution/a/dub.sdl
new file mode 100644
index 0000000..d19952b
--- /dev/null
+++ b/test/issue1005-configuration-resolution/a/dub.sdl
@@ -0,0 +1,10 @@
+name "a"
+dependency "b" version="*"
+
+configuration "x" {
+	subConfiguration "b" "x"
+}
+
+configuration "y" {
+	subConfiguration "b" "y"
+}
diff --git a/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl b/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl
deleted file mode 100644
index 3cfa48b..0000000
--- a/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl
+++ /dev/null
@@ -1,7 +0,0 @@
-name "b"
-
-configuration "x" {
-}
-
-configuration "y" {
-}
\ No newline at end of file
diff --git a/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d b/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d
deleted file mode 100644
index 2a9bb41..0000000
--- a/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d
+++ /dev/null
@@ -1,3 +0,0 @@
-module b;
-
-void foo() {}
diff --git a/test/issue1005-configuration-resolution/b/dub.sdl b/test/issue1005-configuration-resolution/b/dub.sdl
new file mode 100644
index 0000000..3cfa48b
--- /dev/null
+++ b/test/issue1005-configuration-resolution/b/dub.sdl
@@ -0,0 +1,7 @@
+name "b"
+
+configuration "x" {
+}
+
+configuration "y" {
+}
\ No newline at end of file
diff --git a/test/issue1005-configuration-resolution/b/source/b.d b/test/issue1005-configuration-resolution/b/source/b.d
new file mode 100644
index 0000000..2a9bb41
--- /dev/null
+++ b/test/issue1005-configuration-resolution/b/source/b.d
@@ -0,0 +1,3 @@
+module b;
+
+void foo() {}
diff --git a/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl b/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl
deleted file mode 100644
index e46b148..0000000
--- a/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "c"
-dependency "a" version="*"
diff --git a/test/issue1005-configuration-resolution/c/dub.sdl b/test/issue1005-configuration-resolution/c/dub.sdl
new file mode 100644
index 0000000..e46b148
--- /dev/null
+++ b/test/issue1005-configuration-resolution/c/dub.sdl
@@ -0,0 +1,2 @@
+name "c"
+dependency "a" version="*"
diff --git a/test/issue1005-configuration-resolution/main/dub.sdl b/test/issue1005-configuration-resolution/main/dub.sdl
new file mode 100644
index 0000000..d492491
--- /dev/null
+++ b/test/issue1005-configuration-resolution/main/dub.sdl
@@ -0,0 +1,6 @@
+name "main"
+
+dependency "b" version="*"
+dependency "c" version="*"
+
+subConfiguration "b" "y"
diff --git a/test/issue1005-configuration-resolution/main/source/app.d b/test/issue1005-configuration-resolution/main/source/app.d
new file mode 100644
index 0000000..0ec7361
--- /dev/null
+++ b/test/issue1005-configuration-resolution/main/source/app.d
@@ -0,0 +1,6 @@
+import b;
+
+void main()
+{
+	foo();
+}
diff --git a/test/issue1005-configuration-resolution/main/~master/main/dub.sdl b/test/issue1005-configuration-resolution/main/~master/main/dub.sdl
deleted file mode 100644
index d492491..0000000
--- a/test/issue1005-configuration-resolution/main/~master/main/dub.sdl
+++ /dev/null
@@ -1,6 +0,0 @@
-name "main"
-
-dependency "b" version="*"
-dependency "c" version="*"
-
-subConfiguration "b" "y"
diff --git a/test/issue1005-configuration-resolution/main/~master/main/source/app.d b/test/issue1005-configuration-resolution/main/~master/main/source/app.d
deleted file mode 100644
index 0ec7361..0000000
--- a/test/issue1005-configuration-resolution/main/~master/main/source/app.d
+++ /dev/null
@@ -1,6 +0,0 @@
-import b;
-
-void main()
-{
-	foo();
-}
diff --git a/test/issue1024-selective-upgrade.sh b/test/issue1024-selective-upgrade.sh
index ef60333..dc7c009 100755
--- a/test/issue1024-selective-upgrade.sh
+++ b/test/issue1024-selective-upgrade.sh
@@ -2,13 +2,13 @@
 
 . $(dirname "${BASH_SOURCE[0]}")/common.sh
 cd ${CURR_DIR}/issue1024-selective-upgrade
-echo "{\"fileVersion\": 1,\"versions\": {\"a\": \"1.0.0\", \"b\": \"1.0.0\"}}" > main/~master/main/dub.selections.json
-$DUB upgrade --bare --root=main/~master/main/ a
+echo "{\"fileVersion\": 1,\"versions\": {\"a\": \"1.0.0\", \"b\": \"1.0.0\"}}" > main/dub.selections.json
+$DUB upgrade --bare --root=main a
 
-if ! grep -c -e "\"a\": \"1.0.1\"" main/~master/main/dub.selections.json; then
+if ! grep -c -e "\"a\": \"1.0.1\"" main/dub.selections.json; then
 	die $LINENO "Specified dependency was not upgraded."
 fi
 
-if grep -c -e "\"b\": \"1.0.1\"" main/~master/main/dub.selections.json; then
+if grep -c -e "\"b\": \"1.0.1\"" main/dub.selections.json; then
 	die $LINENO "Non-specified dependency got upgraded."
 fi
diff --git a/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl b/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl
new file mode 100644
index 0000000..7ff9fa1
--- /dev/null
+++ b/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl
@@ -0,0 +1,2 @@
+name "a"
+version "1.0.0"
diff --git a/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl b/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl
new file mode 100644
index 0000000..5c8a407
--- /dev/null
+++ b/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl
@@ -0,0 +1,2 @@
+name "a"
+version "1.0.1"
diff --git a/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl b/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl
deleted file mode 100644
index 7ff9fa1..0000000
--- a/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "a"
-version "1.0.0"
diff --git a/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl b/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl
deleted file mode 100644
index 5c8a407..0000000
--- a/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "a"
-version "1.0.1"
diff --git a/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl b/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl
new file mode 100644
index 0000000..5597559
--- /dev/null
+++ b/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl
@@ -0,0 +1,2 @@
+name "b"
+version "1.0.0"
diff --git a/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl b/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl
new file mode 100644
index 0000000..5e0c01a
--- /dev/null
+++ b/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl
@@ -0,0 +1,2 @@
+name "b"
+version "1.0.1"
diff --git a/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl b/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl
deleted file mode 100644
index 5597559..0000000
--- a/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "b"
-version "1.0.0"
diff --git a/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl b/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl
deleted file mode 100644
index 5e0c01a..0000000
--- a/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "b"
-version "1.0.1"
diff --git a/test/issue1024-selective-upgrade/main/dub.sdl b/test/issue1024-selective-upgrade/main/dub.sdl
new file mode 100644
index 0000000..a9da177
--- /dev/null
+++ b/test/issue1024-selective-upgrade/main/dub.sdl
@@ -0,0 +1,3 @@
+name "test"
+dependency "a" version="~>1.0.0"
+dependency "b" version="~>1.0.0"
diff --git a/test/issue1024-selective-upgrade/main/~master/main/dub.sdl b/test/issue1024-selective-upgrade/main/~master/main/dub.sdl
deleted file mode 100644
index a9da177..0000000
--- a/test/issue1024-selective-upgrade/main/~master/main/dub.sdl
+++ /dev/null
@@ -1,3 +0,0 @@
-name "test"
-dependency "a" version="~>1.0.0"
-dependency "b" version="~>1.0.0"
diff --git a/test/issue1504-envvar-in-path/source/app.d b/test/issue1504-envvar-in-path/source/app.d
index 131d984..2b03f41 100644
--- a/test/issue1504-envvar-in-path/source/app.d
+++ b/test/issue1504-envvar-in-path/source/app.d
@@ -1,5 +1,5 @@
 pragma(msg, import("message.txt"));
 
 void main()
-{    
-}
\ No newline at end of file
+{
+}
diff --git a/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d b/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d
index a3caf1d..daa6ae8 100644
--- a/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d
+++ b/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d
@@ -28,11 +28,11 @@
 	auto dub = environment.get("DUB");
 	if (!dub.length)
 		dub = buildPath(".", "bin", "dub");
-        
+
     string destinationDirectory = tempDir;
     // remove any ending slahes (which can for some reason be added at the end by tempDir, which fails on OSX) https://issues.dlang.org/show_bug.cgi?id=22738
     destinationDirectory = buildNormalizedPath(destinationDirectory);
-    
+
 	string filename;
 	// check if the single file package with dependency compiles and runs
 	{
@@ -100,4 +100,4 @@
 		writeln("\nError. Unittest passed.");
 
 	return rc1 | !rc2;
-}
\ No newline at end of file
+}
diff --git a/test/issue2650-deprecated-modules/.no_build b/test/issue2650-deprecated-modules/.no_build
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/issue2650-deprecated-modules/.no_build
diff --git a/test/issue2650-deprecated-modules/.no_run b/test/issue2650-deprecated-modules/.no_run
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/issue2650-deprecated-modules/.no_run
diff --git a/test/issue2650-deprecated-modules/dub.sdl b/test/issue2650-deprecated-modules/dub.sdl
new file mode 100644
index 0000000..4e16198
--- /dev/null
+++ b/test/issue2650-deprecated-modules/dub.sdl
@@ -0,0 +1,3 @@
+name "issue2650"
+targetType "sourceLibrary"
+buildRequirements "disallowDeprecations"
diff --git a/test/issue2650-deprecated-modules/source/test.d b/test/issue2650-deprecated-modules/source/test.d
new file mode 100644
index 0000000..896365b
--- /dev/null
+++ b/test/issue2650-deprecated-modules/source/test.d
@@ -0,0 +1 @@
+deprecated module test;
diff --git a/test/issue2840-build-collision.sh b/test/issue2840-build-collision.sh
new file mode 100755
index 0000000..70ad1cb
--- /dev/null
+++ b/test/issue2840-build-collision.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+. $(dirname "${BASH_SOURCE[0]}")/common.sh
+
+pushd $(dirname "${BASH_SOURCE[0]}")/issue2840-build-collision
+# Copy before building, as dub uses timestamp to check for rebuild
+rm -rf nested/ && mkdir -p nested/ && cp -v build.d nested/
+
+$DUB ./build.d $(pwd)/build.d
+pushd nested
+$DUB ./build.d $(pwd)/build.d
+popd
+
+popd
diff --git a/test/issue2840-build-collision/.no_build b/test/issue2840-build-collision/.no_build
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/issue2840-build-collision/.no_build
diff --git a/test/issue2840-build-collision/build.d b/test/issue2840-build-collision/build.d
new file mode 100755
index 0000000..0876eac
--- /dev/null
+++ b/test/issue2840-build-collision/build.d
@@ -0,0 +1,16 @@
+#!/usr/bin/env dub
+/++ dub.json:
+   {
+       "name": "build"
+   }
++/
+
+import std.format;
+
+immutable FullPath = __FILE_FULL_PATH__;
+
+void main (string[] args)
+{
+    assert(args.length == 2, "Expected a single argument");
+    assert(args[1] == FullPath, format("%s != %s -- %s", args[1], FullPath, args[0]));
+}
diff --git a/test/issue564-invalid-upgrade-dependency.sh b/test/issue564-invalid-upgrade-dependency.sh
index fe1b6a3..19258ce 100755
--- a/test/issue564-invalid-upgrade-dependency.sh
+++ b/test/issue564-invalid-upgrade-dependency.sh
@@ -2,4 +2,7 @@
 
 . $(dirname "${BASH_SOURCE[0]}")/common.sh
 cd ${CURR_DIR}/issue564-invalid-upgrade-dependency
-${DUB} build -f --bare --compiler=${DC} main
+rm -rf a-1.0.0/.dub
+rm -rf a-1.1.0/.dub
+rm -rf main/.dub
+${DUB} build --bare --compiler=${DC} main
diff --git a/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json b/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json
new file mode 100644
index 0000000..cc36ecb
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json
@@ -0,0 +1,4 @@
+{
+	"name": "a",
+	"version": "1.0.0",
+}
diff --git a/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d b/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d
new file mode 100644
index 0000000..45d8a32
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d
@@ -0,0 +1,3 @@
+void test()
+{
+}
diff --git a/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json b/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json
new file mode 100644
index 0000000..4103fe5
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json
@@ -0,0 +1,7 @@
+{
+	"name": "a",
+	"version": "1.1.0",
+	"dependencies": {
+		"invalid": {"path": "invalid"}
+	}
+}
diff --git a/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d b/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d
new file mode 100644
index 0000000..45d8a32
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d
@@ -0,0 +1,3 @@
+void test()
+{
+}
diff --git a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json b/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json
deleted file mode 100644
index cc36ecb..0000000
--- a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-	"name": "a",
-	"version": "1.0.0",
-}
diff --git a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d b/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d
deleted file mode 100644
index 45d8a32..0000000
--- a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d
+++ /dev/null
@@ -1,3 +0,0 @@
-void test()
-{
-}
diff --git a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json b/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json
deleted file mode 100644
index 4103fe5..0000000
--- a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-	"name": "a",
-	"version": "1.1.0",
-	"dependencies": {
-		"invalid": {"path": "invalid"}
-	}
-}
diff --git a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d b/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d
deleted file mode 100644
index 45d8a32..0000000
--- a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d
+++ /dev/null
@@ -1,3 +0,0 @@
-void test()
-{
-}
diff --git a/test/issue564-invalid-upgrade-dependency/main/dub.json b/test/issue564-invalid-upgrade-dependency/main/dub.json
new file mode 100644
index 0000000..7d27d9d
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/main/dub.json
@@ -0,0 +1,6 @@
+{
+	"name": "main",
+	"dependencies": {
+		"a": "~>1.0"
+	}
+}
diff --git a/test/issue564-invalid-upgrade-dependency/main/dub.selections.json b/test/issue564-invalid-upgrade-dependency/main/dub.selections.json
new file mode 100644
index 0000000..e24adfe
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/main/dub.selections.json
@@ -0,0 +1,6 @@
+{
+	"fileVersion": 1,
+	"versions": {
+		"a": "1.0.0"
+	}
+}
diff --git a/test/issue564-invalid-upgrade-dependency/main/source/app.d b/test/issue564-invalid-upgrade-dependency/main/source/app.d
new file mode 100644
index 0000000..b248b89
--- /dev/null
+++ b/test/issue564-invalid-upgrade-dependency/main/source/app.d
@@ -0,0 +1,6 @@
+import a;
+
+void main()
+{
+	test();
+}
diff --git a/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json b/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json
deleted file mode 100644
index 7d27d9d..0000000
--- a/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-	"name": "main",
-	"dependencies": {
-		"a": "~>1.0"
-	}
-}
diff --git a/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d b/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d
deleted file mode 100644
index b248b89..0000000
--- a/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d
+++ /dev/null
@@ -1,6 +0,0 @@
-import a;
-
-void main()
-{
-	test();
-}
diff --git a/test/issue813-pure-sub-dependency.sh b/test/issue813-pure-sub-dependency.sh
index a76dee8..ec2291e 100755
--- a/test/issue813-pure-sub-dependency.sh
+++ b/test/issue813-pure-sub-dependency.sh
@@ -2,5 +2,8 @@
 
 . $(dirname "${BASH_SOURCE[0]}")/common.sh
 cd ${CURR_DIR}/issue813-pure-sub-dependency
-rm -f main/~master/main/dub.selections.json
-${DUB} build -f --bare --compiler=${DC} main
+rm -rf main/.dub
+rm -rf sub/.dub
+rm -rf sub/sub/.dub
+rm -f main/dub.selections.json
+${DUB} build --bare --compiler=${DC} main
diff --git a/test/issue813-pure-sub-dependency/main/dub.sdl b/test/issue813-pure-sub-dependency/main/dub.sdl
new file mode 100644
index 0000000..79f7d71
--- /dev/null
+++ b/test/issue813-pure-sub-dependency/main/dub.sdl
@@ -0,0 +1,3 @@
+name "main"
+targetType "executable"
+dependency "sub:sub" version="*"
diff --git a/test/issue813-pure-sub-dependency/main/src/app.d b/test/issue813-pure-sub-dependency/main/src/app.d
new file mode 100644
index 0000000..0b416f0
--- /dev/null
+++ b/test/issue813-pure-sub-dependency/main/src/app.d
@@ -0,0 +1,6 @@
+import sub.test;
+
+void main()
+{
+	foo();
+}
diff --git a/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl b/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl
deleted file mode 100644
index 79f7d71..0000000
--- a/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl
+++ /dev/null
@@ -1,3 +0,0 @@
-name "main"
-targetType "executable"
-dependency "sub:sub" version="*"
diff --git a/test/issue813-pure-sub-dependency/main/~master/main/src/app.d b/test/issue813-pure-sub-dependency/main/~master/main/src/app.d
deleted file mode 100644
index 0b416f0..0000000
--- a/test/issue813-pure-sub-dependency/main/~master/main/src/app.d
+++ /dev/null
@@ -1,6 +0,0 @@
-import sub.test;
-
-void main()
-{
-	foo();
-}
diff --git a/test/issue813-pure-sub-dependency/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/dub.sdl
new file mode 100644
index 0000000..f8bdac6
--- /dev/null
+++ b/test/issue813-pure-sub-dependency/sub/dub.sdl
@@ -0,0 +1,3 @@
+name "sub"
+subPackage "sub/"
+dependency ":sub" version="*"
diff --git a/test/issue813-pure-sub-dependency/sub/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/sub/dub.sdl
new file mode 100644
index 0000000..a932e26
--- /dev/null
+++ b/test/issue813-pure-sub-dependency/sub/sub/dub.sdl
@@ -0,0 +1 @@
+name "sub"
diff --git a/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d b/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d
new file mode 100644
index 0000000..fe5bb2c
--- /dev/null
+++ b/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d
@@ -0,0 +1,6 @@
+module sub.test;
+
+void foo()
+{
+
+}
\ No newline at end of file
diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl
deleted file mode 100644
index f8bdac6..0000000
--- a/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl
+++ /dev/null
@@ -1,3 +0,0 @@
-name "sub"
-subPackage "sub/"
-dependency ":sub" version="*"
diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl
deleted file mode 100644
index a932e26..0000000
--- a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl
+++ /dev/null
@@ -1 +0,0 @@
-name "sub"
diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d b/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d
deleted file mode 100644
index fe5bb2c..0000000
--- a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d
+++ /dev/null
@@ -1,6 +0,0 @@
-module sub.test;
-
-void foo()
-{
-
-}
\ No newline at end of file
diff --git a/test/issue923-subpackage-deps.sh b/test/issue923-subpackage-deps.sh
index 67e137b..f3be79c 100755
--- a/test/issue923-subpackage-deps.sh
+++ b/test/issue923-subpackage-deps.sh
@@ -2,10 +2,13 @@
 
 . $(dirname "${BASH_SOURCE[0]}")/common.sh
 cd ${CURR_DIR}/issue923-subpackage-deps
-rm -f main/~master/main/dub.selections.json
-${DUB} build -f --bare --compiler=${DC} main
+rm -rf main/.dub
+rm -rf a/.dub
+rm -rf b/.dub
+rm -f main/dub.selections.json
+${DUB} build --bare --compiler=${DC} main
 
 
-if ! grep -c -e \"b\" main/~master/main/dub.selections.json; then
+if ! grep -c -e \"b\" main/dub.selections.json; then
 	die $LINENO 'Dependency b not resolved.'
 fi
diff --git a/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl b/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl
deleted file mode 100644
index 259eecf..0000000
--- a/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl
+++ /dev/null
@@ -1,13 +0,0 @@
-name "a"
-
-dependency ":foo" version="*"
-
-subPackage {
-	name "foo"
-	dependency "b" version="*"
-}
-
-subPackage {
-	name "bar"
-	dependency "a" version="*"
-}
\ No newline at end of file
diff --git a/test/issue923-subpackage-deps/a/dub.sdl b/test/issue923-subpackage-deps/a/dub.sdl
new file mode 100644
index 0000000..259eecf
--- /dev/null
+++ b/test/issue923-subpackage-deps/a/dub.sdl
@@ -0,0 +1,13 @@
+name "a"
+
+dependency ":foo" version="*"
+
+subPackage {
+	name "foo"
+	dependency "b" version="*"
+}
+
+subPackage {
+	name "bar"
+	dependency "a" version="*"
+}
\ No newline at end of file
diff --git a/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl b/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl
deleted file mode 100644
index c37c6fc..0000000
--- a/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl
+++ /dev/null
@@ -1 +0,0 @@
-name "b"
\ No newline at end of file
diff --git a/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d b/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d
deleted file mode 100644
index 5b09673..0000000
--- a/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d
+++ /dev/null
@@ -1,5 +0,0 @@
-module b;
-
-void test()
-{
-}
diff --git a/test/issue923-subpackage-deps/b/dub.sdl b/test/issue923-subpackage-deps/b/dub.sdl
new file mode 100644
index 0000000..c37c6fc
--- /dev/null
+++ b/test/issue923-subpackage-deps/b/dub.sdl
@@ -0,0 +1 @@
+name "b"
\ No newline at end of file
diff --git a/test/issue923-subpackage-deps/b/source/b.d b/test/issue923-subpackage-deps/b/source/b.d
new file mode 100644
index 0000000..5b09673
--- /dev/null
+++ b/test/issue923-subpackage-deps/b/source/b.d
@@ -0,0 +1,5 @@
+module b;
+
+void test()
+{
+}
diff --git a/test/issue923-subpackage-deps/main/dub.sdl b/test/issue923-subpackage-deps/main/dub.sdl
new file mode 100644
index 0000000..42865df
--- /dev/null
+++ b/test/issue923-subpackage-deps/main/dub.sdl
@@ -0,0 +1,2 @@
+name "main"
+dependency "a:bar" version="*"
diff --git a/test/issue923-subpackage-deps/main/source/app.d b/test/issue923-subpackage-deps/main/source/app.d
new file mode 100644
index 0000000..786e416
--- /dev/null
+++ b/test/issue923-subpackage-deps/main/source/app.d
@@ -0,0 +1,6 @@
+import b;
+
+void main()
+{
+	test();
+}
diff --git a/test/issue923-subpackage-deps/main/~master/main/dub.sdl b/test/issue923-subpackage-deps/main/~master/main/dub.sdl
deleted file mode 100644
index 42865df..0000000
--- a/test/issue923-subpackage-deps/main/~master/main/dub.sdl
+++ /dev/null
@@ -1,2 +0,0 @@
-name "main"
-dependency "a:bar" version="*"
diff --git a/test/issue923-subpackage-deps/main/~master/main/source/app.d b/test/issue923-subpackage-deps/main/~master/main/source/app.d
deleted file mode 100644
index 786e416..0000000
--- a/test/issue923-subpackage-deps/main/~master/main/source/app.d
+++ /dev/null
@@ -1,6 +0,0 @@
-import b;
-
-void main()
-{
-	test();
-}
diff --git a/test/use-c-sources/.no_build_gdc b/test/use-c-sources/.no_build_gdc
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/use-c-sources/.no_build_gdc