diff --git a/.gitignore b/.gitignore
index 0a7eb1e4f8cebe191c5871ee8ff4938ae6cff574..a0129822f5ee66e6b514ed2da2bb3835a22e757e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 bin
 obj
+.vs/
 *.suo
 *.user
 _ReSharper.*
@@ -14,8 +15,8 @@ artifacts
 StyleCop.Cache
 node_modules
 *.snk
-.build
 .nuget
 .r
 .deps
-global.json
\ No newline at end of file
+global.json
+korebuild-lock.txt
diff --git a/build.ps1 b/build.ps1
index d982c8fed0e132604a660a155b4be1a861b80e3f..d5eb4d5cf2799baf5807d24996744d87edcbbeb4 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,66 +1,177 @@
-$ErrorActionPreference = "Stop"
-
-function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries)
-{
-    while($true)
-    {
-        try
-        {
-            Invoke-WebRequest $url -OutFile $downloadLocation
-            break
-        }
-        catch
-        {
-            $exceptionMessage = $_.Exception.Message
-            Write-Host "Failed to download '$url': $exceptionMessage"
-            if ($retries -gt 0) {
-                $retries--
-                Write-Host "Waiting 10 seconds before retrying. Retries left: $retries"
-                Start-Sleep -Seconds 10
+#!/usr/bin/env powershell
+#requires -version 4
+
+<#
+.SYNOPSIS
+Build this repository
+
+.DESCRIPTION
+Downloads korebuild if required. Then builds the repository.
+
+.PARAMETER Path
+The folder to build. Defaults to the folder containing this script.
+
+.PARAMETER Channel
+The channel of KoreBuild to download. Overrides the value from the config file.
+
+.PARAMETER DotNetHome
+The directory where .NET Core tools will be stored.
+
+.PARAMETER ToolsSource
+The base url where build tools can be downloaded. Overrides the value from the config file.
+
+.PARAMETER Update
+Updates KoreBuild to the latest version even if a lock file is present.
+
+.PARAMETER ConfigFile
+The path to the configuration file that stores values. Defaults to version.xml.
+
+.PARAMETER MSBuildArgs
+Arguments to be passed to MSBuild
+
+.NOTES
+This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be.
+When the lockfile is not present, KoreBuild will create one using latest available version from $Channel.
+
+The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well.
+
+.EXAMPLE
+Example config file:
+```xml
+<!-- version.xml -->
+<Project>
+  <PropertyGroup>
+    <KoreBuildChannel>dev</KoreBuildChannel>
+    <KoreBuildToolsSource>https://aspnetcore.blob.core.windows.net/buildtools</KoreBuildToolsSource>
+  </PropertyGroup>
+</Project>
+```
+#>
+[CmdletBinding(PositionalBinding = $false)]
+param(
+    [string]$Path = $PSScriptRoot,
+    [Alias('c')]
+    [string]$Channel,
+    [Alias('d')]
+    [string]$DotNetHome,
+    [Alias('s')]
+    [string]$ToolsSource,
+    [Alias('u')]
+    [switch]$Update,
+    [string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'),
+    [Parameter(ValueFromRemainingArguments = $true)]
+    [string[]]$MSBuildArgs
+)
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = 'Stop'
+
+#
+# Functions
+#
+
+function Get-KoreBuild {
+
+    $lockFile = Join-Path $Path 'korebuild-lock.txt'
+
+    if (!(Test-Path $lockFile) -or $Update) {
+        Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile
+    }
 
+    $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
+    if (!$version) {
+        Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'"
+    }
+    $version = $version.TrimStart('version:').Trim()
+    $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version)
+
+    if (!(Test-Path $korebuildPath)) {
+        Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version"
+        New-Item -ItemType Directory -Path $korebuildPath | Out-Null
+        $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip"
+
+        try {
+            $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip"
+            Get-RemoteFile $remotePath $tmpfile
+            if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) {
+                # Use built-in commands where possible as they are cross-plat compatible
+                Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath
             }
-            else
-            {
-                $exception = $_.Exception
-                throw $exception
+            else {
+                # Fallback to old approach for old installations of PowerShell
+                Add-Type -AssemblyName System.IO.Compression.FileSystem
+                [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath)
             }
         }
+        catch {
+            Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore
+            throw
+        }
+        finally {
+            Remove-Item $tmpfile -ErrorAction Ignore
+        }
     }
+
+    return $korebuildPath
+}
+
+function Join-Paths([string]$path, [string[]]$childPaths) {
+    $childPaths | ForEach-Object { $path = Join-Path $path $_ }
+    return $path
 }
 
-cd $PSScriptRoot
+function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
+    if ($RemotePath -notlike 'http*') {
+        Copy-Item $RemotePath $LocalPath
+        return
+    }
+
+    $retries = 10
+    while ($retries -gt 0) {
+        $retries -= 1
+        try {
+            Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath
+            return
+        }
+        catch {
+            Write-Verbose "Request failed. $retries retries remaining"
+        }
+    }
 
-$repoFolder = $PSScriptRoot
-$env:REPO_FOLDER = $repoFolder
-$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
-if ($env:KOREBUILD_ZIP)
-{
-    $koreBuildZip=$env:KOREBUILD_ZIP
+    Write-Error "Download failed: '$RemotePath'."
 }
 
-$buildFolder = ".build"
-$buildFile="$buildFolder\KoreBuild.ps1"
+#
+# Main
+#
 
-if (!(Test-Path $buildFolder)) {
-    Write-Host "Downloading KoreBuild from $koreBuildZip"
+# Load configuration or set defaults
 
-    $tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid()
-    New-Item -Path "$tempFolder" -Type directory | Out-Null
+if (Test-Path $ConfigFile) {
+    [xml] $config = Get-Content $ConfigFile
+    if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' }
+    if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' }
+}
 
-    $localZipFile="$tempFolder\korebuild.zip"
+if (!$DotNetHome) {
+    $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } `
+        elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} `
+        elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}`
+        else { Join-Path $PSScriptRoot '.dotnet'}
+}
 
-    DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6
+if (!$Channel) { $Channel = 'dev' }
+if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' }
 
-    Add-Type -AssemblyName System.IO.Compression.FileSystem
-    [System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder)
+# Execute
 
-    New-Item -Path "$buildFolder" -Type directory | Out-Null
-    copy-item "$tempFolder\**\build\*" $buildFolder -Recurse
+$korebuildPath = Get-KoreBuild
+Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1')
 
-    # Cleanup
-    if (Test-Path $tempFolder) {
-        Remove-Item -Recurse -Force $tempFolder
-    }
+try {
+    Install-Tools $ToolsSource $DotNetHome
+    Invoke-RepositoryBuild $Path @MSBuildArgs
+}
+finally {
+    Remove-Module 'KoreBuild' -ErrorAction Ignore
 }
-
-&"$buildFile" @args
diff --git a/build.sh b/build.sh
index b0bcadb579286546bcd6be3ab4ae4a571132a7c1..d102c8f9054b59492c34ed9d5e3c15a7182c0f6a 100755
--- a/build.sh
+++ b/build.sh
@@ -1,46 +1,195 @@
 #!/usr/bin/env bash
-repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-cd $repoFolder
 
-koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
-if [ ! -z $KOREBUILD_ZIP ]; then
-    koreBuildZip=$KOREBUILD_ZIP
-fi
+set -euo pipefail
+
+#
+# variables
+#
 
-buildFolder=".build"
-buildFile="$buildFolder/KoreBuild.sh"
+RESET="\033[0m"
+RED="\033[0;31m"
+MAGENTA="\033[0;95m"
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+[ -z "${DOTNET_HOME:-}"] && DOTNET_HOME="$HOME/.dotnet"
+config_file="$DIR/version.xml"
+verbose=false
+update=false
+repo_path="$DIR"
+channel=''
+tools_source=''
 
-if test ! -d $buildFolder; then
-    echo "Downloading KoreBuild from $koreBuildZip"
+#
+# Functions
+#
+__usage() {
+    echo "Usage: $(basename ${BASH_SOURCE[0]}) [options] [[--] <MSBUILD_ARG>...]"
+    echo ""
+    echo "Arguments:"
+    echo "    <MSBUILD_ARG>...         Arguments passed to MSBuild. Variable number of arguments allowed."
+    echo ""
+    echo "Options:"
+    echo "    --verbose                Show verbose output."
+    echo "    -c|--channel <CHANNEL>   The channel of KoreBuild to download. Overrides the value from the config file.."
+    echo "    --config-file <FILE>     TThe path to the configuration file that stores values. Defaults to version.xml."
+    echo "    -d|--dotnet-home <DIR>   The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
+    echo "    --path <PATH>            The directory to build. Defaults to the directory containing the script."
+    echo "    -s|--tools-source <URL>  The base url where build tools can be downloaded. Overrides the value from the config file."
+    echo "    -u|--update              Update to the latest KoreBuild even if the lock file is present."
+    echo ""
+    echo "Description:"
+    echo "    This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be."
+    echo "    When the lockfile is not present, KoreBuild will create one using latest available version from \$channel."
 
-    tempFolder="/tmp/KoreBuild-$(uuidgen)"
-    mkdir $tempFolder
+    if [[ "${1:-}" != '--no-exit' ]]; then
+        exit 2
+    fi
+}
+
+get_korebuild() {
+    local lock_file="$repo_path/korebuild-lock.txt"
+    if [ ! -f $lock_file ] || [ "$update" = true ]; then
+        __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" $lock_file
+    fi
+    local version="$(grep 'version:*' -m 1 $lock_file)"
+    if [[ "$version" == '' ]]; then
+        __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'"
+        return 1
+    fi
+    version="$(echo ${version#version:} | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
+    local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version"
 
-    localZipFile="$tempFolder/korebuild.zip"
+    {
+        if [ ! -d "$korebuild_path" ]; then
+            mkdir -p "$korebuild_path"
+            local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip"
+            tmpfile="$(mktemp)"
+            echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}"
+            if __get_remote_file $remote_path $tmpfile; then
+                unzip -q -d "$korebuild_path" $tmpfile
+            fi
+            rm $tmpfile || true
+        fi
 
-    retries=6
-    until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null)
-    do
-        echo "Failed to download '$koreBuildZip'"
-        if [ "$retries" -le 0 ]; then
-            exit 1
+        source "$korebuild_path/KoreBuild.sh"
+    } || {
+        if [ -d "$korebuild_path" ]; then
+            echo "Cleaning up after failed installation"
+            rm -rf "$korebuild_path" || true
         fi
-        retries=$((retries - 1))
-        echo "Waiting 10 seconds before retrying. Retries left: $retries"
-        sleep 10s
-    done
+        return 1
+    }
+}
+
+__error() {
+    echo -e "${RED}$@${RESET}" 1>&2
+}
 
-    unzip -q -d $tempFolder $localZipFile
+__machine_has() {
+    hash "$1" > /dev/null 2>&1
+    return $?
+}
 
-    mkdir $buildFolder
-    cp -r $tempFolder/**/build/** $buildFolder
+__get_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+
+    if [[ "$remote_path" != 'http'* ]]; then
+        cp $remote_path $local_path
+        return 0
+    fi
 
-    chmod +x $buildFile
+    failed=false
+    if __machine_has curl ; then
+        curl --retry 10 -sSL -f --create-dirs -o $local_path $remote_path || failed=true
+    elif __machine_has wget; then
+        wget --tries 10 -O $local_path $remote_path || failed=true
+    else
+        failed=true
+    fi
 
-    # Cleanup
-    if test -d $tempFolder; then
-        rm -rf $tempFolder
+    if [ "$failed" = true ]; then
+        __error "Download failed: $remote_path" 1>&2
+        return 1
     fi
+}
+
+__read_dom () { local IFS=\> ; read -d \< ENTITY CONTENT ;}
+
+#
+# main
+#
+
+while [[ $# > 0 ]]; do
+    case $1 in
+        -\?|-h|--help)
+            __usage --no-exit
+            exit 0
+            ;;
+        -c|--channel|-Channel)
+            shift
+            channel=${1:-}
+            [ -z "$channel" ] && __usage
+            ;;
+        --config-file|-ConfigFile)
+            shift
+            config_file="${1:-}"
+            [ -z "$config_file" ] && __usage
+            ;;
+        -d|--dotnet-home|-DotNetHome)
+            shift
+            DOTNET_HOME=${1:-}
+            [ -z "$DOTNET_HOME" ] && __usage
+            ;;
+        --path|-Path)
+            shift
+            repo_path="${1:-}"
+            [ -z "$repo_path" ] && __usage
+            ;;
+        -s|--tools-source|-ToolsSource)
+            shift
+            tools_source="${1:-}"
+            [ -z "$tools_source" ] && __usage
+            ;;
+        -u|--update|-Update)
+            update=true
+            ;;
+        --verbose|-Verbose)
+            verbose=true
+            ;;
+        --)
+            shift
+            break
+            ;;
+        *)
+            break
+            ;;
+    esac
+    shift
+done
+
+if ! __machine_has unzip; then
+    __error 'Missing required command: unzip'
+    exit 1
 fi
 
-$buildFile -r $repoFolder "$@"
+if ! __machine_has curl && ! __machine_has wget; then
+    __error 'Missing required command. Either wget or curl is required.'
+    exit 1
+fi
+
+if [ -f $config_file ]; then
+    comment=false
+    while __read_dom; do
+        if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi
+        if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi
+        if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi
+        if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi
+    done < $config_file
+fi
+
+[ -z "$channel" ] && channel='dev'
+[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
+
+get_korebuild
+install_tools "$tools_source" "$DOTNET_HOME"
+invoke_repository_build "$repo_path" $@
diff --git a/build/RepositoryBuild.targets b/build/RepositoryBuild.targets
index 5099e9801d60076a52febeb1bfd5f8098a2a98b6..aabf59367f4e1852a89be176320e1dd627a14452 100644
--- a/build/RepositoryBuild.targets
+++ b/build/RepositoryBuild.targets
@@ -42,20 +42,14 @@
 
     <Message Text="============ Building $(RepositoryToBuild) ============" Importance="High" />
 
-    <!-- Copy Korebuild folder to individual repos to avoid downloading it again and again and also to
-          prevent from taking in newer Korebuild changes when Universe is building midway -->
-    <ItemGroup>
-      <_KorebuildItems Include="$(RepositoryRoot).build\**\*.*" />
-    </ItemGroup>
-
-    <Message Text="Copying Korebuild from Universe to repository $(BuildRepositoryRoot)"/>
-    <Copy SourceFiles="@(_KorebuildItems)" DestinationFolder="$(BuildRepositoryRoot).build\%(RecursiveDir)" SkipUnchangedFiles="true" UseHardlinksIfPossible="true" />
+    <!-- Copy Korebuild lock file to individual repos to align version if the repo doesn't already have one -->
+    <Message Text="Copying KoreBuild lockfile from Universe to repository $(BuildRepositoryRoot)"/>
+    <Copy SourceFiles="$(RepositoryRoot)korebuild-lock.txt" DestinationFolder="$(BuildRepositoryRoot)" />
 
     <Exec
-      Command="./$(_BuildScriptToExecute) $(BuildArguments)"
-      EnvironmentVariables="KOREBUILD_SKIP_RUNTIME_INSTALL=1"
+      Command="./$(_BuildScriptToExecute) -Path $(BuildRepositoryRoot) $(BuildArguments)"
       IgnoreStandardErrorWarningFormat="true"
-      WorkingDirectory="$(BuildRepositoryRoot)" />
+      WorkingDirectory="$(RepositoryRoot)" />
 
     <ItemGroup>
       <RepositoryArtifacts Include="$(RepositoryArtifactsBuildDirectory)*" />
@@ -75,8 +69,10 @@
     <Message Text="Publishing the following packages to the volatile feed: @(RepositoryNupkgs -> '%(Filename)%(Extension)', ', ')"
       Condition="'$(PublishPackages)'=='true' AND '@(RepositoryNupkgs)' != ''" />
 
-    <Exec
-      Command="$(DotNetPath) $(PackagePublisherNetCoreApp) -d $(RepositoryArtifactsBuildDirectory) -f $(NuGetPublishVolatileFeed)"
+    <PushNuGetPackages
+      Packages="@(RepositoryNupkgs)"
+      Feed="$(NuGetPublishVolatileFeed)"
+      ApiKey="$(APIKey)"
       Condition="'$(PublishPackages)'=='true' AND '@(RepositoryNupkgs)' != ''" />
 
     <Message Text="============ Done building $(RepositoryToBuild) ============" Importance="High" />
diff --git a/build/repo.targets b/build/repo.targets
index df2b9e68b72cfeee1d529a26978af95acde1541f..6e3e9ebd9b1ce57f82fcc7c3ab8eef0b39e44802 100644
--- a/build/repo.targets
+++ b/build/repo.targets
@@ -23,10 +23,6 @@
     <BuildDependsOn>$(BuildDependsOn);CloneRepositories;BuildRepositories</BuildDependsOn>
   </PropertyGroup>
 
-  <ItemGroup>
-    <PackageReference Include="PackagePublisher" Version="2.0.0-*" />
-  </ItemGroup>
-
   <Import Project="$(_RepositoryListToImport)" />
 
   <Target Name="CleanUniverseArtifacts">
@@ -111,7 +107,7 @@
   </Target>
 
   <Target Name="BuildRepositories"
-     DependsOnTargets="_PrepareRepositories;_FindDotNetPath;_CreateRepositoriesListWithCommits;_UpdateNuGetConfig;_GenerateBuildGraph;_BuildRepositories" />
+     DependsOnTargets="_PrepareRepositories;_CreateRepositoriesListWithCommits;_UpdateNuGetConfig;_GenerateBuildGraph;_BuildRepositories" />
 
   <Target Name="_PrepareRestoreGraphSpecs" DependsOnTargets="_PrepareRepositories">
     <ItemGroup>
diff --git a/version.xml b/version.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3c05022b7d7d810d34304bafeec2a7b6ec1ef4bc
--- /dev/null
+++ b/version.xml
@@ -0,0 +1,8 @@
+<!-- This file may be overwritten by automation. -->
+<Project>
+  <PropertyGroup>
+    <KoreBuildChannel>dev</KoreBuildChannel>
+    <VersionPrefix>2.1.0</VersionPrefix>
+    <VersionSuffix>preview1</VersionSuffix>
+  </PropertyGroup>
+</Project>