diff --git a/test/NuGet.Config b/NuGet.Config
similarity index 100%
rename from test/NuGet.Config
rename to NuGet.Config
diff --git a/test-msbuild/NuGet.Config b/NuGet.withDevFeed.Config
similarity index 74%
rename from test-msbuild/NuGet.Config
rename to NuGet.withDevFeed.Config
index 5d8cd8b..dc288e2 100644
--- a/test-msbuild/NuGet.Config
+++ b/NuGet.withDevFeed.Config
@@ -3,6 +3,5 @@
-
diff --git a/build.cmd b/build.cmd
index aa85df8..731409b 100644
--- a/build.cmd
+++ b/build.cmd
@@ -1,5 +1,4 @@
@echo off
-powershell -NoProfile -NoLogo -Command "%~dp0scripts\build.ps1 %*; exit $LastExitCode;"
+powershell -NoProfile -NoLogo -Command "%~dp0\run-build.ps1 %*; exit $LastExitCode;"
if %errorlevel% neq 0 exit /b %errorlevel%
-
diff --git a/build.proj b/build.proj
index 742b165..3166511 100644
--- a/build.proj
+++ b/build.proj
@@ -24,9 +24,20 @@
.exe
+
+ $(RepoRoot)/packages
+ $(RepoRoot)/artifacts
-
+
+ 1.0.0-beta-060000
+ 1.0.0-beta-060000
+
+
+
+
+
+
-
diff --git a/build/Build.targets b/build/Build.targets
new file mode 100644
index 0000000..c88096c
--- /dev/null
+++ b/build/Build.targets
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/Test.targets b/build/Test.targets
new file mode 100644
index 0000000..d409d14
--- /dev/null
+++ b/build/Test.targets
@@ -0,0 +1,63 @@
+
+
+
+
+
+ <_CommandText>
+
+
+
+
+
+
+
+
+
+
+ $(FSharpSdkVersion)
+ $(FSharpNETSdkVersion)
+
+
+
+ <_NupkgsToTest Include="$(ArtifactsDir)/nupkgs/FSharp.Sdk.$(FSharpSdkVersionToTest).nupkg" />
+ <_NupkgsToTest Include="$(ArtifactsDir)/nupkgs/FSharp.NET.Sdk.$(FSharpNETSdkVersionToTest).nupkg" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test-msbuild/TestAppConsole/Program.fs b/examples/preview2.1/console/Program.fs
similarity index 78%
rename from test-msbuild/TestAppConsole/Program.fs
rename to examples/preview2.1/console/Program.fs
index a4928de..d5fd26e 100644
--- a/test-msbuild/TestAppConsole/Program.fs
+++ b/examples/preview2.1/console/Program.fs
@@ -4,5 +4,5 @@ open System
[]
let main argv =
- printfn "Hello World from F#!"
+ printfn "Hello World!"
0 // return an integer exit code
diff --git a/examples/preview2.1/console/project.json b/examples/preview2.1/console/project.json
new file mode 100644
index 0000000..4124075
--- /dev/null
+++ b/examples/preview2.1/console/project.json
@@ -0,0 +1,27 @@
+{
+ "version": "1.0.0-*",
+ "buildOptions": {
+ "debugType": "portable",
+ "emitEntryPoint": true,
+ "compilerName": "fsc",
+ "compile": {
+ "includeFiles": [
+ "Program.fs"
+ ]
+ }
+ },
+ "tools": {
+ "dotnet-compile-fsc": "1.0.0-preview2.1-*"
+ },
+ "frameworks": {
+ "netcoreapp1.1": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.1.0"
+ },
+ "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
+ }
+ }
+ }
+}
diff --git a/examples/preview3/lib/Library.fs b/examples/preview2.1/lib/Library.fs
similarity index 100%
rename from examples/preview3/lib/Library.fs
rename to examples/preview2.1/lib/Library.fs
diff --git a/test/TestLibrary/project.json b/examples/preview2.1/lib/project.json
similarity index 50%
rename from test/TestLibrary/project.json
rename to examples/preview2.1/lib/project.json
index 95eaad3..7bb4132 100644
--- a/test/TestLibrary/project.json
+++ b/examples/preview2.1/lib/project.json
@@ -5,25 +5,19 @@
"compilerName": "fsc",
"compile": {
"includeFiles": [
- "Helper2.fs",
- "Helper.fs"
+ "Library.fs"
]
}
},
- "dependencies": {
- "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*"
+ "tools": {
+ "dotnet-compile-fsc": "1.0.0-preview2.1-*"
},
"frameworks": {
"netstandard1.6": {
"dependencies": {
- "NETStandard.Library": "1.6.0"
+ "NETStandard.Library": "1.6.1",
+ "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629"
}
}
- },
- "tools": {
- "dotnet-compile-fsc": {
- "version": "1.0.0-preview2-*",
- "imports": "dnxcore50"
- }
}
}
diff --git a/scripts/build.ps1 b/run-build.ps1
similarity index 71%
rename from scripts/build.ps1
rename to run-build.ps1
index 41dfa34..c55501d 100644
--- a/scripts/build.ps1
+++ b/run-build.ps1
@@ -17,16 +17,18 @@ if($Help)
}
#make path absolute
-$rootDir = Split-Path -parent (Split-Path -parent $PSCommandPath)
+$RepoRoot = "$PSScriptRoot"
+
+$sdkVersion = '1.0.0-rc3-004530'
function Install-DotnetSdk([string] $sdkVersion)
{
Write-Host "# Install .NET Core Sdk versione '$sdkVersion'" -foregroundcolor "magenta"
- $sdkInstallScriptUrl = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview3/scripts/obtain/dotnet-install.ps1"
+ $sdkInstallScriptUrl = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-rc3/scripts/obtain/dotnet-install.ps1"
$sdkInstallScriptPath = ".dotnetsdk\dotnet_cli_install.ps1"
Write-Host "Downloading sdk install script '$sdkInstallScriptUrl' to '$sdkInstallScriptPath'"
- New-Item "$rootDir\.dotnetsdk" -Type directory -ErrorAction Ignore
- Invoke-WebRequest $sdkInstallScriptUrl -OutFile "$rootDir\$sdkInstallScriptPath"
+ New-Item "$RepoRoot\.dotnetsdk" -Type directory -ErrorAction Ignore
+ Invoke-WebRequest $sdkInstallScriptUrl -OutFile "$RepoRoot\$sdkInstallScriptPath"
Write-Host "Running sdk install script..."
./.dotnetsdk/dotnet_cli_install.ps1 -InstallDir ".dotnetsdk\sdk-$sdkVersion" -Channel "preview" -version $sdkVersion
@@ -45,28 +47,16 @@ function Run-Cmd
function Using-Sdk ([string] $sdkVersion)
{
- $sdkPath = "$rootDir\.dotnetsdk\sdk-$sdkVersion"
+ $sdkPath = "$RepoRoot\.dotnetsdk\sdk-$sdkVersion"
Write-Host "# Using sdk '$sdkVersion'" -foregroundcolor "magenta"
$env:Path = "$sdkPath;$env:Path"
Run-Cmd "dotnet" "--version"
}
-function Do-preview3
-{
- Install-DotnetSdk '1.0.0-preview3-004056'
-
- Using-Sdk '1.0.0-preview3-004056'
-
- dotnet msbuild build.proj /m /p:Architecture=$Architecture $ExtraParameters
- if ($LASTEXITCODE -ne 0) { throw "Failed to build" }
-}
-
# main
-try {
- Push-Location $PWD
+Install-DotnetSdk $sdkVersion
- Do-preview3
-}
-finally {
- Pop-Location
-}
+Using-Sdk $sdkVersion
+
+dotnet msbuild build.proj /m /v:diag /p:Architecture=$Architecture $ExtraParameters
+if ($LASTEXITCODE -ne 0) { throw "Failed to build" }
diff --git a/run-build.sh b/run-build.sh
index d7c42ca..5ce1ae8 100755
--- a/run-build.sh
+++ b/run-build.sh
@@ -140,13 +140,13 @@ if [ ! -d ".dotnetsdk" ]; then
mkdir ".dotnetsdk"
fi
-sdkInstallScriptUrl=https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview3/scripts/obtain/dotnet-install.sh
-sdkInstallScriptPath=$REPOROOT/.dotnetsdk/dotnet_cli_install.ps1
+sdkInstallScriptUrl=https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-rc3/scripts/obtain/dotnet-install.sh
+sdkInstallScriptPath=$REPOROOT/.dotnetsdk/dotnet_cli_install.sh
download $sdkInstallScriptUrl $sdkInstallScriptPath
chmod u+x $sdkInstallScriptPath
-sdkVersion=1.0.0-preview3-004056
+sdkVersion=1.0.0-rc3-004530
sdkPath=$REPOROOT/.dotnetsdk/sdk-$sdkVersion
DOTNET_INSTALL_DIR=$sdkPath
diff --git a/scripts/docker/centos/Dockerfile b/scripts/docker/centos/Dockerfile
index ae3e87d..6bba0fb 100644
--- a/scripts/docker/centos/Dockerfile
+++ b/scripts/docker/centos/Dockerfile
@@ -6,6 +6,10 @@
# Dockerfile that creates a container suitable to build dotnet-cli
FROM centos:7.1.1503
+# Swap the "fakesystemd" package with the real "systemd" package, because fakesystemd conflicts with openssl-devel.
+# The CentOS Docker image uses fakesystemd instead of systemd to reduce disk space.
+RUN yum -q -y swap -- remove fakesystemd -- install systemd systemd-libs
+
RUN yum -q -y install deltarpm
RUN yum -q -y install epel-release
# RUN yum -y update
diff --git a/scripts/dockerrun.sh b/scripts/dockerrun.sh
index 4c289cc..b478a2a 100755
--- a/scripts/dockerrun.sh
+++ b/scripts/dockerrun.sh
@@ -132,5 +132,7 @@ docker run $INTERACTIVE -t --rm --sig-proxy=true \
-e CHECKSUM_STORAGE_KEY \
-e CHECKSUM_STORAGE_ACCOUNT \
-e CHECKSUM_STORAGE_CONTAINER \
+ -e CLIBUILD_SKIP_TESTS \
+ -e CommitCount \
$DOTNET_BUILD_CONTAINER_TAG \
$BUILD_COMMAND "$@"
diff --git a/scripts/run-tests.ps1 b/scripts/run-tests.ps1
deleted file mode 100644
index 869bff6..0000000
--- a/scripts/run-tests.ps1
+++ /dev/null
@@ -1,126 +0,0 @@
-$global:testSuite = @{}
-
-# test helper
-
-function Run-Test {
- Param([string] $Name, [scriptblock] $Check)
-
- Write-Host "## Testing $Name..." -ForegroundColor "magenta"
- try {
- $Check.Invoke()
- Write-Host "## Testing $Name [OK]" -ForegroundColor "green"
- $global:testSuite.Add($Name, $TRUE)
- }
- catch {
- Write-Host "## Testing $Name [FAILED]" -ForegroundColor "red"
- Write-Host $_.Exception.Message
- $global:testSuite.Add($Name, $FALSE)
- }
-}
-
-function Dotnet-Build {
- Run-Cmd "dotnet" "--verbose build"
-}
-
-function Dotnet-Run {
- Param([string] $Arguments)
-
- Run-Cmd "dotnet" "--verbose run $Arguments"
-}
-
-# dotnet new
-<#
-
-dotnet new doesnt work ootb in preview2
-
-Run-Test "dotnet new" {
-
- Remove-Item "$rootDir\test\test-dotnet-new" -Recurse -ErrorAction Ignore
-
- mkdir "$rootDir\test\test-dotnet-new" -Force | cd
-
- Run-Cmd "dotnet" "new --lang f#"
-
- Run-Cmd "dotnet" "restore -f `"$rootDir\bin`""
-
- Dotnet-Build
-
- Dotnet-Run "c d"
-}
-#>
-
-# test from assets
-
-function Dotnet-Restore {
- Run-Cmd "dotnet" "restore -v Information -f `"$rootDir\bin`" --configfile `"$rootDir\test\NuGet.Config`""
-}
-
-Run-Test "test/TestAppWithArgs" {
-
- cd "$rootDir\test\TestAppWithArgs"
-
- Dotnet-Restore
-
- Dotnet-Build
-
- Dotnet-Run ""
-}
-
-Run-Test "test/TestLibrary" {
-
- cd "$rootDir\test\TestLibrary"
-
- Dotnet-Restore
-
- Dotnet-Build
-}
-
-Run-Test "test/TestApp" {
-
- cd "$rootDir\test\TestApp"
-
- Dotnet-Restore
-
- Dotnet-Build
-
- Dotnet-Run ""
-}
-
-# test templates
-
-function Dotnet-Restore-OnlyFallback {
- Run-Cmd "dotnet" "restore -v Information -f `"$rootDir\bin`""
-}
-
-Run-Test "examples/preview2/console" {
-
- cd "$rootDir\examples\preview2\console"
-
- Dotnet-Restore-OnlyFallback
-
- Dotnet-Build
-
- Dotnet-Run ""
-}
-
-Run-Test "examples/preview2/lib" {
-
- cd "$rootDir\examples\preview2\lib"
-
- Dotnet-Restore-OnlyFallback
-
- Dotnet-Build
-}
-
-# results
-
-Write-Host "# Tests results" -ForegroundColor "magenta"
-foreach ($h in $global:testSuite.GetEnumerator()) {
- $color = If ($h.Value) {"green"} Else {"red"}
- $text = If ($h.Value) {"PASSED"} Else {"FAILED"}
- Write-Host "- $($h.Name): [$text]" -ForegroundColor $color
-}
-
-If ($global:testSuite.ContainsValue($FALSE)) {
- exit 2
-}
diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj b/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj
new file mode 100644
index 0000000..1de4a99
--- /dev/null
+++ b/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj
@@ -0,0 +1,40 @@
+
+
+
+ netstandard1.3;net40
+
+ FSharp.NET.Sdk
+
+ FSharp.NET.Sdk
+ Enrico Sada
+ F# and .NET Core SDK working together
+ Compatible with .NET Core Sdk preview4/rc3
+ f#;sdk;fsharp;msbuild
+ https://github.com/dotnet/netcorecli-fsc
+ git
+ https://github.com/dotnet/netcorecli-fsc
+
+ false
+ true
+
+
+
+
+ .
+ true
+
+
+ .
+ true
+
+
+ .
+ true
+
+
+ .
+ true
+
+
+
+
diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets b/src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets
similarity index 92%
rename from src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets
rename to src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets
index b778491..9e56153 100644
--- a/src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets
+++ b/src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets
@@ -123,12 +123,18 @@ this file.
$(_IntermediateOutputPathFull)dotnet-compile.rsp
+
+
+ <_DotNetHostExecutableName>$(MSBuildExtensionsPath)/../../dotnet
+ <_DotNetHostExecutableName Condition=" '$(OS)' == 'Windows_NT' ">$(_DotNetHostExecutableName).exe
-
+
diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Current.Sdk.targets b/src/FSharp.NET.Sdk/build/FSharp.NET.Current.Sdk.targets
similarity index 100%
rename from src/FSharp.NET.Sdk/FSharp.NET.Current.Sdk.targets
rename to src/FSharp.NET.Sdk/build/FSharp.NET.Current.Sdk.targets
diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.props b/src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props
similarity index 77%
rename from src/FSharp.NET.Sdk/FSharp.NET.Sdk.props
rename to src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props
index 19a7a74..d0c5f60 100644
--- a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.props
+++ b/src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props
@@ -14,6 +14,13 @@ WARNING: You CAN MODIFY this file, doesnt matter if you are not knowledgeable a
4
+
+
+ false
+
+
+ {F2A71F9B-5D33-465A-A702-920D77279786}
+
$(MSBuildThisFileDirectory)\FSharp.NET.Current.Sdk.targets
diff --git a/src/FSharp.NET.Sdk/FSharp.NET.CrossTargeting.Sdk.targets b/src/FSharp.NET.Sdk/buildCrossTargeting/FSharp.NET.CrossTargeting.Sdk.targets
similarity index 100%
rename from src/FSharp.NET.Sdk/FSharp.NET.CrossTargeting.Sdk.targets
rename to src/FSharp.NET.Sdk/buildCrossTargeting/FSharp.NET.CrossTargeting.Sdk.targets
diff --git a/src/FSharp.NET.Sdk/project.json b/src/FSharp.NET.Sdk/project.json
deleted file mode 100644
index 8fddaae..0000000
--- a/src/FSharp.NET.Sdk/project.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "version": "1.0.0-beta-*",
- "frameworks": {
- "netstandard1.3": {}
- },
- "packOptions": {
- "summary": "The .NET Sdk preview4 and F# integration",
- "tags": [ "f#", "sdk", "fsharp", "msbuild" ],
- "owners": [ "Enrico Sada" ],
- "releaseNotes": "use Sdk attribute",
- "projectUrl": "https://github.com/dotnet/netcorecli-fsc",
- "requireLicenseAcceptance": false,
- "repository": {
- "type": "git",
- "url": "https://github.com/dotnet/netcorecli-fsc"
- },
- "files": {
- "mappings": {
- "Sdk/Sdk.props": "Sdk/Sdk.props",
- "Sdk/Sdk.OnRestore.targets": "Sdk/Sdk.OnRestore.targets",
- "Sdk/Sdk.targets": "Sdk/Sdk.targets",
- "buildCrossTargeting/FSharp.NET.Sdk.targets": "FSharp.NET.CrossTargeting.Sdk.targets",
- "build/FSharp.NET.Sdk.props": "FSharp.NET.Sdk.props",
- "build/FSharp.NET.Core.Sdk.targets": "FSharp.NET.Core.Sdk.targets",
- "build/FSharp.NET.Current.Sdk.targets": "FSharp.NET.Current.Sdk.targets"
- }
- }
- }
-}
diff --git a/src/FSharp.Sdk/FSharp.Sdk.csproj b/src/FSharp.Sdk/FSharp.Sdk.csproj
new file mode 100644
index 0000000..9f1b2dd
--- /dev/null
+++ b/src/FSharp.Sdk/FSharp.Sdk.csproj
@@ -0,0 +1,36 @@
+
+
+
+ netstandard1.3
+
+ FSharp.Sdk
+
+ FSharp.Sdk
+ Enrico Sada
+ SDK for F# bundled inside .NET Core SDK
+ Compatible with .NET Core Sdk preview4
+ f#;sdk;fsharp;msbuild
+ https://github.com/dotnet/netcorecli-fsc
+ git
+ https://github.com/dotnet/netcorecli-fsc
+
+ false
+ true
+
+
+
+
+ .
+ true
+
+
+ .
+ true
+
+
+ .
+ true
+
+
+
+
diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.OnRestore.targets b/src/FSharp.Sdk/Sdk/Sdk.OnRestore.targets
similarity index 100%
rename from src/FSharp.NET.Sdk/Sdk/Sdk.OnRestore.targets
rename to src/FSharp.Sdk/Sdk/Sdk.OnRestore.targets
diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.props b/src/FSharp.Sdk/Sdk/Sdk.props
similarity index 100%
rename from src/FSharp.NET.Sdk/Sdk/Sdk.props
rename to src/FSharp.Sdk/Sdk/Sdk.props
diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.targets b/src/FSharp.Sdk/Sdk/Sdk.targets
similarity index 100%
rename from src/FSharp.NET.Sdk/Sdk/Sdk.targets
rename to src/FSharp.Sdk/Sdk/Sdk.targets
diff --git a/test-msbuild/TestApp/TestApp.fsproj b/test-msbuild/TestApp/TestApp.fsproj
deleted file mode 100644
index 84faa16..0000000
--- a/test-msbuild/TestApp/TestApp.fsproj
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
- Exe
- netcoreapp1.0
-
-
-
- 1.0.0-preview3
- Enrico Sada
-
-
-
-
-
-
-
-
-
- 1.0.1
-
-
- $(MicrosoftFSharpCorenetcoreVersion)
-
-
- $(MicrosoftNETSdkVersion)
- All
-
-
- $(FSharpNETSdkVersion)
- All
-
-
- TestLibrary
-
-
-
-
-
- 1.0.0-preview2-020000
-
-
-
-
-
-
diff --git a/test-msbuild/TestAppConsole/TestAppConsole.fsproj b/test-msbuild/TestAppConsole/TestAppConsole.fsproj
deleted file mode 100644
index 0905d8a..0000000
--- a/test-msbuild/TestAppConsole/TestAppConsole.fsproj
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
- Exe
- netcoreapp1.0
-
-
-
- 1.0.0-preview3
- Enrico Sada
-
-
-
-
-
-
-
-
-
- 1.0.1
-
-
- $(MicrosoftFSharpCorenetcoreVersion)
-
-
- $(MicrosoftNETSdkVersion)
- All
-
-
- $(FSharpNETSdkVersion)
- All
-
-
-
-
-
- 1.0.0-preview2-020000
-
-
-
-
-
-
diff --git a/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj b/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj
deleted file mode 100644
index c46649d..0000000
--- a/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
- Exe
- netcoreapp1.0
-
-
-
- 1.0.0-preview3
- Enrico Sada
-
-
-
-
-
-
-
-
-
- 1.0.1
-
-
- $(MicrosoftFSharpCorenetcoreVersion)
-
-
- $(MicrosoftNETSdkVersion)
- All
-
-
-
-
-
- 1.0.0-preview2-020005
-
-
-
-
-
-
-
diff --git a/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs b/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs
deleted file mode 100644
index a4928de..0000000
--- a/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs
+++ /dev/null
@@ -1,8 +0,0 @@
-// Learn more about F# at http://fsharp.org
-
-open System
-
-[]
-let main argv =
- printfn "Hello World from F#!"
- 0 // return an integer exit code
diff --git a/test-msbuild/TestLibrary/TestLibrary.fsproj b/test-msbuild/TestLibrary/TestLibrary.fsproj
deleted file mode 100644
index 86f57cf..0000000
--- a/test-msbuild/TestLibrary/TestLibrary.fsproj
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
- netstandard1.6
-
-
-
- 1.0.0-preview3
- Enrico Sada
-
-
-
-
-
-
-
-
-
-
- 1.6.0
-
-
- $(MicrosoftFSharpCorenetcoreVersion)
-
-
- $(MicrosoftNETSdkVersion)
- All
-
-
- $(FSharpNETSdkVersion)
- All
-
-
-
-
-
- 1.0.0-preview2-020000
-
-
-
-
-
-
diff --git a/test-msbuild/TestXunit/TestXunit.fsproj b/test-msbuild/TestXunit/TestXunit.fsproj
deleted file mode 100644
index 81e63ea..0000000
--- a/test-msbuild/TestXunit/TestXunit.fsproj
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
- Exe
- netcoreapp1.0
-
-
-
-
-
-
-
-
-
- 1.0.1
-
-
- $(MicrosoftNETSdkVersion)
- All
-
-
- $(MicrosoftFSharpCorenetcoreVersion)
-
-
- $(FSharpNETSdkVersion)
- All
-
-
- 15.0.0-preview-20161024-02
-
-
- 2.2.0-beta3-build3402
-
-
- 2.2.0-beta4-build1188
-
-
-
-
-
- 1.0.0-preview2-020000
-
-
-
-
-
-
diff --git a/test-msbuild/TestXunit/Tests.fs b/test-msbuild/TestXunit/Tests.fs
deleted file mode 100644
index 7d11a64..0000000
--- a/test-msbuild/TestXunit/Tests.fs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Tests
-
-open System
-open Xunit
-
-[]
-let ``My test`` () =
- Assert.True(true)
diff --git a/test/.vscode/launch.json b/test/.vscode/launch.json
new file mode 100644
index 0000000..5e99c49
--- /dev/null
+++ b/test/.vscode/launch.json
@@ -0,0 +1,23 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceRoot}/dotnet-new.Tests/bin/Debug/netcoreapp1.0/dotnet-new.Tests.dll",
+ "args": [],
+ "cwd": "${workspaceRoot}",
+ "externalConsole": false,
+ "stopAtEntry": false,
+ "internalConsoleOptions": "openOnSessionStart"
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command.pickProcess}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/.vscode/tasks.json b/test/.vscode/tasks.json
new file mode 100644
index 0000000..dd93bdb
--- /dev/null
+++ b/test/.vscode/tasks.json
@@ -0,0 +1,24 @@
+{
+ "version": "0.1.0",
+ "command": "dotnet",
+ "isShellCommand": true,
+ "args": [],
+ "tasks": [
+ {
+ "taskName": "build",
+ "args": [
+ "${workspaceRoot}/dotnet-new.Tests/dotnet-new.Tests.csproj"
+ ],
+ "isBuildCommand": true,
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "taskName": "test",
+ "args": [
+ "${workspaceRoot}/dotnet-new.Tests/dotnet-new.Tests.csproj"
+ ],
+ "isBuildCommand": true,
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/CompileFailApp/project.json b/test/CompileFailApp/project.json
deleted file mode 100644
index e29c140..0000000
--- a/test/CompileFailApp/project.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "version": "1.0.0-*",
- "buildOptions": {
- "emitEntryPoint": true,
- "compilerName": "fsc",
- "compile": {
- "includeFiles": [
- "Program.fs"
- ]
- }
- },
- "dependencies": {
- "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160509",
- "Microsoft.NETCore.App": "1.0.0"
- },
- "tools": {
- "dotnet-compile-fsc": {
- "version": "1.0.0-preview2-*",
- "imports": [
- "dnxcore50",
- "portable-net45+win81",
- "netstandard1.3"
- ]
- }
- },
- "frameworks": {
- "netcoreapp1.0": {
- "imports": [
- "dnxcore50",
- "netstandard1.3"
- ]
- }
- },
- "runtimes": {
- "win7-x64": {},
- "win7-x86": {},
- "osx.10.10-x64": {},
- "osx.10.11-x64": {},
- "ubuntu.14.04-x64": {},
- "ubuntu.16.04-x64": {},
- "centos.7-x64": {},
- "rhel.7.2-x64": {},
- "debian.8-x64": {},
- "fedora.23-x64": {},
- "opensuse.13.2-x64": {}
- }
-}
diff --git a/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs
new file mode 100644
index 0000000..08c617e
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs
@@ -0,0 +1,52 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class AnsiColorExtensions
+ {
+ public static string Black(this string text)
+ {
+ return "\x1B[30m" + text + "\x1B[39m";
+ }
+
+ public static string Red(this string text)
+ {
+ return "\x1B[31m" + text + "\x1B[39m";
+ }
+ public static string Green(this string text)
+ {
+ return "\x1B[32m" + text + "\x1B[39m";
+ }
+
+ public static string Yellow(this string text)
+ {
+ return "\x1B[33m" + text + "\x1B[39m";
+ }
+
+ public static string Blue(this string text)
+ {
+ return "\x1B[34m" + text + "\x1B[39m";
+ }
+
+ public static string Magenta(this string text)
+ {
+ return "\x1B[35m" + text + "\x1B[39m";
+ }
+
+ public static string Cyan(this string text)
+ {
+ return "\x1B[36m" + text + "\x1B[39m";
+ }
+
+ public static string White(this string text)
+ {
+ return "\x1B[37m" + text + "\x1B[39m";
+ }
+
+ public static string Bold(this string text)
+ {
+ return "\x1B[1m" + text + "\x1B[22m";
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs b/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs
new file mode 100644
index 0000000..308b226
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs
@@ -0,0 +1,145 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class AnsiConsole
+ {
+ private AnsiConsole(TextWriter writer)
+ {
+ Writer = writer;
+
+ OriginalForegroundColor = Console.ForegroundColor;
+ }
+
+ private int _boldRecursion;
+
+ public static AnsiConsole GetOutput()
+ {
+ return new AnsiConsole(Console.Out);
+ }
+
+ public static AnsiConsole GetError()
+ {
+ return new AnsiConsole(Console.Error);
+ }
+
+ public TextWriter Writer { get; }
+
+ public ConsoleColor OriginalForegroundColor { get; }
+
+ private void SetColor(ConsoleColor color)
+ {
+ const int Light = 0x08;
+ int c = (int)color;
+
+ Console.ForegroundColor =
+ c < 0 ? color : // unknown, just use it
+ _boldRecursion > 0 ? (ConsoleColor)(c | Light) : // ensure color is light
+ (ConsoleColor)(c & ~Light); // ensure color is dark
+ }
+
+ private void SetBold(bool bold)
+ {
+ _boldRecursion += bold ? 1 : -1;
+ if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold))
+ {
+ return;
+ }
+
+ // switches on _boldRecursion to handle boldness
+ SetColor(Console.ForegroundColor);
+ }
+
+ public void WriteLine(string message)
+ {
+ Write(message);
+ Writer.WriteLine();
+ }
+
+
+ public void Write(string message)
+ {
+ var escapeScan = 0;
+ for (;;)
+ {
+ var escapeIndex = message.IndexOf("\x1b[", escapeScan, StringComparison.Ordinal);
+ if (escapeIndex == -1)
+ {
+ var text = message.Substring(escapeScan);
+ Writer.Write(text);
+ break;
+ }
+ else
+ {
+ var startIndex = escapeIndex + 2;
+ var endIndex = startIndex;
+ while (endIndex != message.Length &&
+ message[endIndex] >= 0x20 &&
+ message[endIndex] <= 0x3f)
+ {
+ endIndex += 1;
+ }
+
+ var text = message.Substring(escapeScan, escapeIndex - escapeScan);
+ Writer.Write(text);
+ if (endIndex == message.Length)
+ {
+ break;
+ }
+
+ switch (message[endIndex])
+ {
+ case 'm':
+ int value;
+ if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out value))
+ {
+ switch (value)
+ {
+ case 1:
+ SetBold(true);
+ break;
+ case 22:
+ SetBold(false);
+ break;
+ case 30:
+ SetColor(ConsoleColor.Black);
+ break;
+ case 31:
+ SetColor(ConsoleColor.Red);
+ break;
+ case 32:
+ SetColor(ConsoleColor.Green);
+ break;
+ case 33:
+ SetColor(ConsoleColor.Yellow);
+ break;
+ case 34:
+ SetColor(ConsoleColor.Blue);
+ break;
+ case 35:
+ SetColor(ConsoleColor.Magenta);
+ break;
+ case 36:
+ SetColor(ConsoleColor.Cyan);
+ break;
+ case 37:
+ SetColor(ConsoleColor.Gray);
+ break;
+ case 39:
+ Console.ForegroundColor = OriginalForegroundColor;
+ break;
+ }
+ }
+ break;
+ }
+
+ escapeScan = endIndex + 1;
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs b/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs
new file mode 100644
index 0000000..c8469a9
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs
@@ -0,0 +1,200 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class ArgumentEscaper
+ {
+ ///
+ /// Undo the processing which took place to create string[] args in Main,
+ /// so that the next process will receive the same string[] args
+ ///
+ /// See here for more info:
+ /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
+ ///
+ ///
+ ///
+ public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable args)
+ {
+ return string.Join(" ", EscapeArgArray(args));
+ }
+
+ ///
+ /// Undo the processing which took place to create string[] args in Main,
+ /// so that the next process will receive the same string[] args
+ ///
+ /// See here for more info:
+ /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
+ ///
+ ///
+ ///
+ public static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable args)
+ {
+ return string.Join(" ", EscapeArgArrayForCmd(args));
+ }
+
+ ///
+ /// Undo the processing which took place to create string[] args in Main,
+ /// so that the next process will receive the same string[] args
+ ///
+ /// See here for more info:
+ /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
+ ///
+ ///
+ ///
+ private static IEnumerable EscapeArgArray(IEnumerable args)
+ {
+ var escapedArgs = new List();
+
+ foreach (var arg in args)
+ {
+ escapedArgs.Add(EscapeSingleArg(arg));
+ }
+
+ return escapedArgs;
+ }
+
+ ///
+ /// This prefixes every character with the '^' character to force cmd to
+ /// interpret the argument string literally. An alternative option would
+ /// be to do this only for cmd metacharacters.
+ ///
+ /// See here for more info:
+ /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
+ ///
+ ///
+ ///
+ private static IEnumerable EscapeArgArrayForCmd(IEnumerable arguments)
+ {
+ var escapedArgs = new List();
+
+ foreach (var arg in arguments)
+ {
+ escapedArgs.Add(EscapeArgForCmd(arg));
+ }
+
+ return escapedArgs;
+ }
+
+ public static string EscapeSingleArg(string arg)
+ {
+ var sb = new StringBuilder();
+
+ var needsQuotes = ShouldSurroundWithQuotes(arg);
+ var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
+
+ if (needsQuotes) sb.Append("\"");
+
+ for (int i = 0; i < arg.Length; ++i)
+ {
+ var backslashCount = 0;
+
+ // Consume All Backslashes
+ while (i < arg.Length && arg[i] == '\\')
+ {
+ backslashCount++;
+ i++;
+ }
+
+ // Escape any backslashes at the end of the arg
+ // when the argument is also quoted.
+ // This ensures the outside quote is interpreted as
+ // an argument delimiter
+ if (i == arg.Length && isQuoted)
+ {
+ sb.Append('\\', 2 * backslashCount);
+ }
+
+ // At then end of the arg, which isn't quoted,
+ // just add the backslashes, no need to escape
+ else if (i == arg.Length)
+ {
+ sb.Append('\\', backslashCount);
+ }
+
+ // Escape any preceding backslashes and the quote
+ else if (arg[i] == '"')
+ {
+ sb.Append('\\', (2 * backslashCount) + 1);
+ sb.Append('"');
+ }
+
+ // Output any consumed backslashes and the character
+ else
+ {
+ sb.Append('\\', backslashCount);
+ sb.Append(arg[i]);
+ }
+ }
+
+ if (needsQuotes) sb.Append("\"");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Prepare as single argument to
+ /// roundtrip properly through cmd.
+ ///
+ /// This prefixes every character with the '^' character to force cmd to
+ /// interpret the argument string literally. An alternative option would
+ /// be to do this only for cmd metacharacters.
+ ///
+ /// See here for more info:
+ /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
+ ///
+ ///
+ ///
+ private static string EscapeArgForCmd(string argument)
+ {
+ var sb = new StringBuilder();
+
+ var quoted = ShouldSurroundWithQuotes(argument);
+
+ if (quoted) sb.Append("^\"");
+
+ // Prepend every character with ^
+ // This is harmless when passing through cmd
+ // and ensures cmd metacharacters are not interpreted
+ // as such
+ foreach (var character in argument)
+ {
+ sb.Append("^");
+ sb.Append(character);
+ }
+
+ if (quoted) sb.Append("^\"");
+
+ return sb.ToString();
+ }
+
+ internal static bool ShouldSurroundWithQuotes(string argument)
+ {
+ // Don't quote already quoted strings
+ if (IsSurroundedWithQuotes(argument))
+ {
+ return false;
+ }
+
+ // Only quote if whitespace exists in the string
+ return ArgumentContainsWhitespace(argument);
+ }
+
+ internal static bool IsSurroundedWithQuotes(string argument)
+ {
+ return argument.StartsWith("\"", StringComparison.Ordinal) &&
+ argument.EndsWith("\"", StringComparison.Ordinal);
+ }
+
+ internal static bool ArgumentContainsWhitespace(string argument)
+ {
+ return argument.Contains(" ") || argument.Contains("\t") || argument.Contains("\n");
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs b/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs
new file mode 100644
index 0000000..e80061b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs
@@ -0,0 +1,81 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ ///
+ /// An in-memory stream that will block any read calls until something was written to it.
+ ///
+ public sealed class BlockingMemoryStream : Stream
+ {
+ private readonly BlockingCollection _buffers = new BlockingCollection();
+ private ArraySegment _remaining;
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ byte[] tmp = new byte[count];
+ Buffer.BlockCopy(buffer, offset, tmp, 0, count);
+ _buffers.Add(tmp);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (count == 0)
+ {
+ return 0;
+ }
+
+ if (_remaining.Count == 0)
+ {
+ byte[] tmp;
+ if (!_buffers.TryTake(out tmp, Timeout.Infinite) || tmp.Length == 0)
+ {
+ return 0;
+ }
+ _remaining = new ArraySegment(tmp, 0, tmp.Length);
+ }
+
+ if (_remaining.Count <= count)
+ {
+ count = _remaining.Count;
+ Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count);
+ _remaining = default(ArraySegment);
+ }
+ else
+ {
+ Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count);
+ _remaining = new ArraySegment(_remaining.Array, _remaining.Offset + count, _remaining.Count - count);
+ }
+ return count;
+ }
+
+ public void DoneWriting()
+ {
+ _buffers.CompleteAdding();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _buffers.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length { get { throw new NotImplementedException(); } }
+ public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }
+ public override void Flush() { }
+ public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); }
+ public override void SetLength(long value) { throw new NotImplementedException(); }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs b/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs
new file mode 100644
index 0000000..731713a
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs
@@ -0,0 +1,193 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ ///
+ /// A Command that is capable of running in the current process.
+ ///
+ public class BuiltInCommand : ICommand
+ {
+ private readonly IEnumerable _commandArgs;
+ private readonly Func _builtInCommand;
+ private readonly IBuiltInCommandEnvironment _environment;
+ private readonly StreamForwarder _stdOut;
+ private readonly StreamForwarder _stdErr;
+ private string _workingDirectory;
+
+ public string CommandName { get; }
+ public string CommandArgs => string.Join(" ", _commandArgs);
+
+ public BuiltInCommand(string commandName, IEnumerable commandArgs, Func builtInCommand)
+ : this(commandName, commandArgs, builtInCommand, new BuiltInCommandEnvironment())
+ {
+ }
+
+ internal BuiltInCommand(string commandName, IEnumerable commandArgs, Func builtInCommand, IBuiltInCommandEnvironment environment)
+ {
+ CommandName = commandName;
+ _commandArgs = commandArgs;
+ _builtInCommand = builtInCommand;
+ _environment = environment;
+
+ _stdOut = new StreamForwarder();
+ _stdErr = new StreamForwarder();
+ }
+
+ public CommandResult Execute()
+ {
+ TextWriter originalConsoleOut = _environment.GetConsoleOut();
+ TextWriter originalConsoleError = _environment.GetConsoleError();
+ string originalWorkingDirectory = _environment.GetWorkingDirectory();
+
+ try
+ {
+ // redirecting the standard out and error so we can forward
+ // the output to the caller
+ using (BlockingMemoryStream outStream = new BlockingMemoryStream())
+ using (BlockingMemoryStream errorStream = new BlockingMemoryStream())
+ {
+ _environment.SetConsoleOut(new StreamWriter(outStream) { AutoFlush = true });
+ _environment.SetConsoleError(new StreamWriter(errorStream) { AutoFlush = true });
+
+ // Reset the Reporters to the new Console Out and Error.
+ Reporter.Reset();
+
+ if (!string.IsNullOrEmpty(_workingDirectory))
+ {
+ _environment.SetWorkingDirectory(_workingDirectory);
+ }
+
+ var taskOut = _stdOut.BeginRead(new StreamReader(outStream));
+ var taskErr = _stdErr.BeginRead(new StreamReader(errorStream));
+
+ int exitCode = _builtInCommand(_commandArgs.ToArray());
+
+ outStream.DoneWriting();
+ errorStream.DoneWriting();
+
+ Task.WaitAll(taskOut, taskErr);
+
+ // fake out a ProcessStartInfo using the Muxer command name, since this is a built-in command
+ ProcessStartInfo startInfo = new ProcessStartInfo(new Muxer().MuxerPath, $"{CommandName} {CommandArgs}");
+ return new CommandResult(startInfo, exitCode, null, null);
+ }
+ }
+ finally
+ {
+ _environment.SetConsoleOut(originalConsoleOut);
+ _environment.SetConsoleError(originalConsoleError);
+ _environment.SetWorkingDirectory(originalWorkingDirectory);
+
+ Reporter.Reset();
+ }
+ }
+
+ public ICommand OnOutputLine(Action handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ _stdOut.ForwardTo(writeLine: handler);
+
+ return this;
+ }
+
+ public ICommand OnErrorLine(Action handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ _stdErr.ForwardTo(writeLine: handler);
+
+ return this;
+ }
+
+ public ICommand WorkingDirectory(string workingDirectory)
+ {
+ _workingDirectory = workingDirectory;
+
+ return this;
+ }
+
+ private class BuiltInCommandEnvironment : IBuiltInCommandEnvironment
+ {
+ public TextWriter GetConsoleOut()
+ {
+ return Console.Out;
+ }
+
+ public void SetConsoleOut(TextWriter newOut)
+ {
+ Console.SetOut(newOut);
+ }
+
+ public TextWriter GetConsoleError()
+ {
+ return Console.Error;
+ }
+
+ public void SetConsoleError(TextWriter newError)
+ {
+ Console.SetError(newError);
+ }
+
+ public string GetWorkingDirectory()
+ {
+ return Directory.GetCurrentDirectory();
+ }
+
+ public void SetWorkingDirectory(string path)
+ {
+ Directory.SetCurrentDirectory(path);
+ }
+ }
+
+ public CommandResolutionStrategy ResolutionStrategy
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public ICommand CaptureStdErr()
+ {
+ _stdErr.Capture();
+ return this;
+ }
+
+ public ICommand CaptureStdOut()
+ {
+ _stdOut.Capture();
+ return this;
+ }
+
+ public ICommand EnvironmentVariable(string name, string value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Command.cs b/test/Microsoft.DotNet.Cli.Utils/Command.cs
new file mode 100644
index 0000000..631c508
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Command.cs
@@ -0,0 +1,287 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.DotNet.ProjectModel;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class Command : ICommand
+ {
+ private readonly Process _process;
+ private StreamForwarder _stdOut;
+ private StreamForwarder _stdErr;
+
+ private bool _running = false;
+
+ private Command(CommandSpec commandSpec)
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = commandSpec.Path,
+ Arguments = commandSpec.Args,
+ UseShellExecute = false
+ };
+
+ _process = new Process
+ {
+ StartInfo = psi
+ };
+
+ ResolutionStrategy = commandSpec.ResolutionStrategy;
+ }
+
+ public static Command CreateDotNet(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration)
+ {
+ return Create("dotnet",
+ new[] { commandName }.Concat(args),
+ framework,
+ configuration: configuration);
+ }
+
+ ///
+ /// Create a command with the specified arg array. Args will be
+ /// escaped properly to ensure that exactly the strings in this
+ /// array will be present in the corresponding argument array
+ /// in the command's process.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Command Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration,
+ string outputPath = null)
+ {
+ var commandSpec = CommandResolver.TryResolveCommandSpec(commandName,
+ args,
+ framework,
+ configuration: configuration,
+ outputPath: outputPath);
+
+ if (commandSpec == null)
+ {
+ throw new CommandUnknownException(commandName);
+ }
+
+ var command = new Command(commandSpec);
+
+ return command;
+ }
+
+ public static Command Create(CommandSpec commandSpec)
+ {
+ return new Command(commandSpec);
+ }
+
+ public static Command CreateForScript(
+ string commandName,
+ IEnumerable args,
+ Project project,
+ string[] inferredExtensionList)
+ {
+ var commandSpec = CommandResolver.TryResolveScriptCommandSpec(commandName,
+ args,
+ project,
+ inferredExtensionList);
+
+ if (commandSpec == null)
+ {
+ throw new CommandUnknownException(commandName);
+ }
+
+ var command = new Command(commandSpec);
+
+ return command;
+ }
+
+ public CommandResult Execute()
+ {
+
+ Reporter.Verbose.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}");
+
+ ThrowIfRunning();
+ _running = true;
+
+ _process.EnableRaisingEvents = true;
+
+#if DEBUG
+ var sw = Stopwatch.StartNew();
+ Reporter.Verbose.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}".White());
+#endif
+ using (PerfTrace.Current.CaptureTiming($"{Path.GetFileNameWithoutExtension(_process.StartInfo.FileName)} {_process.StartInfo.Arguments}"))
+ {
+ _process.Start();
+
+ Reporter.Verbose.WriteLine($"Process ID: {_process.Id}");
+
+ var taskOut = _stdOut?.BeginRead(_process.StandardOutput);
+ var taskErr = _stdErr?.BeginRead(_process.StandardError);
+ _process.WaitForExit();
+
+ taskOut?.Wait();
+ taskErr?.Wait();
+ }
+
+ var exitCode = _process.ExitCode;
+
+#if DEBUG
+ var message = $"< {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms.";
+ if (exitCode == 0)
+ {
+ Reporter.Verbose.WriteLine(message.Green());
+ }
+ else
+ {
+ Reporter.Verbose.WriteLine(message.Red().Bold());
+ }
+#endif
+
+ return new CommandResult(
+ this._process.StartInfo,
+ exitCode,
+ _stdOut?.CapturedOutput,
+ _stdErr?.CapturedOutput);
+ }
+
+ public ICommand WorkingDirectory(string projectDirectory)
+ {
+ _process.StartInfo.WorkingDirectory = projectDirectory;
+ return this;
+ }
+
+ public ICommand EnvironmentVariable(string name, string value)
+ {
+#if NET451
+ _process.StartInfo.EnvironmentVariables[name] = value;
+#else
+ _process.StartInfo.Environment[name] = value;
+#endif
+ return this;
+ }
+
+ public ICommand CaptureStdOut()
+ {
+ ThrowIfRunning();
+ EnsureStdOut();
+ _stdOut.Capture();
+ return this;
+ }
+
+ public ICommand CaptureStdErr()
+ {
+ ThrowIfRunning();
+ EnsureStdErr();
+ _stdErr.Capture();
+ return this;
+ }
+
+ public ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
+ {
+ ThrowIfRunning();
+ if (!onlyIfVerbose || CommandContext.IsVerbose())
+ {
+ EnsureStdOut();
+
+ if (to == null)
+ {
+ _stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
+ EnvironmentVariable(CommandContext.Variables.AnsiPassThru, ansiPassThrough.ToString());
+ }
+ else
+ {
+ _stdOut.ForwardTo(writeLine: to.WriteLine);
+ }
+ }
+ return this;
+ }
+
+ public ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
+ {
+ ThrowIfRunning();
+ if (!onlyIfVerbose || CommandContext.IsVerbose())
+ {
+ EnsureStdErr();
+
+ if (to == null)
+ {
+ _stdErr.ForwardTo(writeLine: Reporter.Error.WriteLine);
+ EnvironmentVariable(CommandContext.Variables.AnsiPassThru, ansiPassThrough.ToString());
+ }
+ else
+ {
+ _stdErr.ForwardTo(writeLine: to.WriteLine);
+ }
+ }
+ return this;
+ }
+
+ public ICommand OnOutputLine(Action handler)
+ {
+ ThrowIfRunning();
+ EnsureStdOut();
+
+ _stdOut.ForwardTo(writeLine: handler);
+ return this;
+ }
+
+ public ICommand OnErrorLine(Action handler)
+ {
+ ThrowIfRunning();
+ EnsureStdErr();
+
+ _stdErr.ForwardTo(writeLine: handler);
+ return this;
+ }
+
+ public CommandResolutionStrategy ResolutionStrategy { get; }
+
+ public string CommandName => _process.StartInfo.FileName;
+
+ public string CommandArgs => _process.StartInfo.Arguments;
+
+ private string FormatProcessInfo(ProcessStartInfo info)
+ {
+ if (string.IsNullOrWhiteSpace(info.Arguments))
+ {
+ return info.FileName;
+ }
+
+ return info.FileName + " " + info.Arguments;
+ }
+
+ private void EnsureStdOut()
+ {
+ _stdOut = _stdOut ?? new StreamForwarder();
+ _process.StartInfo.RedirectStandardOutput = true;
+ }
+
+ private void EnsureStdErr()
+ {
+ _stdErr = _stdErr ?? new StreamForwarder();
+ _process.StartInfo.RedirectStandardError = true;
+ }
+
+ private void ThrowIfRunning([CallerMemberName] string memberName = null)
+ {
+ if (_running)
+ {
+ throw new InvalidOperationException($"Unable to invoke {memberName} after the command has been run");
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs b/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs
new file mode 100644
index 0000000..8209dd2
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class CommandContext
+ {
+ public static class Variables
+ {
+ private static readonly string Prefix = "DOTNET_CLI_CONTEXT_";
+ public static readonly string Verbose = Prefix + "VERBOSE";
+ public static readonly string AnsiPassThru = Prefix + "ANSI_PASS_THRU";
+ }
+
+ private static Lazy _verbose = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.Verbose));
+ private static Lazy _ansiPassThru = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.AnsiPassThru));
+
+ public static bool IsVerbose()
+ {
+ return _verbose.Value;
+ }
+
+ public static bool ShouldPassAnsiCodesThrough()
+ {
+ return _ansiPassThru.Value;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs
new file mode 100644
index 0000000..f480211
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class CommandFactory : ICommandFactory
+ {
+ public ICommand Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration)
+ {
+ return Command.Create(commandName, args, framework, configuration);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs
new file mode 100644
index 0000000..263f65b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public struct Chain
+ {
+ public Chain(TLeft left, TDown down)
+ : this()
+ {
+ Left = left;
+ Down = down;
+ }
+
+ public readonly TLeft Left;
+ public readonly TDown Down;
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs
new file mode 100644
index 0000000..dfc0d86
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs
@@ -0,0 +1,62 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public class CommandGrammar : Grammar
+ {
+ private CommandGrammar(Func variable, bool preserveSurroundingQuotes)
+ {
+ var environmentVariablePiece = Ch('%').And(Rep(Ch().Not(Ch('%')))).And(Ch('%')).Left().Down().Str()
+ .Build(key => variable(key) ?? "%" + key + "%");
+
+ var escapeSequencePiece =
+ Ch('%').And(Ch('%')).Build(_=>"%")
+ .Or(Ch('^').And(Ch('^')).Build(_ => "^"))
+ .Or(Ch('\\').And(Ch('\\')).Build(_ => "\\"))
+ .Or(Ch('\\').And(Ch('\"')).Build(_ => "\""))
+ ;
+
+ var specialPiece = environmentVariablePiece.Or(escapeSequencePiece);
+
+ var unquotedPiece = Rep1(Ch().Not(specialPiece).Not(Ch(' '))).Str();
+
+ var quotedPiece = Rep1(Ch().Not(specialPiece).Not(Ch('\"'))).Str();
+
+ var unquotedTerm = Rep1(unquotedPiece.Or(specialPiece)).Str();
+
+ var quotedTerm = Ch('\"').And(Rep(quotedPiece.Or(specialPiece)).Str()).And(Ch('\"')).Left().Down();
+ if (preserveSurroundingQuotes)
+ {
+ // Str() value assigned to quotedTerm does not include quotation marks surrounding the quoted or
+ // special piece. Add those quotes back if requested.
+ quotedTerm = quotedTerm.Build(str => "\"" + str + "\"");
+ }
+
+ var whitespace = Rep(Ch(' '));
+
+ var term = whitespace.And(quotedTerm.Or(unquotedTerm)).And(whitespace).Left().Down();
+
+ Parse = Rep(term);
+ }
+
+ public readonly Parser> Parse;
+
+ public static string[] Process(string text, Func variables, bool preserveSurroundingQuotes)
+ {
+ var grammar = new CommandGrammar(variables, preserveSurroundingQuotes);
+ var cursor = new Cursor(text, 0, text.Length);
+
+ var result = grammar.Parse(cursor);
+ if (!result.Remainder.IsEnd)
+ {
+ throw new ArgumentException($"Malformed command text '{text}'", nameof(text));
+ }
+ return result.Value.ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs
new file mode 100644
index 0000000..d8b639a
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public struct Cursor
+ {
+ private readonly string _text;
+ private readonly int _start;
+ private readonly int _end;
+
+ public Cursor(string text, int start, int end)
+ {
+ _text = text;
+ _start = start;
+ _end = end;
+ }
+
+ public bool IsEnd
+ {
+ get { return _start == _end; }
+ }
+
+ public char Peek(int index)
+ {
+ return (index + _start) >= _end ? (char)0 : _text[index + _start];
+ }
+
+ public Result Advance(TValue result, int length)
+ {
+ return new Result(result, new Cursor(_text, _start + length, _end));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs
new file mode 100644
index 0000000..fe32f67
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs
@@ -0,0 +1,52 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public class Grammar
+ {
+ protected static Parser> Rep1(Parser parser)
+ {
+ Parser> rep = Rep(parser);
+ return pos =>
+ {
+ var result = rep(pos);
+ return result.IsEmpty || !result.Value.Any() ? Result>.Empty : result;
+ };
+ }
+
+ protected static Parser> Rep(Parser parser)
+ {
+ return pos =>
+ {
+ var data = new List();
+ for (; ; )
+ {
+ var result = parser(pos);
+ if (result.IsEmpty) break;
+ data.Add(result.Value);
+ pos = result.Remainder;
+ }
+ return new Result>(data, pos);
+ };
+ }
+
+ protected static Parser Ch()
+ {
+ return pos => pos.IsEnd ? Result.Empty : pos.Advance(pos.Peek(0), 1);
+ }
+
+ private static Parser IsEnd()
+ {
+ return pos => pos.IsEnd ? pos.Advance(true, 0) : Result.Empty;
+ }
+
+ protected static Parser Ch(char ch)
+ {
+ return pos => pos.Peek(0) != ch ? Result.Empty : pos.Advance(ch, 1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs
new file mode 100644
index 0000000..997a317
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs
@@ -0,0 +1,7 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public delegate Result Parser(Cursor cursor);
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs
new file mode 100644
index 0000000..04b16f3
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public static class ParserExtensions
+ {
+ public static Parser> And(this Parser parser1,
+ Parser parser2)
+ {
+ return pos =>
+ {
+ var result1 = parser1(pos);
+ if (result1.IsEmpty) return Result>.Empty;
+ var result2 = parser2(result1.Remainder);
+ if (result2.IsEmpty) return Result>.Empty;
+ return result2.AsValue(new Chain(result1.Value, result2.Value));
+ };
+ }
+
+ public static Parser Or(this Parser parser1, Parser parser2)
+ {
+ return pos =>
+ {
+ var result1 = parser1(pos);
+ if (!result1.IsEmpty) return result1;
+ var result2 = parser2(pos);
+ if (!result2.IsEmpty) return result2;
+ return Result.Empty;
+ };
+ }
+
+ public static Parser Not(this Parser parser1, Parser parser2)
+ {
+ return pos =>
+ {
+ var result2 = parser2(pos);
+ if (!result2.IsEmpty) return Result.Empty;
+ return parser1(pos);
+ };
+ }
+
+ public static Parser Left(this Parser> parser)
+ {
+ return pos =>
+ {
+ var result = parser(pos);
+ return result.IsEmpty ? Result.Empty : result.AsValue(result.Value.Left);
+ };
+ }
+
+ public static Parser Down(this Parser> parser)
+ {
+ return pos =>
+ {
+ var result = parser(pos);
+ return result.IsEmpty ? Result.Empty : result.AsValue(result.Value.Down);
+ };
+ }
+
+ public static Parser Build(this Parser parser, Func builder)
+ {
+ return pos =>
+ {
+ var result = parser(pos);
+ if (result.IsEmpty) return Result.Empty;
+ return result.AsValue(builder(result.Value));
+ };
+ }
+
+ public static Parser Str(this Parser> parser)
+ {
+ return parser.Build(x => new string(x.ToArray()));
+ }
+
+ public static Parser Str(this Parser> parser)
+ {
+ return parser.Build(x => String.Concat(x.ToArray()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs
new file mode 100644
index 0000000..9b01596
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils.CommandParsing
+{
+ public struct Result
+ {
+ public Result(TValue value, Cursor remainder)
+ : this()
+ {
+ Value = value;
+ Remainder = remainder;
+ }
+
+ public readonly TValue Value;
+ public readonly Cursor Remainder;
+
+ public bool IsEmpty
+ {
+ get { return Equals(this, default(Result)); }
+ }
+
+ public static Result Empty
+ {
+ get { return default(Result); }
+ }
+
+ public Result AsValue(TValue2 value2)
+ {
+ return new Result(value2, Remainder);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs
new file mode 100644
index 0000000..a9949d2
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs
@@ -0,0 +1,51 @@
+using System;
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public abstract class AbstractPathBasedCommandResolver : ICommandResolver
+ {
+ protected IEnvironmentProvider _environment;
+ protected IPlatformCommandSpecFactory _commandSpecFactory;
+
+ public AbstractPathBasedCommandResolver(IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory commandSpecFactory)
+ {
+ if (environment == null)
+ {
+ throw new ArgumentNullException(nameof(environment));
+ }
+
+ if (commandSpecFactory == null)
+ {
+ throw new ArgumentNullException(nameof(commandSpecFactory));
+ }
+
+ _environment = environment;
+ _commandSpecFactory = commandSpecFactory;
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == null)
+ {
+ return null;
+ }
+
+ var commandPath = ResolveCommandPath(commandResolverArguments);
+
+ if (commandPath == null)
+ {
+ return null;
+ }
+
+ return _commandSpecFactory.CreateCommandSpec(
+ commandResolverArguments.CommandName,
+ commandResolverArguments.CommandArguments.OrEmptyIfNull(),
+ commandPath,
+ GetCommandResolutionStrategy(),
+ _environment);
+ }
+
+ internal abstract string ResolveCommandPath(CommandResolverArguments commandResolverArguments);
+ internal abstract CommandResolutionStrategy GetCommandResolutionStrategy();
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs
new file mode 100644
index 0000000..cdf6104
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs
@@ -0,0 +1,22 @@
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class AppBaseCommandResolver : AbstractPathBasedCommandResolver
+ {
+ public AppBaseCommandResolver(IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { }
+
+ internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments)
+ {
+ return _environment.GetCommandPathFromRootPath(
+ ApplicationEnvironment.ApplicationBasePath,
+ commandResolverArguments.CommandName);
+ }
+
+ internal override CommandResolutionStrategy GetCommandResolutionStrategy()
+ {
+ return CommandResolutionStrategy.BaseDirectory;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs
new file mode 100644
index 0000000..33604c4
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs
@@ -0,0 +1,34 @@
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.DotNet.ProjectModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class AppBaseDllCommandResolver : ICommandResolver
+ {
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == null)
+ {
+ return null;
+ }
+ if (commandResolverArguments.CommandName.EndsWith(FileNameSuffixes.DotNet.DynamicLib))
+ {
+ var localPath = Path.Combine(ApplicationEnvironment.ApplicationBasePath,
+ commandResolverArguments.CommandName);
+ if (File.Exists(localPath))
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
+ new[] { localPath }
+ .Concat(commandResolverArguments.CommandArguments.OrEmptyIfNull()));
+ return new CommandSpec(
+ new Muxer().MuxerPath,
+ escapedArgs,
+ CommandResolutionStrategy.RootedPath);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs
new file mode 100644
index 0000000..e239fcc
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs
@@ -0,0 +1,32 @@
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public enum CommandResolutionStrategy
+ {
+ // command loaded from a deps file
+ DepsFile,
+
+ // command loaded from project dependencies nuget package
+ ProjectDependenciesPackage,
+
+ // command loaded from project tools nuget package
+ ProjectToolsPackage,
+
+ // command loaded from the same directory as the executing assembly
+ BaseDirectory,
+
+ // command loaded from the same directory as a project.json file
+ ProjectLocal,
+
+ // command loaded from PATH environment variable
+ Path,
+
+ // command loaded from rooted path
+ RootedPath,
+
+ // command loaded from project build output path
+ OutputPath,
+
+ // command not found
+ None
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs
new file mode 100644
index 0000000..e43b377
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.DotNet.ProjectModel.Graph;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class CommandResolverArguments
+ {
+ public string CommandName { get; set; }
+
+ public IEnumerable CommandArguments { get; set; }
+
+ public NuGetFramework Framework { get; set; }
+
+ public string OutputPath { get; set; }
+
+ public string ProjectDirectory { get; set; }
+
+ public string Configuration { get; set; }
+
+ public IEnumerable InferredExtensions { get; set; }
+
+ public string BuildBasePath { get; set; }
+
+ public string DepsJsonFile { get; set; }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs
new file mode 100644
index 0000000..bce8811
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class CompositeCommandResolver : ICommandResolver
+ {
+ private IList _orderedCommandResolvers;
+
+ public IEnumerable OrderedCommandResolvers
+ {
+ get
+ {
+ return _orderedCommandResolvers;
+ }
+ }
+
+ public CompositeCommandResolver()
+ {
+ _orderedCommandResolvers = new List();
+ }
+
+ public void AddCommandResolver(ICommandResolver commandResolver)
+ {
+ _orderedCommandResolvers.Add(commandResolver);
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ foreach (var commandResolver in _orderedCommandResolvers)
+ {
+ var commandSpec = commandResolver.Resolve(commandResolverArguments);
+
+ if (commandSpec != null)
+ {
+ return commandSpec;
+ }
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs
new file mode 100644
index 0000000..b4b21fa
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs
@@ -0,0 +1,42 @@
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class DefaultCommandResolverPolicy
+ {
+ public static CompositeCommandResolver Create()
+ {
+ var environment = new EnvironmentProvider();
+ var packagedCommandSpecFactory = new PackagedCommandSpecFactory();
+
+ var platformCommandSpecFactory = default(IPlatformCommandSpecFactory);
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory();
+ }
+ else
+ {
+ platformCommandSpecFactory = new GenericPlatformCommandSpecFactory();
+ }
+
+ return CreateDefaultCommandResolver(environment, packagedCommandSpecFactory, platformCommandSpecFactory);
+ }
+
+ public static CompositeCommandResolver CreateDefaultCommandResolver(
+ IEnvironmentProvider environment,
+ IPackagedCommandSpecFactory packagedCommandSpecFactory,
+ IPlatformCommandSpecFactory platformCommandSpecFactory)
+ {
+ var compositeCommandResolver = new CompositeCommandResolver();
+
+ compositeCommandResolver.AddCommandResolver(new MuxerCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new RootedCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new ProjectToolsCommandResolver(packagedCommandSpecFactory));
+ compositeCommandResolver.AddCommandResolver(new AppBaseDllCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory));
+ compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory));
+
+ return compositeCommandResolver;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs
new file mode 100644
index 0000000..6c67bd2
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.Extensions.DependencyModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class DepsJsonCommandResolver : ICommandResolver
+ {
+ private static readonly string[] s_extensionPreferenceOrder = new[]
+ {
+ "",
+ ".exe",
+ ".dll"
+ };
+
+ private string _nugetPackageRoot;
+ private Muxer _muxer;
+
+ public DepsJsonCommandResolver(string nugetPackageRoot)
+ : this(new Muxer(), nugetPackageRoot) { }
+
+ public DepsJsonCommandResolver(Muxer muxer, string nugetPackageRoot)
+ {
+ _muxer = muxer;
+ _nugetPackageRoot = nugetPackageRoot;
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == null
+ || commandResolverArguments.DepsJsonFile == null)
+ {
+ return null;
+ }
+
+ return ResolveFromDepsJsonFile(
+ commandResolverArguments.CommandName,
+ commandResolverArguments.CommandArguments.OrEmptyIfNull(),
+ commandResolverArguments.DepsJsonFile);
+ }
+
+ private CommandSpec ResolveFromDepsJsonFile(
+ string commandName,
+ IEnumerable commandArgs,
+ string depsJsonFile)
+ {
+ var dependencyContext = LoadDependencyContextFromFile(depsJsonFile);
+
+ var commandPath = GetCommandPathFromDependencyContext(commandName, dependencyContext);
+ if (commandPath == null)
+ {
+ return null;
+ }
+
+ return CreateCommandSpecUsingMuxerIfPortable(
+ commandPath,
+ commandArgs,
+ depsJsonFile,
+ CommandResolutionStrategy.DepsFile,
+ _nugetPackageRoot,
+ IsPortableApp(commandPath));
+ }
+
+ public DependencyContext LoadDependencyContextFromFile(string depsJsonFile)
+ {
+ DependencyContext dependencyContext = null;
+ DependencyContextJsonReader contextReader = new DependencyContextJsonReader();
+
+ using (var contextStream = File.OpenRead(depsJsonFile))
+ {
+ dependencyContext = contextReader.Read(contextStream);
+ }
+
+ return dependencyContext;
+ }
+
+ public string GetCommandPathFromDependencyContext(string commandName, DependencyContext dependencyContext)
+ {
+ var commandCandidates = new List();
+
+ var assemblyCommandCandidates = GetCommandCandidates(
+ commandName,
+ dependencyContext,
+ CommandCandidateType.RuntimeCommandCandidate);
+ var nativeCommandCandidates = GetCommandCandidates(
+ commandName,
+ dependencyContext,
+ CommandCandidateType.NativeCommandCandidate);
+
+ commandCandidates.AddRange(assemblyCommandCandidates);
+ commandCandidates.AddRange(nativeCommandCandidates);
+
+ var command = ChooseCommandCandidate(commandCandidates);
+
+ return command?.GetAbsoluteCommandPath(_nugetPackageRoot);
+ }
+
+ private IEnumerable GetCommandCandidates(
+ string commandName,
+ DependencyContext dependencyContext,
+ CommandCandidateType commandCandidateType)
+ {
+ var commandCandidates = new List();
+
+ foreach (var runtimeLibrary in dependencyContext.RuntimeLibraries)
+ {
+ IEnumerable runtimeAssetGroups = null;
+
+ if (commandCandidateType == CommandCandidateType.NativeCommandCandidate)
+ {
+ runtimeAssetGroups = runtimeLibrary.NativeLibraryGroups;
+ }
+ else if (commandCandidateType == CommandCandidateType.RuntimeCommandCandidate)
+ {
+ runtimeAssetGroups = runtimeLibrary.RuntimeAssemblyGroups;
+ }
+
+ commandCandidates.AddRange(GetCommandCandidatesFromRuntimeAssetGroups(
+ commandName,
+ runtimeAssetGroups,
+ runtimeLibrary.Name,
+ runtimeLibrary.Version));
+ }
+
+ return commandCandidates;
+ }
+
+ private IEnumerable GetCommandCandidatesFromRuntimeAssetGroups(
+ string commandName,
+ IEnumerable runtimeAssetGroups,
+ string PackageName,
+ string PackageVersion)
+ {
+ var candidateAssetGroups = runtimeAssetGroups
+ .Where(r => r.Runtime == string.Empty)
+ .Where(a =>
+ a.AssetPaths.Any(p =>
+ Path.GetFileNameWithoutExtension(p).Equals(commandName, StringComparison.OrdinalIgnoreCase)));
+
+ var commandCandidates = new List();
+ foreach (var candidateAssetGroup in candidateAssetGroups)
+ {
+ var candidateAssetPaths = candidateAssetGroup.AssetPaths.Where(
+ p => Path.GetFileNameWithoutExtension(p)
+ .Equals(commandName, StringComparison.OrdinalIgnoreCase));
+
+ foreach (var candidateAssetPath in candidateAssetPaths)
+ {
+ commandCandidates.Add(new CommandCandidate
+ {
+ PackageName = PackageName,
+ PackageVersion = PackageVersion,
+ RelativeCommandPath = candidateAssetPath
+ });
+ }
+ }
+
+ return commandCandidates;
+ }
+
+ private CommandCandidate ChooseCommandCandidate(IEnumerable commandCandidates)
+ {
+ foreach (var extension in s_extensionPreferenceOrder)
+ {
+ var candidate = commandCandidates
+ .FirstOrDefault(p => Path.GetExtension(p.RelativeCommandPath)
+ .Equals(extension, StringComparison.OrdinalIgnoreCase));
+
+ if (candidate != null)
+ {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+
+ private CommandSpec CreateCommandSpecUsingMuxerIfPortable(
+ string commandPath,
+ IEnumerable commandArgs,
+ string depsJsonFile,
+ CommandResolutionStrategy commandResolutionStrategy,
+ string nugetPackagesRoot,
+ bool isPortable)
+ {
+ var depsFileArguments = GetDepsFileArguments(depsJsonFile);
+ var additionalProbingPathArguments = GetAdditionalProbingPathArguments();
+
+ var muxerArgs = new List();
+ muxerArgs.Add("exec");
+ muxerArgs.AddRange(depsFileArguments);
+ muxerArgs.AddRange(additionalProbingPathArguments);
+ muxerArgs.Add(commandPath);
+ muxerArgs.AddRange(commandArgs);
+
+ var escapedArgString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(muxerArgs);
+
+ return new CommandSpec(_muxer.MuxerPath, escapedArgString, commandResolutionStrategy);
+ }
+
+ private bool IsPortableApp(string commandPath)
+ {
+ var commandDir = Path.GetDirectoryName(commandPath);
+
+ var runtimeConfigPath = Directory.EnumerateFiles(commandDir)
+ .FirstOrDefault(x => x.EndsWith("runtimeconfig.json"));
+
+ if (runtimeConfigPath == null)
+ {
+ return false;
+ }
+
+ var runtimeConfig = new RuntimeConfig(runtimeConfigPath);
+
+ return runtimeConfig.IsPortable;
+ }
+
+ private IEnumerable GetDepsFileArguments(string depsJsonFile)
+ {
+ return new[] { "--depsfile", depsJsonFile };
+ }
+
+ private IEnumerable GetAdditionalProbingPathArguments()
+ {
+ return new[] { "--additionalProbingPath", _nugetPackageRoot };
+ }
+
+ private class CommandCandidate
+ {
+ public string PackageName { get; set; }
+ public string PackageVersion { get; set; }
+ public string RelativeCommandPath { get; set; }
+
+ public string GetAbsoluteCommandPath(string nugetPackageRoot)
+ {
+ return Path.Combine(
+ nugetPackageRoot.Replace('/', Path.DirectorySeparatorChar),
+ PackageName.Replace('/', Path.DirectorySeparatorChar),
+ PackageVersion.Replace('/', Path.DirectorySeparatorChar),
+ RelativeCommandPath.Replace('/', Path.DirectorySeparatorChar));
+ }
+ }
+
+ private enum CommandCandidateType
+ {
+ NativeCommandCandidate,
+ RuntimeCommandCandidate
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs
new file mode 100644
index 0000000..9472eb6
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class GenericPlatformCommandSpecFactory : IPlatformCommandSpecFactory
+ {
+ public CommandSpec CreateCommandSpec(
+ string commandName,
+ IEnumerable args,
+ string commandPath,
+ CommandResolutionStrategy resolutionStrategy,
+ IEnvironmentProvider environment)
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args);
+ return new CommandSpec(commandPath, escapedArgs, resolutionStrategy);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs
new file mode 100644
index 0000000..25b9835
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface ICommandResolver
+ {
+ CommandSpec Resolve(CommandResolverArguments arguments);
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs
new file mode 100644
index 0000000..3c53164
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.DotNet.ProjectModel.Graph;
+using Microsoft.DotNet.ProjectModel.Compilation;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface IPackagedCommandSpecFactory
+ {
+ CommandSpec CreateCommandSpecFromLibrary(
+ LockFileTargetLibrary toolLibrary,
+ string commandName,
+ IEnumerable commandArguments,
+ IEnumerable allowedExtensions,
+ string nugetPackagesRoot,
+ CommandResolutionStrategy commandResolutionStrategy,
+ string depsFilePath,
+ string runtimeConfigPath);
+
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs
new file mode 100644
index 0000000..3748a91
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface IPlatformCommandSpecFactory
+ {
+ CommandSpec CreateCommandSpec(
+ string commandName,
+ IEnumerable args,
+ string commandPath,
+ CommandResolutionStrategy resolutionStrategy,
+ IEnvironmentProvider environment);
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs
new file mode 100644
index 0000000..132efca
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs
@@ -0,0 +1,17 @@
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class MuxerCommandResolver : ICommandResolver
+ {
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == Muxer.MuxerName)
+ {
+ var muxer = new Muxer();
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
+ commandResolverArguments.CommandArguments.OrEmptyIfNull());
+ return new CommandSpec(muxer.MuxerPath, escapedArgs, CommandResolutionStrategy.RootedPath);
+ }
+ return null;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs
new file mode 100644
index 0000000..37996e7
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.DotNet.ProjectModel;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class OutputPathCommandResolver : AbstractPathBasedCommandResolver
+ {
+ public OutputPathCommandResolver(IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory)
+ { }
+
+
+ internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.Framework == null
+ || commandResolverArguments.ProjectDirectory == null
+ || commandResolverArguments.Configuration == null
+ || commandResolverArguments.CommandName == null)
+ {
+ return null;
+ }
+
+ return ResolveFromProjectOutput(
+ commandResolverArguments.ProjectDirectory,
+ commandResolverArguments.Framework,
+ commandResolverArguments.Configuration,
+ commandResolverArguments.CommandName,
+ commandResolverArguments.CommandArguments.OrEmptyIfNull(),
+ commandResolverArguments.OutputPath,
+ commandResolverArguments.BuildBasePath);
+ }
+
+ private string ResolveFromProjectOutput(
+ string projectDirectory,
+ NuGetFramework framework,
+ string configuration,
+ string commandName,
+ IEnumerable commandArguments,
+ string outputPath,
+ string buildBasePath)
+ {
+ var projectContext = GetProjectContextFromDirectory(
+ projectDirectory,
+ framework);
+
+ if (projectContext == null)
+ {
+ return null;
+ }
+
+ var buildOutputPath = projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.BasePath;
+
+ if (! Directory.Exists(buildOutputPath))
+ {
+ Reporter.Verbose.WriteLine($"outputpathresolver: {buildOutputPath} does not exist");
+ return null;
+ }
+
+ return _environment.GetCommandPathFromRootPath(buildOutputPath, commandName);
+ }
+
+ private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework)
+ {
+ if (directory == null || framework == null)
+ {
+ return null;
+ }
+
+ var projectRootPath = directory;
+
+ if (!File.Exists(Path.Combine(projectRootPath, Project.FileName)))
+ {
+ return null;
+ }
+
+ var projectContext = ProjectContext.Create(
+ projectRootPath,
+ framework,
+ DotnetRuntimeIdentifiers.InferCurrentRuntimeIdentifiers());
+
+ if (projectContext.RuntimeIdentifier == null)
+ {
+ return null;
+ }
+
+ return projectContext;
+ }
+
+ internal override CommandResolutionStrategy GetCommandResolutionStrategy()
+ {
+ return CommandResolutionStrategy.OutputPath;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs
new file mode 100644
index 0000000..6842d3e
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.DotNet.ProjectModel.Graph;
+using NuGet.Packaging;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class PackagedCommandSpecFactory : IPackagedCommandSpecFactory
+ {
+ public CommandSpec CreateCommandSpecFromLibrary(
+ LockFileTargetLibrary toolLibrary,
+ string commandName,
+ IEnumerable commandArguments,
+ IEnumerable allowedExtensions,
+ string nugetPackagesRoot,
+ CommandResolutionStrategy commandResolutionStrategy,
+ string depsFilePath,
+ string runtimeConfigPath)
+ {
+
+ var toolAssembly = toolLibrary?.RuntimeAssemblies
+ .FirstOrDefault(r => Path.GetFileNameWithoutExtension(r.Path) == commandName);
+
+ if (toolAssembly == null)
+ {
+ return null;
+ }
+
+ var commandPath = GetCommandFilePath(nugetPackagesRoot, toolLibrary, toolAssembly);
+
+ if (!File.Exists(commandPath))
+ {
+ return null;
+ }
+
+ return CreateCommandSpecWrappingWithMuxerIfDll(
+ commandPath,
+ commandArguments,
+ depsFilePath,
+ commandResolutionStrategy,
+ nugetPackagesRoot,
+ runtimeConfigPath);
+ }
+
+ private string GetCommandFilePath(string nugetPackagesRoot, LockFileTargetLibrary toolLibrary, LockFileItem runtimeAssembly)
+ {
+ var packageDirectory = new VersionFolderPathResolver(nugetPackagesRoot)
+ .GetInstallPath(toolLibrary.Name, toolLibrary.Version);
+
+ var filePath = Path.Combine(packageDirectory, runtimeAssembly.Path);
+
+ return filePath;
+ }
+
+ private CommandSpec CreateCommandSpecWrappingWithMuxerIfDll(
+ string commandPath,
+ IEnumerable commandArguments,
+ string depsFilePath,
+ CommandResolutionStrategy commandResolutionStrategy,
+ string nugetPackagesRoot,
+ string runtimeConfigPath)
+ {
+ var commandExtension = Path.GetExtension(commandPath);
+
+ if (commandExtension == FileNameSuffixes.DotNet.DynamicLib)
+ {
+ return CreatePackageCommandSpecUsingMuxer(
+ commandPath,
+ commandArguments,
+ depsFilePath,
+ commandResolutionStrategy,
+ nugetPackagesRoot,
+ runtimeConfigPath);
+ }
+
+ return CreateCommandSpec(commandPath, commandArguments, commandResolutionStrategy);
+ }
+
+ private CommandSpec CreatePackageCommandSpecUsingMuxer(
+ string commandPath,
+ IEnumerable commandArguments,
+ string depsFilePath,
+ CommandResolutionStrategy commandResolutionStrategy,
+ string nugetPackagesRoot,
+ string runtimeConfigPath)
+ {
+ var host = string.Empty;
+ var arguments = new List();
+
+ var muxer = new Muxer();
+
+ host = muxer.MuxerPath;
+ if (host == null)
+ {
+ throw new Exception("Unable to locate dotnet multiplexer");
+ }
+
+ arguments.Add("exec");
+
+ if (runtimeConfigPath != null)
+ {
+ arguments.Add("--runtimeconfig");
+ arguments.Add(runtimeConfigPath);
+ }
+
+ if (depsFilePath != null)
+ {
+ arguments.Add("--depsfile");
+ arguments.Add(depsFilePath);
+ }
+
+ arguments.Add("--additionalprobingpath");
+ arguments.Add(nugetPackagesRoot);
+
+ arguments.Add(commandPath);
+ arguments.AddRange(commandArguments);
+
+ return CreateCommandSpec(host, arguments, commandResolutionStrategy);
+ }
+
+ private CommandSpec CreateCommandSpec(
+ string commandPath,
+ IEnumerable commandArguments,
+ CommandResolutionStrategy commandResolutionStrategy)
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments);
+
+ return new CommandSpec(commandPath, escapedArgs, commandResolutionStrategy);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs
new file mode 100644
index 0000000..1a263d8
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs
@@ -0,0 +1,18 @@
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class PathCommandResolver : AbstractPathBasedCommandResolver
+ {
+ public PathCommandResolver(IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { }
+
+ internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments)
+ {
+ return _environment.GetCommandPath(commandResolverArguments.CommandName);
+ }
+
+ internal override CommandResolutionStrategy GetCommandResolutionStrategy()
+ {
+ return CommandResolutionStrategy.Path;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs
new file mode 100644
index 0000000..08ec98d
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.DotNet.ProjectModel.Graph;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ProjectDependenciesCommandResolver : ICommandResolver
+ {
+ private static readonly CommandResolutionStrategy s_commandResolutionStrategy =
+ CommandResolutionStrategy.ProjectDependenciesPackage;
+
+ private readonly IEnvironmentProvider _environment;
+ private readonly IPackagedCommandSpecFactory _packagedCommandSpecFactory;
+
+ public ProjectDependenciesCommandResolver(
+ IEnvironmentProvider environment,
+ IPackagedCommandSpecFactory packagedCommandSpecFactory)
+ {
+ if (environment == null)
+ {
+ throw new ArgumentNullException(nameof(environment));
+ }
+
+ if (packagedCommandSpecFactory == null)
+ {
+ throw new ArgumentNullException(nameof(packagedCommandSpecFactory));
+ }
+
+ _environment = environment;
+ _packagedCommandSpecFactory = packagedCommandSpecFactory;
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.Framework == null
+ || commandResolverArguments.ProjectDirectory == null
+ || commandResolverArguments.Configuration == null
+ || commandResolverArguments.CommandName == null)
+ {
+ return null;
+ }
+
+ return ResolveFromProjectDependencies(
+ commandResolverArguments.ProjectDirectory,
+ commandResolverArguments.Framework,
+ commandResolverArguments.Configuration,
+ commandResolverArguments.CommandName,
+ commandResolverArguments.CommandArguments.OrEmptyIfNull(),
+ commandResolverArguments.OutputPath,
+ commandResolverArguments.BuildBasePath);
+ }
+
+ private CommandSpec ResolveFromProjectDependencies(
+ string projectDirectory,
+ NuGetFramework framework,
+ string configuration,
+ string commandName,
+ IEnumerable commandArguments,
+ string outputPath,
+ string buildBasePath)
+ {
+ var allowedExtensions = GetAllowedCommandExtensionsFromEnvironment(_environment);
+
+ var projectContext = GetProjectContextFromDirectory(
+ projectDirectory,
+ framework);
+
+ if (projectContext == null)
+ {
+ return null;
+ }
+
+ var depsFilePath =
+ projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.DepsJson;
+
+ if (! File.Exists(depsFilePath))
+ {
+ Reporter.Verbose.WriteLine($"projectdependenciescommandresolver: {depsFilePath} does not exist");
+ return null;
+ }
+
+ var runtimeConfigPath =
+ projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.RuntimeConfigJson;
+
+ if (! File.Exists(runtimeConfigPath))
+ {
+ Reporter.Verbose.WriteLine($"projectdependenciescommandresolver: {runtimeConfigPath} does not exist");
+ return null;
+ }
+
+ var toolLibrary = GetToolLibraryForContext(projectContext, commandName);
+
+ return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary(
+ toolLibrary,
+ commandName,
+ commandArguments,
+ allowedExtensions,
+ projectContext.PackagesDirectory,
+ s_commandResolutionStrategy,
+ depsFilePath,
+ runtimeConfigPath);
+ }
+
+ private LockFileTargetLibrary GetToolLibraryForContext(
+ ProjectContext projectContext, string commandName)
+ {
+ var toolLibraries = projectContext.LockFile.Targets
+ .FirstOrDefault(t => t.TargetFramework.GetShortFolderName()
+ .Equals(projectContext.TargetFramework.GetShortFolderName()))
+ ?.Libraries.Where(l => l.Name == commandName ||
+ l.RuntimeAssemblies.Any(r => Path.GetFileNameWithoutExtension(r.Path) == commandName)).ToList();
+
+ if (toolLibraries?.Count() > 1)
+ {
+ throw new InvalidOperationException($"Ambiguous command name: {commandName}");
+ }
+
+ return toolLibraries?.FirstOrDefault();
+ }
+
+ private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework)
+ {
+ if (directory == null || framework == null)
+ {
+ return null;
+ }
+
+ var projectRootPath = directory;
+
+ if (!File.Exists(Path.Combine(projectRootPath, Project.FileName)))
+ {
+ return null;
+ }
+
+ return ProjectContext.Create(
+ projectRootPath,
+ framework,
+ DotnetRuntimeIdentifiers.InferCurrentRuntimeIdentifiers());
+
+ }
+
+ private IEnumerable GetAllowedCommandExtensionsFromEnvironment(IEnvironmentProvider environment)
+ {
+ var allowedCommandExtensions = new List();
+ allowedCommandExtensions.AddRange(environment.ExecutableExtensions);
+ allowedCommandExtensions.Add(FileNameSuffixes.DotNet.DynamicLib);
+
+ return allowedCommandExtensions;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs
new file mode 100644
index 0000000..720e406
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs
@@ -0,0 +1,26 @@
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ProjectPathCommandResolver : AbstractPathBasedCommandResolver
+ {
+ public ProjectPathCommandResolver(IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { }
+
+ internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.ProjectDirectory == null)
+ {
+ return null;
+ }
+
+ return _environment.GetCommandPathFromRootPath(
+ commandResolverArguments.ProjectDirectory,
+ commandResolverArguments.CommandName,
+ commandResolverArguments.InferredExtensions.OrEmptyIfNull());
+ }
+
+ internal override CommandResolutionStrategy GetCommandResolutionStrategy()
+ {
+ return CommandResolutionStrategy.ProjectLocal;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs
new file mode 100644
index 0000000..f104769
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.DotNet.ProjectModel;
+using Microsoft.DotNet.ProjectModel.Graph;
+using Microsoft.Extensions.DependencyModel;
+using NuGet.Frameworks;
+using FileFormatException = Microsoft.DotNet.ProjectModel.FileFormatException;
+using LockFile = Microsoft.DotNet.ProjectModel.Graph.LockFile;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ProjectToolsCommandResolver : ICommandResolver
+ {
+ private static readonly NuGetFramework s_toolPackageFramework = FrameworkConstants.CommonFrameworks.NetCoreApp10;
+
+ private static readonly CommandResolutionStrategy s_commandResolutionStrategy =
+ CommandResolutionStrategy.ProjectToolsPackage;
+
+ private List _allowedCommandExtensions;
+ private IPackagedCommandSpecFactory _packagedCommandSpecFactory;
+
+ public ProjectToolsCommandResolver(IPackagedCommandSpecFactory packagedCommandSpecFactory)
+ {
+ _packagedCommandSpecFactory = packagedCommandSpecFactory;
+
+ _allowedCommandExtensions = new List()
+ {
+ FileNameSuffixes.DotNet.DynamicLib
+ };
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == null
+ || commandResolverArguments.ProjectDirectory == null)
+ {
+ return null;
+ }
+
+ return ResolveFromProjectTools(
+ commandResolverArguments.CommandName,
+ commandResolverArguments.CommandArguments.OrEmptyIfNull(),
+ commandResolverArguments.ProjectDirectory);
+ }
+
+ private CommandSpec ResolveFromProjectTools(
+ string commandName,
+ IEnumerable args,
+ string projectDirectory)
+ {
+ var projectContext = GetProjectContextFromDirectoryForFirstTarget(projectDirectory);
+
+ if (projectContext == null)
+ {
+ return null;
+ }
+
+ var toolsLibraries = projectContext.ProjectFile.Tools.OrEmptyIfNull();
+
+ return ResolveCommandSpecFromAllToolLibraries(
+ toolsLibraries,
+ commandName,
+ args,
+ projectContext);
+ }
+
+ private CommandSpec ResolveCommandSpecFromAllToolLibraries(
+ IEnumerable toolsLibraries,
+ string commandName,
+ IEnumerable args,
+ ProjectContext projectContext)
+ {
+ foreach (var toolLibrary in toolsLibraries)
+ {
+ var commandSpec = ResolveCommandSpecFromToolLibrary(toolLibrary, commandName, args, projectContext);
+
+ if (commandSpec != null)
+ {
+ return commandSpec;
+ }
+ }
+
+ return null;
+ }
+
+ private CommandSpec ResolveCommandSpecFromToolLibrary(
+ LibraryRange toolLibraryRange,
+ string commandName,
+ IEnumerable args,
+ ProjectContext projectContext)
+ {
+ var nugetPackagesRoot = projectContext.PackagesDirectory;
+
+ var lockFile = GetToolLockFile(toolLibraryRange, nugetPackagesRoot);
+
+ var toolLibrary = lockFile.Targets
+ .FirstOrDefault(t => t.TargetFramework.GetShortFolderName().Equals(s_toolPackageFramework.GetShortFolderName()))
+ ?.Libraries.FirstOrDefault(l => l.Name == toolLibraryRange.Name);
+
+ if (toolLibrary == null)
+ {
+ return null;
+ }
+
+ var depsFileRoot = Path.GetDirectoryName(lockFile.LockFilePath);
+ var depsFilePath = GetToolDepsFilePath(toolLibraryRange, lockFile, depsFileRoot);
+
+ return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary(
+ toolLibrary,
+ commandName,
+ args,
+ _allowedCommandExtensions,
+ projectContext.PackagesDirectory,
+ s_commandResolutionStrategy,
+ depsFilePath,
+ null);
+ }
+
+ private LockFile GetToolLockFile(
+ LibraryRange toolLibrary,
+ string nugetPackagesRoot)
+ {
+ var lockFilePath = GetToolLockFilePath(toolLibrary, nugetPackagesRoot);
+
+ if (!File.Exists(lockFilePath))
+ {
+ return null;
+ }
+
+ LockFile lockFile = null;
+
+ try
+ {
+ lockFile = LockFileReader.Read(lockFilePath, designTime: false);
+ }
+ catch (FileFormatException ex)
+ {
+ throw ex;
+ }
+
+ return lockFile;
+ }
+
+ private string GetToolLockFilePath(
+ LibraryRange toolLibrary,
+ string nugetPackagesRoot)
+ {
+ var toolPathCalculator = new ToolPathCalculator(nugetPackagesRoot);
+
+ return toolPathCalculator.GetBestLockFilePath(
+ toolLibrary.Name,
+ toolLibrary.VersionRange,
+ s_toolPackageFramework);
+ }
+
+ private ProjectContext GetProjectContextFromDirectoryForFirstTarget(string projectRootPath)
+ {
+ if (projectRootPath == null)
+ {
+ return null;
+ }
+
+ if (!File.Exists(Path.Combine(projectRootPath, Project.FileName)))
+ {
+ return null;
+ }
+
+ var projectContext = ProjectContext.CreateContextForEachTarget(projectRootPath).FirstOrDefault();
+
+ return projectContext;
+ }
+
+ private string GetToolDepsFilePath(
+ LibraryRange toolLibrary,
+ LockFile toolLockFile,
+ string depsPathRoot)
+ {
+ var depsJsonPath = Path.Combine(
+ depsPathRoot,
+ toolLibrary.Name + FileNameSuffixes.DepsJson);
+
+ EnsureToolJsonDepsFileExists(toolLockFile, depsJsonPath);
+
+ return depsJsonPath;
+ }
+
+ private void EnsureToolJsonDepsFileExists(
+ LockFile toolLockFile,
+ string depsPath)
+ {
+ if (!File.Exists(depsPath))
+ {
+ GenerateDepsJsonFile(toolLockFile, depsPath);
+ }
+ }
+
+ // Need to unit test this, so public
+ public void GenerateDepsJsonFile(
+ LockFile toolLockFile,
+ string depsPath)
+ {
+ Reporter.Verbose.WriteLine($"Generating deps.json at: {depsPath}");
+
+ var projectContext = new ProjectContextBuilder()
+ .WithLockFile(toolLockFile)
+ .WithTargetFramework(s_toolPackageFramework.ToString())
+ .Build();
+
+ var exporter = projectContext.CreateExporter(Constants.DefaultConfiguration);
+
+ var dependencyContext = new DependencyContextBuilder()
+ .Build(null,
+ null,
+ exporter.GetAllExports(),
+ true,
+ s_toolPackageFramework,
+ string.Empty);
+
+ var tempDepsFile = Path.GetTempFileName();
+ using (var fileStream = File.Open(tempDepsFile, FileMode.Open, FileAccess.Write))
+ {
+ var dependencyContextWriter = new DependencyContextWriter();
+
+ dependencyContextWriter.Write(dependencyContext, fileStream);
+ }
+
+ try
+ {
+ File.Copy(tempDepsFile, depsPath);
+ }
+ catch (Exception e)
+ {
+ Reporter.Verbose.WriteLine($"unable to generate deps.json, it may have been already generated: {e.Message}");
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(tempDepsFile);
+ }
+ catch (Exception e2)
+ {
+ Reporter.Verbose.WriteLine($"unable to delete temporary deps.json file: {e2.Message}");
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs
new file mode 100644
index 0000000..d7dac8b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs
@@ -0,0 +1,25 @@
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class RootedCommandResolver : ICommandResolver
+ {
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
+ {
+ if (commandResolverArguments.CommandName == null)
+ {
+ return null;
+ }
+
+ if (Path.IsPathRooted(commandResolverArguments.CommandName))
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
+ commandResolverArguments.CommandArguments.OrEmptyIfNull());
+
+ return new CommandSpec(commandResolverArguments.CommandName, escapedArgs, CommandResolutionStrategy.RootedPath);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs
new file mode 100644
index 0000000..04878c5
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs
@@ -0,0 +1,39 @@
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ScriptCommandResolverPolicy
+ {
+ public static CompositeCommandResolver Create()
+ {
+ var environment = new EnvironmentProvider();
+
+ var platformCommandSpecFactory = default(IPlatformCommandSpecFactory);
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory();
+ }
+ else
+ {
+ platformCommandSpecFactory = new GenericPlatformCommandSpecFactory();
+ }
+
+ return CreateScriptCommandResolver(environment, platformCommandSpecFactory);
+ }
+
+ public static CompositeCommandResolver CreateScriptCommandResolver(
+ IEnvironmentProvider environment,
+ IPlatformCommandSpecFactory platformCommandSpecFactory)
+ {
+ var compositeCommandResolver = new CompositeCommandResolver();
+
+ compositeCommandResolver.AddCommandResolver(new RootedCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new MuxerCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new ProjectPathCommandResolver(environment, platformCommandSpecFactory));
+ compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory));
+ compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory));
+
+ return compositeCommandResolver;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs
new file mode 100644
index 0000000..dcb0f15
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs
@@ -0,0 +1,95 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+using NuGet.Frameworks;
+using NuGet.Versioning;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ToolPathCalculator
+ {
+ private readonly string _packagesDirectory;
+
+ public ToolPathCalculator(string packagesDirectory)
+ {
+ _packagesDirectory = packagesDirectory;
+ }
+
+ public string GetBestLockFilePath(string packageId, VersionRange versionRange, NuGetFramework framework)
+ {
+ if (versionRange == null)
+ {
+ throw new ArgumentNullException(nameof(versionRange));
+ }
+
+ if (framework == null)
+ {
+ throw new ArgumentNullException(nameof(framework));
+ }
+
+ var availableToolVersions = GetAvailableToolVersions(packageId);
+
+ var bestVersion = versionRange.FindBestMatch(availableToolVersions);
+ if (bestVersion == null)
+ {
+ throw new GracefulException($"Version for package `{packageId}` could not be resolved.");
+ }
+
+ return GetLockFilePath(packageId, bestVersion, framework);
+ }
+
+ public string GetLockFilePath(string packageId, NuGetVersion version, NuGetFramework framework)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ if (framework == null)
+ {
+ throw new ArgumentNullException(nameof(framework));
+ }
+
+ return Path.Combine(
+ GetBaseToolPath(packageId),
+ version.ToNormalizedString(),
+ framework.GetShortFolderName(),
+ "project.lock.json");
+ }
+
+ private string GetBaseToolPath(string packageId)
+ {
+ return Path.Combine(
+ _packagesDirectory,
+ ".tools",
+ packageId);
+ }
+
+ private IEnumerable GetAvailableToolVersions(string packageId)
+ {
+ var availableVersions = new List();
+
+ var toolBase = GetBaseToolPath(packageId);
+ var versionDirectories = Directory.EnumerateDirectories(toolBase);
+
+ foreach (var versionDirectory in versionDirectories)
+ {
+ var version = Path.GetFileName(versionDirectory);
+
+ NuGetVersion nugetVersion = null;
+ NuGetVersion.TryParse(version, out nugetVersion);
+
+ if (nugetVersion != null)
+ {
+ availableVersions.Add(nugetVersion);
+ }
+ }
+
+ return availableVersions;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs
new file mode 100644
index 0000000..4619f5b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class WindowsExePreferredCommandSpecFactory : IPlatformCommandSpecFactory
+ {
+ public CommandSpec CreateCommandSpec(
+ string commandName,
+ IEnumerable args,
+ string commandPath,
+ CommandResolutionStrategy resolutionStrategy,
+ IEnvironmentProvider environment)
+ {
+ var useCmdWrapper = false;
+
+ if (Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase))
+ {
+ var preferredCommandPath = environment.GetCommandPath(commandName, ".exe");
+
+ if (preferredCommandPath == null)
+ {
+ useCmdWrapper = true;
+ }
+ else
+ {
+ commandPath = preferredCommandPath;
+ }
+ }
+
+ return useCmdWrapper
+ ? CreateCommandSpecWrappedWithCmd(commandPath, args, resolutionStrategy)
+ : CreateCommandSpecFromExecutable(commandPath, args, resolutionStrategy);
+ }
+
+ private CommandSpec CreateCommandSpecFromExecutable(
+ string command,
+ IEnumerable args,
+ CommandResolutionStrategy resolutionStrategy)
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args);
+ return new CommandSpec(command, escapedArgs, resolutionStrategy);
+ }
+
+ private CommandSpec CreateCommandSpecWrappedWithCmd(
+ string command,
+ IEnumerable args,
+ CommandResolutionStrategy resolutionStrategy)
+ {
+ var comSpec = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe";
+
+ // Handle the case where ComSpec is already the command
+ if (command.Equals(comSpec, StringComparison.OrdinalIgnoreCase))
+ {
+ command = args.FirstOrDefault();
+ args = args.Skip(1);
+ }
+
+ var cmdEscapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForCmdProcessStart(args);
+
+ if (ArgumentEscaper.ShouldSurroundWithQuotes(command))
+ {
+ command = $"\"{command}\"";
+ }
+
+ var escapedArgString = $"/s /c \"{command} {cmdEscapedArgs}\"";
+
+ return new CommandSpec(comSpec, escapedArgString, resolutionStrategy);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs
new file mode 100644
index 0000000..35f2f3c
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.DotNet.ProjectModel;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ internal static class CommandResolver
+ {
+ public static CommandSpec TryResolveCommandSpec(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration,
+ string outputPath = null)
+ {
+ var commandResolverArgs = new CommandResolverArguments
+ {
+ CommandName = commandName,
+ CommandArguments = args,
+ Framework = framework,
+ ProjectDirectory = Directory.GetCurrentDirectory(),
+ Configuration = configuration,
+ OutputPath = outputPath
+ };
+
+ var defaultCommandResolver = DefaultCommandResolverPolicy.Create();
+
+ return defaultCommandResolver.Resolve(commandResolverArgs);
+ }
+
+ public static CommandSpec TryResolveScriptCommandSpec(
+ string commandName,
+ IEnumerable args,
+ Project project,
+ string[] inferredExtensionList)
+ {
+ var commandResolverArgs = new CommandResolverArguments
+ {
+ CommandName = commandName,
+ CommandArguments = args,
+ ProjectDirectory = project.ProjectDirectory,
+ InferredExtensions = inferredExtensionList
+ };
+
+ var scriptCommandResolver = ScriptCommandResolverPolicy.Create();
+
+ return scriptCommandResolver.Resolve(commandResolverArgs);
+ }
+ }
+}
+
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs
new file mode 100644
index 0000000..9b7af5b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs
@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public struct CommandResult
+ {
+ public static readonly CommandResult Empty = new CommandResult();
+
+ public ProcessStartInfo StartInfo { get; }
+ public int ExitCode { get; }
+ public string StdOut { get; }
+ public string StdErr { get; }
+
+ public CommandResult(ProcessStartInfo startInfo, int exitCode, string stdOut, string stdErr)
+ {
+ StartInfo = startInfo;
+ ExitCode = exitCode;
+ StdOut = stdOut;
+ StdErr = stdErr;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs b/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs
new file mode 100644
index 0000000..5b94151
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs
@@ -0,0 +1,18 @@
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class CommandSpec
+ {
+ public CommandSpec(string path, string args, CommandResolutionStrategy resolutionStrategy)
+ {
+ Path = path;
+ Args = args;
+ ResolutionStrategy = resolutionStrategy;
+ }
+
+ public string Path { get; }
+
+ public string Args { get; }
+
+ public CommandResolutionStrategy ResolutionStrategy { get; }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs b/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs
new file mode 100644
index 0000000..d3d6fa9
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class CommandUnknownException : GracefulException
+ {
+ public CommandUnknownException()
+ {
+ }
+
+ public CommandUnknownException(string commandName) : base($"No executable found matching command \"{commandName}\"")
+ {
+ }
+
+ public CommandUnknownException(string commandName, Exception innerException) : base($"No executable found matching command \"{commandName}\"", innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/Constants.cs b/test/Microsoft.DotNet.Cli.Utils/Constants.cs
new file mode 100644
index 0000000..93bb49d
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Constants.cs
@@ -0,0 +1,51 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class Constants
+ {
+ private static Platform CurrentPlatform => RuntimeEnvironment.OperatingSystemPlatform;
+ public const string DefaultConfiguration = "Debug";
+
+ public static readonly string ProjectFileName = "project.json";
+ public static readonly string ExeSuffix = CurrentPlatform == Platform.Windows ? ".exe" : string.Empty;
+ public static readonly string ConfigSuffix = ".config";
+
+ // Priority order of runnable suffixes to look for and run
+ public static readonly string[] RunnableSuffixes = CurrentPlatform == Platform.Windows
+ ? new string[] { ".exe", ".cmd", ".bat" }
+ : new string[] { string.Empty };
+
+ public static readonly string BinDirectoryName = "bin";
+ public static readonly string ObjDirectoryName = "obj";
+
+ public static readonly string DynamicLibSuffix = CurrentPlatform == Platform.Windows ? ".dll" :
+ CurrentPlatform == Platform.Darwin ? ".dylib" : ".so";
+
+ public static readonly string LibCoreClrFileName = (CurrentPlatform == Platform.Windows ? "coreclr" : "libcoreclr");
+ public static readonly string LibCoreClrName = LibCoreClrFileName + DynamicLibSuffix;
+
+ public static readonly string StaticLibSuffix = CurrentPlatform == Platform.Windows ? ".lib" : ".a";
+
+ public static readonly string ResponseFileSuffix = ".rsp";
+
+ public static readonly string PublishedHostExecutableName = "dotnet";
+ public static readonly string HostExecutableName = "corehost" + ExeSuffix;
+ public static readonly string[] HostBinaryNames = new string[] {
+ HostExecutableName,
+ (CurrentPlatform == Platform.Windows ? "hostpolicy" : "libhostpolicy") + DynamicLibSuffix,
+ (CurrentPlatform == Platform.Windows ? "hostfxr" : "libhostfxr") + DynamicLibSuffix
+ };
+
+ public static readonly string[] LibCoreClrBinaryNames = new string[]
+ {
+ "coreclr.dll",
+ "libcoreclr.so",
+ "libcoreclr.dylib"
+ };
+
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs b/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs
new file mode 100644
index 0000000..2424bf6
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs
@@ -0,0 +1,52 @@
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class CoreHost
+ {
+ internal static string _hostDir;
+ internal static string _hostExePath;
+
+ public static string HostExePath
+ {
+ get
+ {
+ if (_hostExePath == null)
+ {
+ _hostExePath = Path.Combine(HostDir, Constants.HostExecutableName);
+ }
+ return _hostExePath;
+ }
+ }
+
+ private static string HostDir
+ {
+ get
+ {
+ if (_hostDir == null)
+ {
+ var fxDepsFile = Muxer.GetDataFromAppDomain("FX_DEPS_FILE");
+ _hostDir = Path.GetDirectoryName(fxDepsFile);
+ }
+
+ return _hostDir;
+ }
+ }
+
+ public static void CopyTo(string destinationPath, string hostExeName)
+ {
+ foreach (var binaryName in Constants.HostBinaryNames)
+ {
+ var outputBinaryName = binaryName.Equals(Constants.HostExecutableName)
+ ? hostExeName : binaryName;
+ var outputBinaryPath = Path.Combine(destinationPath, outputBinaryName);
+ var hostBinaryPath = Path.Combine(HostDir, binaryName);
+ File.Copy(hostBinaryPath, outputBinaryPath, overwrite: true);
+
+ // Update the last write time so this file can be treated as an output of a build
+ File.SetLastWriteTimeUtc(outputBinaryPath, DateTime.UtcNow);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs b/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs
new file mode 100644
index 0000000..1675e83
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class DebugHelper
+ {
+ [Conditional("DEBUG")]
+ public static void HandleDebugSwitch(ref string[] args)
+ {
+ if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
+ {
+ args = args.Skip(1).ToArray();
+ WaitForDebugger();
+ }
+ }
+
+ public static void WaitForDebugger()
+ {
+ Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
+ Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}");
+ Console.ReadLine();
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs
new file mode 100644
index 0000000..55108f4
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs
@@ -0,0 +1,50 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using NuGet.Frameworks;
+using System.IO;
+using System;
+using Microsoft.DotNet.ProjectModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class DepsJsonCommandFactory : ICommandFactory
+ {
+ private DepsJsonCommandResolver _depsJsonCommandResolver;
+ private string _temporaryDirectory;
+ private string _depsJsonFile;
+ private string _runtimeConfigFile;
+
+ public DepsJsonCommandFactory(
+ string depsJsonFile,
+ string runtimeConfigFile,
+ string nugetPackagesRoot,
+ string temporaryDirectory)
+ {
+ _depsJsonCommandResolver = new DepsJsonCommandResolver(nugetPackagesRoot);
+
+ _temporaryDirectory = temporaryDirectory;
+ _depsJsonFile = depsJsonFile;
+ _runtimeConfigFile = runtimeConfigFile;
+ }
+
+ public ICommand Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration)
+ {
+ var commandResolverArgs = new CommandResolverArguments()
+ {
+ CommandName = commandName,
+ CommandArguments = args,
+ DepsJsonFile = _depsJsonFile
+ };
+
+ var commandSpec = _depsJsonCommandResolver.Resolve(commandResolverArgs);
+
+ return Command.Create(commandSpec);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs
new file mode 100644
index 0000000..56f6471
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs
@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Reflection;
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class DotnetFiles
+ {
+ private static string SdkRootFolder => Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, "..");
+
+ private static Lazy s_versionFileObject =
+ new Lazy(() => new DotnetVersionFile(VersionFile));
+
+ ///
+ /// The CLI ships with a .version file that stores the commit information and CLI version
+ ///
+ public static string VersionFile => Path.GetFullPath(Path.Combine(SdkRootFolder, ".version"));
+
+ internal static DotnetVersionFile VersionFileObject
+ {
+ get { return s_versionFileObject.Value; }
+ }
+
+ public static string NuGetPackagesArchive =>
+ Path.GetFullPath(Path.Combine(SdkRootFolder, "nuGetPackagesArchive.lzma"));
+
+ ///
+ /// Reads the version file and adds runtime specific information
+ ///
+ public static string ReadAndInterpretVersionFile()
+ {
+ var content = File.ReadAllText(DotnetFiles.VersionFile);
+ content += Environment.NewLine;
+ content += RuntimeEnvironment.GetRuntimeIdentifier();
+ return content;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs
new file mode 100644
index 0000000..7f3da4c
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs
@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ internal static class DotnetRuntimeIdentifiers
+ {
+ public static IEnumerable InferCurrentRuntimeIdentifiers()
+ {
+ IEnumerable fallbackIdentifiers = null;
+
+ // If the machine's RID isn't supported by the shared framework (i.e. the CLI
+ // is being used on a newer version of an OS), add the RID that the CLI was built
+ // with as a fallback. The RID the CLI was built with will have the correct
+ // runtime.* NuGet packages available.
+ // For example, when a user is using osx.10.12, but we only support osx.10.10 and
+ // osx.10.11, the project.json "runtimes" section cannot contain osx.10.12, since
+ // that RID isn't contained in the runtime graph - users will get a restore error.
+ FrameworkDependencyFile fxDepsFile = new FrameworkDependencyFile();
+ if (!fxDepsFile.SupportsCurrentRuntime())
+ {
+ string buildRid = DotnetFiles.VersionFileObject.BuildRid;
+ if (!string.IsNullOrEmpty(buildRid))
+ {
+ fallbackIdentifiers = new string[] { buildRid };
+ }
+ }
+
+ return RuntimeEnvironmentRidExtensions.GetAllCandidateRuntimeIdentifiers(fallbackIdentifiers);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs
new file mode 100644
index 0000000..1f76067
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs
@@ -0,0 +1,61 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ internal class DotnetVersionFile
+ {
+ public bool Exists { get; set; }
+
+ public string CommitSha { get; set; }
+
+ public string BuildNumber { get; set; }
+
+ ///
+ /// The runtime identifier (rid) that this CLI was built for.
+ ///
+ ///
+ /// This is different than RuntimeEnvironment.GetRuntimeIdentifier() because the
+ /// BuildRid is a RID that is guaranteed to exist and works on the current machine. The
+ /// RuntimeEnvironment.GetRuntimeIdentifier() may be for a new version of the OS that
+ /// doesn't have full support yet.
+ ///
+ public string BuildRid { get; set; }
+
+ public DotnetVersionFile(string versionFilePath)
+ {
+ Exists = File.Exists(versionFilePath);
+
+ if (Exists)
+ {
+ IEnumerable lines = File.ReadLines(versionFilePath);
+
+ int index = 0;
+ foreach (string line in lines)
+ {
+ if (index == 0)
+ {
+ CommitSha = line.Substring(0, 10);
+ }
+ else if (index == 1)
+ {
+ BuildNumber = line;
+ }
+ else if (index == 2)
+ {
+ BuildRid = line;
+ }
+ else
+ {
+ break;
+ }
+
+ index++;
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Env.cs b/test/Microsoft.DotNet.Cli.Utils/Env.cs
new file mode 100644
index 0000000..20e9e43
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Env.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class Env
+ {
+ private static IEnvironmentProvider _environment = new EnvironmentProvider();
+
+ public static IEnumerable ExecutableExtensions
+ {
+ get
+ {
+ return _environment.ExecutableExtensions;
+ }
+ }
+
+ public static string GetCommandPath(string commandName, params string[] extensions)
+ {
+ return _environment.GetCommandPath(commandName, extensions);
+ }
+
+ public static string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions)
+ {
+ return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions);
+ }
+
+ public static string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions)
+ {
+ return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions);
+ }
+
+ public static bool GetEnvironmentVariableAsBool(string name, bool defaultValue = false)
+ {
+ return _environment.GetEnvironmentVariableAsBool(name, defaultValue);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs b/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs
new file mode 100644
index 0000000..aa73345
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class EnvironmentProvider : IEnvironmentProvider
+ {
+ private IEnumerable _searchPaths;
+ private IEnumerable _executableExtensions;
+
+ public IEnumerable ExecutableExtensions
+ {
+ get
+ {
+ if (_executableExtensions == null)
+ {
+
+ _executableExtensions = RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows
+ ? Environment.GetEnvironmentVariable("PATHEXT")
+ .Split(';')
+ .Select(e => e.ToLower().Trim('"'))
+ : new [] { string.Empty };
+ }
+
+ return _executableExtensions;
+ }
+ }
+
+ private IEnumerable SearchPaths
+ {
+ get
+ {
+ if (_searchPaths == null)
+ {
+ var searchPaths = new List { ApplicationEnvironment.ApplicationBasePath };
+
+ searchPaths.AddRange(Environment
+ .GetEnvironmentVariable("PATH")
+ .Split(Path.PathSeparator)
+ .Select(p => p.Trim('"')));
+
+ _searchPaths = searchPaths;
+ }
+
+ return _searchPaths;
+ }
+ }
+
+ public EnvironmentProvider(
+ IEnumerable extensionsOverride = null,
+ IEnumerable searchPathsOverride = null)
+ {
+ _executableExtensions = extensionsOverride;
+ _searchPaths = searchPathsOverride;
+ }
+
+ public string GetCommandPath(string commandName, params string[] extensions)
+ {
+ if (!extensions.Any())
+ {
+ extensions = ExecutableExtensions.ToArray();
+ }
+
+ var commandPath = SearchPaths.Join(
+ extensions,
+ p => true, s => true,
+ (p, s) => Path.Combine(p, commandName + s))
+ .FirstOrDefault(File.Exists);
+
+ return commandPath;
+ }
+
+ public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions)
+ {
+ if (!extensions.Any())
+ {
+ extensions = ExecutableExtensions.ToArray();
+ }
+
+ var commandPath = extensions.Select(e => Path.Combine(rootPath, commandName + e))
+ .FirstOrDefault(File.Exists);
+
+ return commandPath;
+ }
+
+ public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions)
+ {
+ var extensionsArr = extensions.OrEmptyIfNull().ToArray();
+
+ return GetCommandPathFromRootPath(rootPath, commandName, extensionsArr);
+ }
+
+ public bool GetEnvironmentVariableAsBool(string name, bool defaultValue)
+ {
+ var str = Environment.GetEnvironmentVariable(name);
+ if (string.IsNullOrEmpty(str))
+ {
+ return defaultValue;
+ }
+
+ switch (str.ToLowerInvariant())
+ {
+ case "true":
+ case "1":
+ case "yes":
+ return true;
+ case "false":
+ case "0":
+ case "no":
+ return false;
+ default:
+ return defaultValue;
+ }
+ }
+
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs
new file mode 100644
index 0000000..08779f6
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class CollectionsExtensions
+ {
+ public static IEnumerable OrEmptyIfNull(this IEnumerable enumerable)
+ {
+ return enumerable == null
+ ? Enumerable.Empty()
+ : enumerable;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs
new file mode 100644
index 0000000..249df0e
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs
@@ -0,0 +1,51 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.DotNet.ProjectModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class ProjectContextCollectionExtensions
+ {
+ public static ProjectContextCollection EnsureValid(this ProjectContextCollection contextCollection, string projectFilePath)
+ {
+ IEnumerable errors;
+
+ if (contextCollection == null)
+ {
+ errors = new[]
+ {
+ new DiagnosticMessage(
+ ErrorCodes.DOTNET1017,
+ $"Project file does not exist '{ProjectPathHelper.NormalizeProjectFilePath(projectFilePath)}'.",
+ projectFilePath,
+ DiagnosticMessageSeverity.Error)
+ };
+ }
+ else
+ {
+ errors = contextCollection
+ .ProjectDiagnostics
+ .Where(d => d.Severity == DiagnosticMessageSeverity.Error);
+ }
+
+ if (errors.Any())
+ {
+ StringBuilder errorMessage = new StringBuilder($"The current project is not valid because of the following errors:{Environment.NewLine}");
+
+ foreach (DiagnosticMessage message in errors)
+ {
+ errorMessage.AppendLine(message.FormattedMessage);
+ }
+
+ throw new GracefulException(errorMessage.ToString());
+ }
+
+ return contextCollection;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs b/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs
new file mode 100644
index 0000000..24130ed
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO;
+using System.Linq;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.Extensions.DependencyModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ ///
+ /// Represents the .deps.json file in the shared framework
+ /// that the CLI is running against.
+ ///
+ internal class FrameworkDependencyFile
+ {
+ private readonly string _depsFilePath;
+
+ public FrameworkDependencyFile()
+ {
+ _depsFilePath = Muxer.GetDataFromAppDomain("FX_DEPS_FILE");
+ }
+
+ public bool SupportsCurrentRuntime()
+ {
+ return IsRuntimeSupported(RuntimeEnvironment.GetRuntimeIdentifier());
+ }
+
+ public bool IsRuntimeSupported(string runtimeIdentifier)
+ {
+ DependencyContext fxDependencyContext = CreateDependencyContext();
+
+ return fxDependencyContext.RuntimeGraph.Any(g => g.Runtime == runtimeIdentifier);
+ }
+
+ private DependencyContext CreateDependencyContext()
+ {
+ using (Stream depsFileStream = File.OpenRead(_depsFilePath))
+ using (DependencyContextJsonReader reader = new DependencyContextJsonReader())
+ {
+ return reader.Read(depsFileStream);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs b/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs
new file mode 100644
index 0000000..4c061e4
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class GracefulException : Exception
+ {
+ public GracefulException()
+ {
+ }
+
+ public GracefulException(string message) : base(message)
+ {
+ }
+
+ public GracefulException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs b/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs
new file mode 100644
index 0000000..e71cbdb
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ internal interface IBuiltInCommandEnvironment
+ {
+ TextWriter GetConsoleOut();
+ void SetConsoleOut(TextWriter newOut);
+
+ TextWriter GetConsoleError();
+ void SetConsoleError(TextWriter newError);
+
+ string GetWorkingDirectory();
+ void SetWorkingDirectory(string path);
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ICommand.cs b/test/Microsoft.DotNet.Cli.Utils/ICommand.cs
new file mode 100644
index 0000000..9f1cfe3
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ICommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface ICommand
+ {
+ CommandResult Execute();
+
+ ICommand WorkingDirectory(string projectDirectory);
+
+ ICommand EnvironmentVariable(string name, string value);
+
+ ICommand CaptureStdOut();
+
+ ICommand CaptureStdErr();
+
+ ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true);
+
+ ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true);
+
+ ICommand OnOutputLine(Action handler);
+
+ ICommand OnErrorLine(Action handler);
+
+ CommandResolutionStrategy ResolutionStrategy { get; }
+
+ string CommandName { get; }
+
+ string CommandArgs { get; }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs
new file mode 100644
index 0000000..017d7a0
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs
@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface ICommandFactory
+ {
+ ICommand Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration);
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs b/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs
new file mode 100644
index 0000000..e9cc613
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public interface IEnvironmentProvider
+ {
+ IEnumerable ExecutableExtensions { get; }
+
+ string GetCommandPath(string commandName, params string[] extensions);
+
+ string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions);
+
+ string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions);
+
+ bool GetEnvironmentVariableAsBool(string name, bool defaultValue);
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs b/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs
new file mode 100644
index 0000000..c858f03
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class InvalidProjectException : Exception
+ {
+ public InvalidProjectException() { }
+ public InvalidProjectException(string message) : base(message) { }
+ public InvalidProjectException(string message, Exception innerException) : base(message, innerException) { }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj
new file mode 100644
index 0000000..031f4af
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net451;netstandard1.6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.DotNet.Cli.Utils/Muxer.cs b/test/Microsoft.DotNet.Cli.Utils/Muxer.cs
new file mode 100644
index 0000000..a2387c1
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Muxer.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class Muxer
+ {
+ public static readonly string MuxerName = "dotnet";
+ private static readonly string s_muxerFileName = MuxerName + Constants.ExeSuffix;
+
+ private string _muxerPath;
+
+ public string MuxerPath
+ {
+ get
+ {
+ if (_muxerPath == null)
+ {
+ throw new InvalidOperationException("Unable to locate dotnet multiplexer");
+ }
+ return _muxerPath;
+ }
+ }
+
+ public Muxer()
+ {
+ if (!TryResolveMuxerFromParentDirectories())
+ {
+ TryResolverMuxerFromPath();
+ }
+ }
+
+ public static string GetDataFromAppDomain(string propertyName)
+ {
+ var appDomainType = typeof(object).GetTypeInfo().Assembly?.GetType("System.AppDomain");
+ var currentDomain = appDomainType?.GetProperty("CurrentDomain")?.GetValue(null);
+ var deps = appDomainType?.GetMethod("GetData")?.Invoke(currentDomain, new[] { propertyName });
+
+ return deps as string;
+ }
+
+ private bool TryResolveMuxerFromParentDirectories()
+ {
+ var fxDepsFile = GetDataFromAppDomain("FX_DEPS_FILE");
+ if (string.IsNullOrEmpty(fxDepsFile))
+ {
+ return false;
+ }
+
+ var muxerDir = new FileInfo(fxDepsFile).Directory?.Parent?.Parent?.Parent;
+ if (muxerDir == null)
+ {
+ return false;
+ }
+
+ var muxerCandidate = Path.Combine(muxerDir.FullName, s_muxerFileName);
+
+ if (!File.Exists(muxerCandidate))
+ {
+ return false;
+ }
+
+ _muxerPath = muxerCandidate;
+ return true;
+ }
+
+ private bool TryResolverMuxerFromPath()
+ {
+ var muxerPath = Env.GetCommandPath(MuxerName, Constants.ExeSuffix);
+
+ if (muxerPath == null || !File.Exists(muxerPath))
+ {
+ return false;
+ }
+
+ _muxerPath = muxerPath;
+
+ return true;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs b/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs
new file mode 100644
index 0000000..c2e9c36
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs
@@ -0,0 +1,247 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using Microsoft.DotNet.InternalAbstractions;
+
+namespace Microsoft.DotNet.Tools.Common
+{
+ public static class PathUtility
+ {
+ public static bool IsPlaceholderFile(string path)
+ {
+ return string.Equals(Path.GetFileName(path), "_._", StringComparison.Ordinal);
+ }
+
+ public static bool IsChildOfDirectory(string dir, string candidate)
+ {
+ if (dir == null)
+ {
+ throw new ArgumentNullException(nameof(dir));
+ }
+ if (candidate == null)
+ {
+ throw new ArgumentNullException(nameof(candidate));
+ }
+ dir = Path.GetFullPath(dir);
+ dir = EnsureTrailingSlash(dir);
+ candidate = Path.GetFullPath(candidate);
+ return candidate.StartsWith(dir, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static string EnsureTrailingSlash(string path)
+ {
+ return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar);
+ }
+
+ public static string EnsureTrailingForwardSlash(string path)
+ {
+ return EnsureTrailingCharacter(path, '/');
+ }
+
+ private static string EnsureTrailingCharacter(string path, char trailingCharacter)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ // if the path is empty, we want to return the original string instead of a single trailing character.
+ if (path.Length == 0 || path[path.Length - 1] == trailingCharacter)
+ {
+ return path;
+ }
+
+ return path + trailingCharacter;
+ }
+
+ public static void EnsureParentDirectory(string filePath)
+ {
+ string directory = Path.GetDirectoryName(filePath);
+
+ EnsureDirectory(directory);
+ }
+
+ public static void EnsureDirectory(string directoryPath)
+ {
+ if (!Directory.Exists(directoryPath))
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+ }
+
+ ///
+ /// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator
+ ///
+ public static string GetRelativePath(string path1, string path2)
+ {
+ return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, true);
+ }
+
+ ///
+ /// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator but ignoring directory
+ /// traversals.
+ ///
+ public static string GetRelativePathIgnoringDirectoryTraversals(string path1, string path2)
+ {
+ return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, false);
+ }
+
+ ///
+ /// Returns path2 relative to path1, with given path separator
+ ///
+ public static string GetRelativePath(string path1, string path2, char separator, bool includeDirectoryTraversals)
+ {
+ if (string.IsNullOrEmpty(path1))
+ {
+ throw new ArgumentException("Path must have a value", nameof(path1));
+ }
+
+ if (string.IsNullOrEmpty(path2))
+ {
+ throw new ArgumentException("Path must have a value", nameof(path2));
+ }
+
+ StringComparison compare;
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ compare = StringComparison.OrdinalIgnoreCase;
+ // check if paths are on the same volume
+ if (!string.Equals(Path.GetPathRoot(path1), Path.GetPathRoot(path2)))
+ {
+ // on different volumes, "relative" path is just path2
+ return path2;
+ }
+ }
+ else
+ {
+ compare = StringComparison.Ordinal;
+ }
+
+ var index = 0;
+ var path1Segments = path1.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ var path2Segments = path2.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ // if path1 does not end with / it is assumed the end is not a directory
+ // we will assume that is isn't a directory by ignoring the last split
+ var len1 = path1Segments.Length - 1;
+ var len2 = path2Segments.Length;
+
+ // find largest common absolute path between both paths
+ var min = Math.Min(len1, len2);
+ while (min > index)
+ {
+ if (!string.Equals(path1Segments[index], path2Segments[index], compare))
+ {
+ break;
+ }
+ // Handle scenarios where folder and file have same name (only if os supports same name for file and directory)
+ // e.g. /file/name /file/name/app
+ else if ((len1 == index && len2 > index + 1) || (len1 > index && len2 == index + 1))
+ {
+ break;
+ }
+ ++index;
+ }
+
+ var path = "";
+
+ // check if path2 ends with a non-directory separator and if path1 has the same non-directory at the end
+ if (len1 + 1 == len2 && !string.IsNullOrEmpty(path1Segments[index]) &&
+ string.Equals(path1Segments[index], path2Segments[index], compare))
+ {
+ return path;
+ }
+
+ if (includeDirectoryTraversals)
+ {
+ for (var i = index; len1 > i; ++i)
+ {
+ path += ".." + separator;
+ }
+ }
+
+ for (var i = index; len2 - 1 > i; ++i)
+ {
+ path += path2Segments[i] + separator;
+ }
+ // if path2 doesn't end with an empty string it means it ended with a non-directory name, so we add it back
+ if (!string.IsNullOrEmpty(path2Segments[len2 - 1]))
+ {
+ path += path2Segments[len2 - 1];
+ }
+
+ return path;
+ }
+
+ public static string GetAbsolutePath(string basePath, string relativePath)
+ {
+ if (basePath == null)
+ {
+ throw new ArgumentNullException(nameof(basePath));
+ }
+
+ if (relativePath == null)
+ {
+ throw new ArgumentNullException(nameof(relativePath));
+ }
+
+ Uri resultUri = new Uri(new Uri(basePath), new Uri(relativePath, UriKind.Relative));
+ return resultUri.LocalPath;
+ }
+
+ public static string GetDirectoryName(string path)
+ {
+ path = path.TrimEnd(Path.DirectorySeparatorChar);
+ return path.Substring(Path.GetDirectoryName(path).Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ public static string GetPathWithForwardSlashes(string path)
+ {
+ return path.Replace('\\', '/');
+ }
+
+ public static string GetPathWithBackSlashes(string path)
+ {
+ return path.Replace('/', '\\');
+ }
+
+ public static string GetPathWithDirectorySeparator(string path)
+ {
+ if (Path.DirectorySeparatorChar == '/')
+ {
+ return GetPathWithForwardSlashes(path);
+ }
+ else
+ {
+ return GetPathWithBackSlashes(path);
+ }
+ }
+
+ public static bool HasExtension(string filePath, string extension)
+ {
+ var comparison = StringComparison.Ordinal;
+
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ comparison = StringComparison.OrdinalIgnoreCase;
+ }
+
+ return Path.GetExtension(filePath).Equals(extension, comparison);
+ }
+
+ ///
+ /// Gets the fully-qualified path without failing if the
+ /// path is empty.
+ ///
+ public static string GetFullPath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return path;
+ }
+
+ return Path.GetFullPath(path);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/Product.cs b/test/Microsoft.DotNet.Cli.Utils/Product.cs
new file mode 100644
index 0000000..8d2a8f0
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Product.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Reflection;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class Product
+ {
+ public static readonly string LongName = ".NET Command Line Tools";
+ public static readonly string Version = GetProductVersion();
+
+ private static string GetProductVersion()
+ {
+ var attr = typeof(Product).GetTypeInfo().Assembly.GetCustomAttribute();
+ return attr?.InformationalVersion;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs
new file mode 100644
index 0000000..0f5cb31
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs
@@ -0,0 +1,121 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using Microsoft.DotNet.InternalAbstractions;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class ProjectDependenciesCommandFactory : ICommandFactory
+ {
+ private readonly NuGetFramework _nugetFramework;
+ private readonly string _configuration;
+ private readonly string _outputPath;
+ private readonly string _buildBasePath;
+ private readonly string _projectDirectory;
+
+ public ProjectDependenciesCommandFactory(
+ NuGetFramework nugetFramework,
+ string configuration,
+ string outputPath,
+ string buildBasePath,
+ string projectDirectory)
+ {
+ _nugetFramework = nugetFramework;
+ _configuration = configuration;
+ _outputPath = outputPath;
+ _buildBasePath = buildBasePath;
+ _projectDirectory = projectDirectory;
+
+ if (_configuration == null)
+ {
+ _configuration = Constants.DefaultConfiguration;
+ }
+ }
+
+ public ICommand Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = null)
+ {
+ if (string.IsNullOrEmpty(configuration))
+ {
+ configuration = _configuration;
+ }
+
+ if (framework == null)
+ {
+ framework = _nugetFramework;
+ }
+
+ var commandSpec = FindProjectDependencyCommands(
+ commandName,
+ args,
+ configuration,
+ framework,
+ _outputPath,
+ _buildBasePath,
+ _projectDirectory);
+
+ return Command.Create(commandSpec);
+ }
+
+ private CommandSpec FindProjectDependencyCommands(
+ string commandName,
+ IEnumerable commandArgs,
+ string configuration,
+ NuGetFramework framework,
+ string outputPath,
+ string buildBasePath,
+ string projectDirectory)
+ {
+ var commandResolverArguments = new CommandResolverArguments
+ {
+ CommandName = commandName,
+ CommandArguments = commandArgs,
+ Framework = framework,
+ Configuration = configuration,
+ OutputPath = outputPath,
+ BuildBasePath = buildBasePath,
+ ProjectDirectory = projectDirectory
+ };
+
+ var commandResolver = GetProjectDependenciesCommandResolver(framework);
+
+ var commandSpec = commandResolver.Resolve(commandResolverArguments);
+ if (commandSpec == null)
+ {
+ throw new CommandUnknownException(commandName);
+ }
+
+ return commandSpec;
+ }
+
+ private ICommandResolver GetProjectDependenciesCommandResolver(NuGetFramework framework)
+ {
+ var environment = new EnvironmentProvider();
+
+ if (framework.IsDesktop())
+ {
+ IPlatformCommandSpecFactory platformCommandSpecFactory = null;
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory();
+ }
+ else
+ {
+ platformCommandSpecFactory = new GenericPlatformCommandSpecFactory();
+ }
+
+ return new OutputPathCommandResolver(environment, platformCommandSpecFactory);
+ }
+ else
+ {
+ var packagedCommandSpecFactory = new PackagedCommandSpecFactory();
+ return new ProjectDependenciesCommandResolver(environment, packagedCommandSpecFactory);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs b/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9b043e9
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyMetadataAttribute("Serviceable", "True")]
+[assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.DotNet.Cli.Utils.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/test/Microsoft.DotNet.Cli.Utils/Reporter.cs b/test/Microsoft.DotNet.Cli.Utils/Reporter.cs
new file mode 100644
index 0000000..0afffa1
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Reporter.cs
@@ -0,0 +1,83 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ // Stupid-simple console manager
+ public class Reporter
+ {
+ private static readonly Reporter NullReporter = new Reporter(console: null);
+ private static object _lock = new object();
+
+ private readonly AnsiConsole _console;
+
+ static Reporter()
+ {
+ Reset();
+ }
+
+ private Reporter(AnsiConsole console)
+ {
+ _console = console;
+ }
+
+ public static Reporter Output { get; private set; }
+ public static Reporter Error { get; private set; }
+ public static Reporter Verbose { get; private set; }
+
+ ///
+ /// Resets the Reporters to write to the current Console Out/Error.
+ ///
+ public static void Reset()
+ {
+ lock (_lock)
+ {
+ Output = new Reporter(AnsiConsole.GetOutput());
+ Error = new Reporter(AnsiConsole.GetError());
+ Verbose = IsVerbose ?
+ new Reporter(AnsiConsole.GetOutput()) :
+ NullReporter;
+ }
+ }
+
+ public static bool IsVerbose => CommandContext.IsVerbose();
+
+ public void WriteLine(string message)
+ {
+ lock (_lock)
+ {
+ if (CommandContext.ShouldPassAnsiCodesThrough())
+ {
+ _console?.Writer?.WriteLine(message);
+ }
+ else
+ {
+ _console?.WriteLine(message);
+ }
+ }
+ }
+
+ public void WriteLine()
+ {
+ lock (_lock)
+ {
+ _console?.Writer?.WriteLine();
+ }
+ }
+
+ public void Write(string message)
+ {
+ lock (_lock)
+ {
+ if (CommandContext.ShouldPassAnsiCodesThrough())
+ {
+ _console?.Writer?.Write(message);
+ }
+ else
+ {
+ _console?.Write(message);
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs b/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs
new file mode 100644
index 0000000..e2dbd3b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.DotNet.Cli.Utils.CommandParsing;
+using Microsoft.DotNet.InternalAbstractions;
+using Microsoft.DotNet.ProjectModel;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class ScriptExecutor
+ {
+ public static ICommand CreateCommandForScript(Project project, string scriptCommandLine, IDictionary variables)
+ {
+ return CreateCommandForScript(project, scriptCommandLine, WrapVariableDictionary(variables));
+ }
+
+ public static ICommand CreateCommandForScript(Project project, string scriptCommandLine, Func getVariable)
+ {
+ var scriptArguments = ParseScriptArguments(project, scriptCommandLine, getVariable);
+ if (scriptArguments == null)
+ {
+ throw new Exception($"ScriptExecutor: failed to parse script \"{scriptCommandLine}\"");
+ }
+
+ var inferredExtensions = DetermineInferredScriptExtensions();
+
+ return Command
+ .CreateForScript(scriptArguments.First(), scriptArguments.Skip(1), project, inferredExtensions)
+ .WorkingDirectory(project.ProjectDirectory);
+ }
+
+ private static IEnumerable ParseScriptArguments(Project project, string scriptCommandLine, Func getVariable)
+ {
+ var scriptArguments = CommandGrammar.Process(
+ scriptCommandLine,
+ GetScriptVariable(project, getVariable),
+ preserveSurroundingQuotes: false);
+
+ scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray();
+ if (scriptArguments.Length == 0)
+ {
+ return null;
+ }
+
+ return scriptArguments;
+ }
+
+ private static string[] DetermineInferredScriptExtensions()
+ {
+ if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
+ {
+ return new string[] { "", ".cmd" };
+ }
+ else
+ {
+ return new string[] { "", ".sh" };
+ }
+ }
+
+ private static Func WrapVariableDictionary(IDictionary contextVariables)
+ {
+ return key =>
+ {
+ string value;
+ contextVariables.TryGetValue(key, out value);
+ return value;
+ };
+ }
+
+ private static Func GetScriptVariable(Project project, Func getVariable)
+ {
+ var keys = new Dictionary>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "project:Directory", () => project.ProjectDirectory },
+ { "project:Name", () => project.Name },
+ { "project:Version", () => project.Version.ToString() },
+ };
+
+ return key =>
+ {
+ // try returning key from dictionary
+ Func valueFactory;
+ if (keys.TryGetValue(key, out valueFactory))
+ {
+ return valueFactory();
+ }
+
+ // try returning command-specific key
+ var value = getVariable(key);
+ if (!string.IsNullOrEmpty(value))
+ {
+ return value;
+ }
+
+ // try returning environment variable
+ return Environment.GetEnvironmentVariable(key);
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs b/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs
new file mode 100644
index 0000000..41bece0
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class ScriptNames
+ {
+ public const string PreCompile = "precompile";
+ public const string PostCompile = "postcompile";
+ public const string PrePublish = "prepublish";
+ public const string PostPublish = "postpublish";
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs b/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs
new file mode 100644
index 0000000..aa16cb2
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs
@@ -0,0 +1,133 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public sealed class StreamForwarder
+ {
+ private static readonly char[] s_ignoreCharacters = new char[] { '\r' };
+ private static readonly char s_flushBuilderCharacter = '\n';
+
+ private StringBuilder _builder;
+ private StringWriter _capture;
+ private Action _writeLine;
+
+ public string CapturedOutput
+ {
+ get
+ {
+ return _capture?.GetStringBuilder()?.ToString();
+ }
+ }
+
+ public StreamForwarder Capture()
+ {
+ ThrowIfCaptureSet();
+
+ _capture = new StringWriter();
+
+ return this;
+ }
+
+ public StreamForwarder ForwardTo(Action writeLine)
+ {
+ ThrowIfNull(writeLine);
+
+ ThrowIfForwarderSet();
+
+ _writeLine = writeLine;
+
+ return this;
+ }
+
+ public Task BeginRead(TextReader reader)
+ {
+ return Task.Run(() => Read(reader));
+ }
+
+ public void Read(TextReader reader)
+ {
+ var bufferSize = 1;
+
+ int readCharacterCount;
+ char currentCharacter;
+
+ var buffer = new char[bufferSize];
+ _builder = new StringBuilder();
+
+ // Using Read with buffer size 1 to prevent looping endlessly
+ // like we would when using Read() with no buffer
+ while ((readCharacterCount = reader.Read(buffer, 0, bufferSize)) > 0)
+ {
+ currentCharacter = buffer[0];
+
+ if (currentCharacter == s_flushBuilderCharacter)
+ {
+ WriteBuilder();
+ }
+ else if (! s_ignoreCharacters.Contains(currentCharacter))
+ {
+ _builder.Append(currentCharacter);
+ }
+ }
+
+ // Flush anything else when the stream is closed
+ // Which should only happen if someone used console.Write
+ WriteBuilder();
+ }
+
+ private void WriteBuilder()
+ {
+ if (_builder.Length == 0)
+ {
+ return;
+ }
+
+ WriteLine(_builder.ToString());
+ _builder.Clear();
+ }
+
+ private void WriteLine(string str)
+ {
+ if (_capture != null)
+ {
+ _capture.WriteLine(str);
+ }
+
+ if (_writeLine != null)
+ {
+ _writeLine(str);
+ }
+ }
+
+ private void ThrowIfNull(object obj)
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ }
+
+ private void ThrowIfForwarderSet()
+ {
+ if (_writeLine != null)
+ {
+ throw new InvalidOperationException("WriteLine forwarder set previously");
+ }
+ }
+
+ private void ThrowIfCaptureSet()
+ {
+ if (_capture != null)
+ {
+ throw new InvalidOperationException("Already capturing stream!");
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs
new file mode 100644
index 0000000..abbf866
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public static class PerfTrace
+ {
+ private static ConcurrentBag _threads = new ConcurrentBag();
+
+ [ThreadStatic]
+ private static PerfTraceThreadContext _current;
+
+ public static bool Enabled { get; set; }
+
+ public static PerfTraceThreadContext Current => _current ?? (_current = InitializeCurrent());
+
+ private static PerfTraceThreadContext InitializeCurrent()
+ {
+ var context = new PerfTraceThreadContext(Thread.CurrentThread.ManagedThreadId);
+ _threads.Add(context);
+ return context;
+ }
+
+ public static IEnumerable GetEvents()
+ {
+ return _threads;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs
new file mode 100644
index 0000000..bcfb53b
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class PerfTraceEvent
+ {
+ public string Type { get; }
+ public string Instance { get; }
+ public DateTime StartUtc { get; }
+ public TimeSpan Duration { get; }
+ public IList Children { get; }
+
+ public PerfTraceEvent(string type, string instance, IEnumerable children, DateTime startUtc, TimeSpan duration)
+ {
+ Type = type;
+ Instance = instance;
+ StartUtc = startUtc;
+ Duration = duration;
+ Children = children.OrderBy(e => e.StartUtc).ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs
new file mode 100644
index 0000000..2212efb
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs
@@ -0,0 +1,81 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class PerfTraceOutput
+ {
+ private static TimeSpan _minDuration = TimeSpan.FromSeconds(0.001);
+
+ public static void Print(Reporter reporter, IEnumerable contexts)
+ {
+ foreach (var threadContext in contexts)
+ {
+ Print(reporter, new[] { threadContext.Root }, threadContext.Root, null);
+ }
+ }
+
+ private static void Print(Reporter reporter, IEnumerable events, PerfTraceEvent root, PerfTraceEvent parent, int padding = 0)
+ {
+ foreach (var e in events)
+ {
+ if (e.Duration < _minDuration)
+ {
+ continue;
+ }
+ reporter.Write(new string(' ', padding));
+ reporter.WriteLine(FormatEvent(e, root, parent));
+ Print(reporter, e.Children, root, e, padding + 2);
+ }
+ }
+
+ private static string FormatEvent(PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent)
+ {
+ var builder = new StringBuilder();
+ FormatEventTimeStat(builder, e, root, parent);
+ builder.Append($" {e.Type.Bold()} {e.Instance}");
+ return builder.ToString();
+ }
+
+ private static void FormatEventTimeStat(StringBuilder builder, PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent)
+ {
+ builder.Append("[");
+ if (root != e)
+ {
+ AppendTime(builder, e.Duration.TotalSeconds / root.Duration.TotalSeconds, 0.2);
+ }
+ AppendTime(builder, e.Duration.TotalSeconds / parent?.Duration.TotalSeconds, 0.5);
+ builder.Append($"{e.Duration.ToString("ss\\.fff\\s").Blue()}]");
+ }
+
+ private static void AppendTime(StringBuilder builder, double? percent, double treshold)
+ {
+ if (percent != null)
+ {
+ var formattedPercent = $"{percent*100:00\\.00%}";
+ if (percent > treshold)
+ {
+ builder.Append(formattedPercent.Red());
+ }
+ else if (percent > treshold / 2)
+ {
+ builder.Append(formattedPercent.Yellow());
+ }
+ else if (percent > treshold / 5)
+ {
+ builder.Append(formattedPercent.Green());
+ }
+ else
+ {
+ builder.Append(formattedPercent);
+ }
+ builder.Append(" ");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs
new file mode 100644
index 0000000..41a42d7
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs
@@ -0,0 +1,76 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class PerfTraceThreadContext
+ {
+ private readonly int _threadId;
+
+ private TimerDisposable _activeEvent;
+
+ public PerfTraceEvent Root => _activeEvent.CreateEvent();
+
+ public PerfTraceThreadContext(int threadId)
+ {
+ _activeEvent = new TimerDisposable(this, "Thread", $"{threadId.ToString()}");
+ _threadId = threadId;
+ }
+
+ public IDisposable CaptureTiming(string instance = "", [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "")
+ {
+ if(!PerfTrace.Enabled)
+ {
+ return null;
+ }
+
+ var newTimer = new TimerDisposable(this, $"{Path.GetFileNameWithoutExtension(filePath)}:{memberName}", instance);
+ var previousTimer = Interlocked.Exchange(ref _activeEvent, newTimer);
+ newTimer.Parent = previousTimer;
+ return newTimer;
+ }
+
+ private void RecordTiming(PerfTraceEvent newEvent, TimerDisposable parent)
+ {
+ Interlocked.Exchange(ref _activeEvent, parent);
+ _activeEvent.Children.Add(newEvent);
+ }
+
+ private class TimerDisposable : IDisposable
+ {
+ private readonly PerfTraceThreadContext _context;
+ private string _eventType;
+ private string _instance;
+ private DateTime _startUtc;
+ private Stopwatch _stopwatch = Stopwatch.StartNew();
+
+ public TimerDisposable Parent { get; set; }
+
+ public ConcurrentBag Children { get; set; } = new ConcurrentBag();
+
+ public TimerDisposable(PerfTraceThreadContext context, string eventType, string instance)
+ {
+ _context = context;
+ _eventType = eventType;
+ _instance = instance;
+ _startUtc = DateTime.UtcNow;
+ }
+
+ public void Dispose()
+ {
+ _stopwatch.Stop();
+
+ _context.RecordTiming(CreateEvent(), Parent);
+ }
+
+ public PerfTraceEvent CreateEvent() => new PerfTraceEvent(_eventType, _instance, Children, _startUtc, _stopwatch.Elapsed);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs
new file mode 100644
index 0000000..5a04d82
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs
@@ -0,0 +1,136 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Text.RegularExpressions;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public class CommandResultAssertions
+ {
+ private CommandResult _commandResult;
+
+ public CommandResultAssertions(CommandResult commandResult)
+ {
+ _commandResult = commandResult;
+ }
+
+ public AndConstraint ExitWith(int expectedExitCode)
+ {
+ Execute.Assertion.ForCondition(_commandResult.ExitCode == expectedExitCode)
+ .FailWith(AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not."));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint Pass()
+ {
+ Execute.Assertion.ForCondition(_commandResult.ExitCode == 0)
+ .FailWith(AppendDiagnosticsTo($"Expected command to pass but it did not."));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint Fail()
+ {
+ Execute.Assertion.ForCondition(_commandResult.ExitCode != 0)
+ .FailWith(AppendDiagnosticsTo($"Expected command to fail but it did not."));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdOut()
+ {
+ Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdOut))
+ .FailWith(AppendDiagnosticsTo("Command did not output anything to stdout"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdOut(string expectedOutput)
+ {
+ Execute.Assertion.ForCondition(_commandResult.StdOut.Equals(expectedOutput, StringComparison.Ordinal))
+ .FailWith(AppendDiagnosticsTo($"Command did not output with Expected Output. Expected: {expectedOutput}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdOutContaining(string pattern)
+ {
+ Execute.Assertion.ForCondition(_commandResult.StdOut.Contains(pattern))
+ .FailWith(AppendDiagnosticsTo($"The command output did not contain expected result: {pattern}{Environment.NewLine}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None)
+ {
+ Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdOut, pattern, options).Success)
+ .FailWith(AppendDiagnosticsTo($"Matching the command output failed. Pattern: {pattern}{Environment.NewLine}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdErr()
+ {
+ Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdErr))
+ .FailWith(AppendDiagnosticsTo("Command did not output anything to stderr."));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdErrContaining(string pattern)
+ {
+ Execute.Assertion.ForCondition(_commandResult.StdErr.Contains(pattern))
+ .FailWith(AppendDiagnosticsTo($"The command error output did not contain expected result: {pattern}{Environment.NewLine}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotHaveStdErrContaining(string pattern)
+ {
+ Execute.Assertion.ForCondition(!_commandResult.StdErr.Contains(pattern))
+ .FailWith(AppendDiagnosticsTo($"The command error output contained a result it should not have contained: {pattern}{Environment.NewLine}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None)
+ {
+ Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdErr, pattern, options).Success)
+ .FailWith(AppendDiagnosticsTo($"Matching the command error output failed. Pattern: {pattern}{Environment.NewLine}"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotHaveStdOut()
+ {
+ Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdOut))
+ .FailWith(AppendDiagnosticsTo($"Expected command to not output to stdout but it was not:"));
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotHaveStdErr()
+ {
+ Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdErr))
+ .FailWith(AppendDiagnosticsTo("Expected command to not output to stderr but it was not:"));
+ return new AndConstraint(this);
+ }
+
+ private string AppendDiagnosticsTo(string s)
+ {
+ return s + $"{Environment.NewLine}" +
+ $"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" +
+ $"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" +
+ $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" +
+ $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" +
+ $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}"; ;
+ }
+
+ public AndConstraint HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName)
+ {
+ _commandResult.StdOut.Should().Contain($"Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.");
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveCompiledProject(string compiledProject, string frameworkFullName)
+ {
+ _commandResult.StdOut.Should().Contain($"Project {compiledProject} ({frameworkFullName}) will be compiled");
+
+ return new AndConstraint(this);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs
new file mode 100644
index 0000000..ad3bf23
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public static class CommandResultExtensions
+ {
+ public static CommandResultAssertions Should(this CommandResult commandResult)
+ {
+ return new CommandResultAssertions(commandResult);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs
new file mode 100644
index 0000000..9a4e834
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs
@@ -0,0 +1,93 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using FluentAssertions;
+using FluentAssertions.Execution;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public class DirectoryInfoAssertions
+ {
+ private DirectoryInfo _dirInfo;
+
+ public DirectoryInfoAssertions(DirectoryInfo dir)
+ {
+ _dirInfo = dir;
+ }
+
+ public DirectoryInfo DirectoryInfo => _dirInfo;
+
+ public AndConstraint Exist()
+ {
+ Execute.Assertion.ForCondition(_dirInfo.Exists)
+ .FailWith("Expected directory {0} does not exist.", _dirInfo.FullName);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveFile(string expectedFile)
+ {
+ var file = _dirInfo.EnumerateFiles(expectedFile, SearchOption.TopDirectoryOnly).SingleOrDefault();
+ Execute.Assertion.ForCondition(file != null)
+ .FailWith("Expected File {0} cannot be found in directory {1}.", expectedFile, _dirInfo.FullName);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotHaveFile(string expectedFile)
+ {
+ var file = _dirInfo.EnumerateFiles(expectedFile, SearchOption.TopDirectoryOnly).SingleOrDefault();
+ Execute.Assertion.ForCondition(file == null)
+ .FailWith("File {0} should not be found in directory {1}.", expectedFile, _dirInfo.FullName);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveFiles(IEnumerable expectedFiles)
+ {
+ foreach (var expectedFile in expectedFiles)
+ {
+ HaveFile(expectedFile);
+ }
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotHaveFiles(IEnumerable expectedFiles)
+ {
+ foreach (var expectedFile in expectedFiles)
+ {
+ NotHaveFile(expectedFile);
+ }
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveDirectory(string expectedDir)
+ {
+ var dir = _dirInfo.EnumerateDirectories(expectedDir, SearchOption.TopDirectoryOnly).SingleOrDefault();
+ Execute.Assertion.ForCondition(dir != null)
+ .FailWith("Expected directory {0} cannot be found inside directory {1}.", expectedDir, _dirInfo.FullName);
+
+ return new AndConstraint(new DirectoryInfoAssertions(dir));
+ }
+
+ public AndConstraint OnlyHaveFiles(IEnumerable expectedFiles)
+ {
+ var actualFiles = _dirInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name);
+ var missingFiles = Enumerable.Except(expectedFiles, actualFiles);
+ var extraFiles = Enumerable.Except(actualFiles, expectedFiles);
+ var nl = Environment.NewLine;
+
+ Execute.Assertion.ForCondition(!missingFiles.Any())
+ .FailWith($"Following files cannot be found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, missingFiles)}");
+
+ Execute.Assertion.ForCondition(!extraFiles.Any())
+ .FailWith($"Following extra files are found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, extraFiles)}");
+
+ return new AndConstraint(this);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs
new file mode 100644
index 0000000..e71f64b
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs
@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public static class DirectoryInfoExtensions
+ {
+ public static DirectoryInfoAssertions Should(this DirectoryInfo dir)
+ {
+ return new DirectoryInfoAssertions(dir);
+ }
+
+ public static DirectoryInfo Sub(this DirectoryInfo dir, string name)
+ {
+ return new DirectoryInfo(Path.Combine(dir.FullName, name));
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs
new file mode 100644
index 0000000..95e4341
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs
@@ -0,0 +1,69 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public sealed class RunCommand : TestCommand
+ {
+ private string _projectPath;
+ private string _framework;
+ private string _configuration;
+ private string _appArgs;
+
+ private string ProjectPathOption => string.IsNullOrEmpty(_projectPath) ? "" : $"-p \"{_projectPath}\"";
+
+ private string FrameworkOption => string.IsNullOrEmpty(_framework) ? "" : $"-f {_framework}";
+
+ private string ConfigurationOption => string.IsNullOrEmpty(_configuration) ? "" : $"-c {_configuration}";
+
+ private string AppArgsArgument => _appArgs;
+
+ public RunCommand(
+ string projectPath,
+ string framework = "",
+ string configuration = "",
+ string appArgs = "")
+ : base("dotnet")
+ {
+ _projectPath = projectPath;
+ _framework = framework;
+ _configuration = configuration;
+ _appArgs = appArgs;
+ }
+
+ public override CommandResult Execute(string args = "")
+ {
+ args = $"run {BuildArgs()} {args}";
+ return base.Execute(args);
+ }
+
+ public override CommandResult ExecuteWithCapturedOutput(string args = "")
+ {
+ args = $"run {BuildArgs()} {args}";
+ return base.ExecuteWithCapturedOutput(args);
+ }
+
+ public override Task ExecuteAsync(string args = "")
+ {
+ args = $"run {BuildArgs()} {args}";
+ return base.ExecuteAsync(args);
+ }
+
+ private string BuildArgs()
+ {
+ return string.Join(" ",
+ new[]
+ {
+ ProjectPathOption,
+ FrameworkOption,
+ ConfigurationOption,
+ AppArgsArgument,
+ }
+ .Where(s => !string.IsNullOrEmpty(s)));
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs
new file mode 100644
index 0000000..c694fca
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs
@@ -0,0 +1,204 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public class TestCommand
+ {
+ protected string _command;
+
+ private string _baseDirectory;
+
+ public string WorkingDirectory { get; set; }
+
+ public Process CurrentProcess { get; set; }
+
+ public Dictionary Environment { get; } = new Dictionary();
+
+ public TestCommand(string command)
+ {
+ _command = command;
+#if NET451
+ _baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
+#else
+ _baseDirectory = AppContext.BaseDirectory;
+#endif
+ }
+
+ public TestCommand WithWorkingDirectory(string workingDirectory)
+ {
+ WorkingDirectory = workingDirectory;
+ return this;
+ }
+
+ public virtual CommandResult Execute(string args = "")
+ {
+ var commandPath = _command;
+ ResolveCommand(ref commandPath, ref args);
+
+ Console.WriteLine($"Executing - {commandPath} {args}");
+
+ var stdOut = new StreamForwarder();
+ var stdErr = new StreamForwarder();
+
+ stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
+ stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine);
+
+ return RunProcess(commandPath, args, stdOut, stdErr);
+ }
+
+ public virtual Task ExecuteAsync(string args = "")
+ {
+ var commandPath = _command;
+ ResolveCommand(ref commandPath, ref args);
+
+ Console.WriteLine($"Executing - {commandPath} {args}");
+
+ var stdOut = new StreamForwarder();
+ var stdErr = new StreamForwarder();
+
+ stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine);
+ stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine);
+
+ return RunProcessAsync(commandPath, args, stdOut, stdErr);
+ }
+
+ public virtual CommandResult ExecuteWithCapturedOutput(string args = "")
+ {
+ var command = _command;
+ ResolveCommand(ref command, ref args);
+ var commandPath = Env.GetCommandPath(command, ".exe", ".cmd", "") ??
+ Env.GetCommandPathFromRootPath(_baseDirectory, command, ".exe", ".cmd", "");
+
+ Console.WriteLine($"Executing (Captured Output) - {commandPath} {args}");
+
+ var stdOut = new StreamForwarder();
+ var stdErr = new StreamForwarder();
+
+ stdOut.Capture();
+ stdErr.Capture();
+
+ return RunProcess(commandPath, args, stdOut, stdErr);
+ }
+
+ public void KillTree()
+ {
+ if (CurrentProcess == null)
+ {
+ throw new InvalidOperationException("No process is available to be killed");
+ }
+
+ CurrentProcess.KillTree();
+ }
+
+ private void ResolveCommand(ref string executable, ref string args)
+ {
+ if (executable.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ {
+ var newArgs = ArgumentEscaper.EscapeSingleArg(executable);
+ if (!string.IsNullOrEmpty(args))
+ {
+ newArgs += " " + args;
+ }
+ args = newArgs;
+ executable = "dotnet";
+ }
+
+ if (!Path.IsPathRooted(executable))
+ {
+ executable = Env.GetCommandPath(executable) ??
+ Env.GetCommandPathFromRootPath(_baseDirectory, executable);
+ }
+ }
+
+ private CommandResult RunProcess(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
+ {
+ CurrentProcess = StartProcess(executable, args);
+ var taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
+ var taskErr = stdErr.BeginRead(CurrentProcess.StandardError);
+
+ CurrentProcess.WaitForExit();
+ Task.WaitAll(taskOut, taskErr);
+
+ var result = new CommandResult(
+ CurrentProcess.StartInfo,
+ CurrentProcess.ExitCode,
+ stdOut.CapturedOutput,
+ stdErr.CapturedOutput);
+
+ return result;
+ }
+
+ private Task RunProcessAsync(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
+ {
+ CurrentProcess = StartProcess(executable, args);
+ var taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
+ var taskErr = stdErr.BeginRead(CurrentProcess.StandardError);
+
+ var tcs = new TaskCompletionSource();
+ CurrentProcess.Exited += (sender, arg) =>
+ {
+ Task.WaitAll(taskOut, taskErr);
+ var result = new CommandResult(
+ CurrentProcess.StartInfo,
+ CurrentProcess.ExitCode,
+ stdOut.CapturedOutput,
+ stdErr.CapturedOutput);
+ tcs.SetResult(result);
+ };
+
+ return tcs.Task;
+ }
+
+ private Process StartProcess(string executable, string args)
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = executable,
+ Arguments = args,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ RedirectStandardInput = true,
+ UseShellExecute = false
+ };
+
+ foreach (var item in Environment)
+ {
+#if NET451
+ psi.EnvironmentVariables[item.Key] = item.Value;
+#else
+ psi.Environment[item.Key] = item.Value;
+#endif
+ }
+
+ if (!string.IsNullOrWhiteSpace(WorkingDirectory))
+ {
+ psi.WorkingDirectory = WorkingDirectory;
+ }
+
+ var process = new Process
+ {
+ StartInfo = psi
+ };
+
+ process.EnableRaisingEvents = true;
+ process.Start();
+ return process;
+ }
+
+ public TestCommand WithEnvironmentVariable(string name, string value)
+ {
+ Environment.Add(name, value);
+
+ return this;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj b/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj
new file mode 100644
index 0000000..5c7eea6
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netcoreapp1.0;net451
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs
new file mode 100644
index 0000000..75f5447
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public class NetworkHelper
+ {
+ // in milliseconds
+ private const int Timeout = 50000;
+
+ public static string Localhost { get; } = "http://localhost";
+
+ public static bool IsServerUp(string url)
+ {
+ return SpinWait.SpinUntil(() =>
+ {
+ using (var client = new HttpClient())
+ {
+ try
+ {
+ client.BaseAddress = new Uri(url);
+ HttpResponseMessage response = client.GetAsync("").Result;
+ return response.IsSuccessStatusCode;
+ }
+ catch (Exception)
+ {
+ Thread.Sleep(100);
+ return false;
+ }
+ }
+ }, Timeout);
+ }
+
+ public static void TestGetRequest(string url, string expectedResponse)
+ {
+ using (var client = new HttpClient())
+ {
+ client.BaseAddress = new Uri(url);
+
+ HttpResponseMessage response = client.GetAsync("").Result;
+ if (response.IsSuccessStatusCode)
+ {
+ var responseString = response.Content.ReadAsStringAsync().Result;
+ responseString.Should().Contain(expectedResponse);
+ }
+ }
+ }
+
+ public static string GetLocalhostUrlWithFreePort()
+ {
+ return $"{Localhost}:{PortManager.GetPort()}/";
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs
new file mode 100644
index 0000000..303f89a
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public static class PortManager
+ {
+ private static int s_nextPort = 8001;
+
+ public static int GetPort()
+ {
+ return Interlocked.Increment(ref s_nextPort);
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs
new file mode 100644
index 0000000..1cbcf02
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public static class NuGetConfig
+ {
+ public static void Write(string directory)
+ {
+ var contents = @"
+
+
+
+
+
+
+
+";
+
+ var path = Path.Combine(directory, "NuGet.config");
+
+ File.WriteAllText(path, contents);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs
new file mode 100644
index 0000000..c82fd7a
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ internal static class ProcessExtensions
+ {
+#if NET451
+ private static readonly bool _isWindows = true;
+#else
+ private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+#endif
+ private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
+
+ public static void KillTree(this Process process)
+ {
+ process.KillTree(_defaultTimeout);
+ }
+
+ public static void KillTree(this Process process, TimeSpan timeout)
+ {
+ string stdout;
+ if (_isWindows)
+ {
+ RunProcessAndWaitForExit(
+ "taskkill",
+ $"/T /F /PID {process.Id}",
+ timeout,
+ out stdout);
+ }
+ else
+ {
+ var children = new HashSet();
+ GetAllChildIdsUnix(process.Id, children, timeout);
+ foreach (var childId in children)
+ {
+ KillProcessUnix(childId, timeout);
+ }
+ KillProcessUnix(process.Id, timeout);
+ }
+ }
+
+ private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout)
+ {
+ string stdout;
+ var exitCode = RunProcessAndWaitForExit(
+ "pgrep",
+ $"-P {parentId}",
+ timeout,
+ out stdout);
+
+ if (exitCode == 0 && !string.IsNullOrEmpty(stdout))
+ {
+ using (var reader = new StringReader(stdout))
+ {
+ while (true)
+ {
+ var text = reader.ReadLine();
+ if (text == null)
+ {
+ return;
+ }
+
+ int id;
+ if (int.TryParse(text, out id))
+ {
+ children.Add(id);
+ // Recursively get the children
+ GetAllChildIdsUnix(id, children, timeout);
+ }
+ }
+ }
+ }
+ }
+
+ private static void KillProcessUnix(int processId, TimeSpan timeout)
+ {
+ string stdout;
+ RunProcessAndWaitForExit(
+ "kill",
+ $"-TERM {processId}",
+ timeout,
+ out stdout);
+ }
+
+ private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false
+ };
+
+ var process = Process.Start(startInfo);
+
+ stdout = null;
+ if (process.WaitForExit((int)timeout.TotalMilliseconds))
+ {
+ stdout = process.StandardOutput.ReadToEnd();
+ }
+ else
+ {
+ process.Kill();
+ }
+
+ return process.ExitCode;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs
new file mode 100644
index 0000000..3b812ed
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public class ProjectUtils
+ {
+ public static string GetProjectJson(string testRoot, string project)
+ {
+ // We assume that the project name same as the directory name with contains the project.json
+ // We can do better here by using ProjectReader to get the correct project name
+ string projectPath = Directory.GetFiles(testRoot, "project.json", SearchOption.AllDirectories)
+ .FirstOrDefault(pj => Directory.GetParent(pj).Name.Equals(project));
+
+ if (string.IsNullOrEmpty(projectPath))
+ {
+ throw new Exception($"Cannot file project '{project}' in '{testRoot}'");
+ }
+
+ return projectPath;
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs
new file mode 100644
index 0000000..88c50a9
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Extensions.DependencyModel.Tests , PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.DotNet.Configurer.UnitTests , PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs
new file mode 100644
index 0000000..8baa5a1
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public sealed class DisposableDirectory : TempDirectory, IDisposable
+ {
+ public DisposableDirectory(TempRoot root)
+ : base(root)
+ {
+ }
+
+ public void Dispose()
+ {
+ if (Path != null && Directory.Exists(Path))
+ {
+ try
+ {
+ Directory.Delete(Path, recursive: true);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs
new file mode 100644
index 0000000..c17c1dd
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs
@@ -0,0 +1,83 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ public sealed class DisposableFile : TempFile, IDisposable
+ {
+ public DisposableFile(string path)
+ : base(path)
+ {
+ }
+
+ public DisposableFile(string prefix = null, string extension = null, string directory = null, string callerSourcePath = null, int callerLineNumber = 0)
+ : base(prefix, extension, directory, callerSourcePath, callerLineNumber)
+ {
+ }
+
+ public void Dispose()
+ {
+ if (Path != null && File.Exists(Path))
+ {
+ try
+ {
+ File.Delete(Path);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ try
+ {
+ // the file might still be memory-mapped, delete on close:
+ DeleteFileOnClose(Path);
+ }
+ catch (IOException ex)
+ {
+ throw new InvalidOperationException(string.Format(@"
+The file '{0}' seems to have been opened in a way that prevents us from deleting it on close.
+Is the file loaded as an assembly (e.g. via Assembly.LoadFile)?
+
+{1}: {2}", Path, ex.GetType().Name, ex.Message), ex);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // We should ignore this exception if we got it the second time,
+ // the most important reason is that the file has already been
+ // scheduled for deletion and will be deleted when all handles
+ // are closed.
+ }
+ }
+ }
+ }
+
+ [DllImport("kernel32.dll", PreserveSig = false)]
+ private static extern void SetFileInformationByHandle(SafeFileHandle handle, int fileInformationClass, ref uint fileDispositionInfoDeleteFile, int bufferSize);
+
+ private const int FileDispositionInfo = 4;
+
+ internal static void PrepareDeleteOnCloseStreamForDisposal(FileStream stream)
+ {
+ // tomat: Set disposition to "delete" on the stream, so to avoid ForeFront EndPoint
+ // Protection driver scanning the file. Note that after calling this on a file that's open with DeleteOnClose,
+ // the file can't be opened again, not even by the same process.
+ uint trueValue = 1;
+ SetFileInformationByHandle(stream.SafeFileHandle, FileDispositionInfo, ref trueValue, sizeof(uint));
+ }
+
+ ///
+ /// Marks given file for automatic deletion when all its handles are closed.
+ /// Note that after doing this the file can't be opened again, not even by the same process.
+ ///
+ internal static void DeleteFileOnClose(string fullPath)
+ {
+ using (var stream = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Delete | FileShare.ReadWrite, 8, FileOptions.DeleteOnClose))
+ {
+ PrepareDeleteOnCloseStreamForDisposal(stream);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs
new file mode 100644
index 0000000..625370b
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs
@@ -0,0 +1,183 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ ///
+ /// Implements a few file name utilities that are needed by the compiler.
+ /// In general the compiler is not supposed to understand the format of the paths.
+ /// In rare cases it needs to check if a string is a valid file name or change the extension
+ /// (embedded resources, netmodules, output name).
+ /// The APIs are intentionally limited to cover just these rare cases. Do not add more APIs.
+ ///
+ internal static class FileNameUtilities
+ {
+ private const string DirectorySeparatorStr = "\\";
+ internal const char DirectorySeparatorChar = '\\';
+ internal const char AltDirectorySeparatorChar = '/';
+ internal const char VolumeSeparatorChar = ':';
+
+ ///
+ /// Returns true if the string represents an unqualified file name.
+ /// The name may contain any characters but directory and volume separators.
+ ///
+ /// Path.
+ ///
+ /// True if is a simple file name, false if it is null or includes a directory specification.
+ ///
+ internal static bool IsFileName(string path)
+ {
+ return IndexOfFileName(path) == 0;
+ }
+
+ ///
+ /// Returns the offset in where the dot that starts an extension is, or -1 if the path doesn't have an extension.
+ ///
+ ///
+ /// Returns 0 for path ".foo".
+ /// Returns -1 for path "foo.".
+ ///
+ private static int IndexOfExtension(string path)
+ {
+ if (path == null)
+ {
+ return -1;
+ }
+
+ int length = path.Length;
+ int i = length;
+
+ while (--i >= 0)
+ {
+ char c = path[i];
+ if (c == '.')
+ {
+ if (i != length - 1)
+ {
+ return i;
+ }
+
+ return -1;
+ }
+
+ if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar || c == VolumeSeparatorChar)
+ {
+ break;
+ }
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Returns an extension of the specified path string.
+ ///
+ ///
+ /// The same functionality as but doesn't throw an exception
+ /// if there are invalid characters in the path.
+ ///
+ internal static string GetExtension(string path)
+ {
+ if (path == null)
+ {
+ return null;
+ }
+
+ int index = IndexOfExtension(path);
+ return (index >= 0) ? path.Substring(index) : string.Empty;
+ }
+
+ ///
+ /// Removes extension from path.
+ ///
+ ///
+ /// Returns "foo" for path "foo.".
+ /// Returns "foo.." for path "foo...".
+ ///
+ private static string RemoveExtension(string path)
+ {
+ if (path == null)
+ {
+ return null;
+ }
+
+ int index = IndexOfExtension(path);
+ if (index >= 0)
+ {
+ return path.Substring(0, index);
+ }
+
+ // trim last ".", if present
+ if (path.Length > 0 && path[path.Length - 1] == '.')
+ {
+ return path.Substring(0, path.Length - 1);
+ }
+
+ return path;
+ }
+
+ ///
+ /// Returns path with the extension changed to .
+ ///
+ ///
+ /// Equivalent of
+ ///
+ /// If is null, returns null.
+ /// If path does not end with an extension, the new extension is appended to the path.
+ /// If extension is null, equivalent to .
+ ///
+ internal static string ChangeExtension(string path, string extension)
+ {
+ if (path == null)
+ {
+ return null;
+ }
+
+ var pathWithoutExtension = RemoveExtension(path);
+ if (extension == null || path.Length == 0)
+ {
+ return pathWithoutExtension;
+ }
+
+ if (extension.Length == 0 || extension[0] != '.')
+ {
+ return pathWithoutExtension + "." + extension;
+ }
+
+ return pathWithoutExtension + extension;
+ }
+
+ ///
+ /// Returns the position in given path where the file name starts.
+ ///
+ /// -1 if path is null.
+ internal static int IndexOfFileName(string path)
+ {
+ if (path == null)
+ {
+ return -1;
+ }
+
+ for (int i = path.Length - 1; i >= 0; i--)
+ {
+ char ch = path[i];
+ if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
+ {
+ return i + 1;
+ }
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Get file name from path.
+ ///
+ /// Unlike doesn't check for invalid path characters.
+ internal static string GetFileName(string path)
+ {
+ int fileNameStart = IndexOfFileName(path);
+ return (fileNameStart <= 0) ? path : path.Substring(fileNameStart);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs
new file mode 100644
index 0000000..b3dcb57
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs
@@ -0,0 +1,43 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.IO;
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ ///
+ /// The collection of extension methods for the type
+ ///
+ public static class ImmutableArrayTestExtensions
+ {
+ ///
+ /// Writes read-only array of bytes to the specified file.
+ ///
+ /// Data to write to the file.
+ /// File path.
+ internal static void WriteToFile(this ImmutableArray bytes, string path)
+ {
+ Debug.Assert(!bytes.IsDefault);
+
+ const int bufferSize = 4096;
+ using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize))
+ {
+ // PERF: Consider using an ObjectPool here
+ byte[] buffer = new byte[Math.Min(bufferSize, bytes.Length)];
+
+ int offset = 0;
+ while (offset < bytes.Length)
+ {
+ int length = Math.Min(bufferSize, bytes.Length - offset);
+ bytes.CopyTo(offset, buffer, 0, length);
+ fileStream.Write(buffer, 0, length);
+ offset += length;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs
new file mode 100644
index 0000000..0c37a6f
--- /dev/null
+++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs
@@ -0,0 +1,43 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.Tools.Test.Utilities
+{
+ internal enum PathKind
+ {
+ ///