diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1fa778a9c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +This project has adopted the [Microsoft Open Source Code of +Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct +FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) +with any additional questions or comments. + +For our general contributing guidelines please see [our dotnet/runtime contributing guide](https://github.com/dotnet/runtime/blob/master/CONTRIBUTING.md). + +## Best practices + +* Use Windows PowerShell or [PowerShell Core][pwsh] (including on Linux/OSX) to run .ps1 scripts. + Some scripts set environment variables to help you, but they are only retained if you use PowerShell as your shell. + +## Prerequisites + +All dependencies can be installed by running the `init.ps1` script at the root of the repository +using Windows PowerShell or [PowerShell Core][pwsh] (on any OS). + +The only prerequisite for building, testing, and deploying from this repository +is the [.NET SDK](https://get.dot.net/). +You should install the version specified in `global.json` or a later version within +the same major.minor.Bxx "hundreds" band. +For example if 2.2.300 is specified, you may install 2.2.300, 2.2.301, or 2.2.310 +while the 2.2.400 version would not be considered compatible by .NET SDK. +See [.NET Core Versioning](https://docs.microsoft.com/en-us/dotnet/core/versions/) for more information. + +The development experience is best with [Visual Studio][VisualStudio]. + +## Building + +This repository can be built on Windows, Linux, and OSX. + +Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). + +[pwsh]: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-6 +[VisualStudio]: https://docs.microsoft.com/dotnet/core/install/sdk?pivots=os-windows#install-with-visual-studio diff --git a/README.md b/README.md index 050fb04eb..53ca5cdda 100644 --- a/README.md +++ b/README.md @@ -3,39 +3,38 @@ This repository contains the source code for .NET Portability Analyzer tools and dependencies. -|Branch|Build Status| -|---|---| -|master|[![][BuildStatus-Master]][myget]| -|dev|[![][BuildStatus-Dev]][myget]| +|Branch|Build Status +|---|---|---| +|master|[![Build Status](https://devdiv.visualstudio.com/DevDiv/_apis/build/status/CoreFxTools/dotnet-apiport-yaml?branchName=master)](https://devdiv.visualstudio.com/DevDiv/_build/latest?definitionId=12912&branchName=master) +|dev|[![Build Status](https://devdiv.visualstudio.com/DevDiv/_apis/build/status/CoreFxTools/dotnet-apiport-yaml?branchName=dev)](https://devdiv.visualstudio.com/DevDiv/_build/latest?definitionId=12912&branchName=dev) For a quick introduction, check out [this video on Channel 9][Channel 9 Video]: [][Channel 9 Video] -## Using this Repository +There is a Visual Studio extension available for VS 2017 and VS 2019: [.NET Portability Analyzer](https://marketplace.visualstudio.com/items?itemName=ConnieYau.NETPortabilityAnalyzer) -### Windows -There is a Visual Studio extension available for VS2017: [.NET Portability Analyzer](https://marketplace.visualstudio.com/items?itemName=ConnieYau.NETPortabilityAnalyzer) +## Using this Repository -Download and build for yourself: -1. Install [Visual Studio 2017 with .NET Core Workload][Visual Studio 2017] -2. Building: - * Visual Studio: `PortabilityTools.sln` - * Powershell: `.\build.ps1 -Configuration Debug -Platform AnyCPU` +See our [contributing guide](CONTRIBUTING.md) for instructions to +build and run from the source code in this repo. -### Linux/Mac +Sample usage to run the analysis from the command line: -1. Install [.NET Core SDK](https://dotnet.microsoft.com/download) -2. Execute: `build.sh` -3. Go to: `bin/Debug/ApiPort/netcoreapp2.1` -4. Run ApiPort by executing: `dotnet ApiPort.dll` - * Example: `dotnet ApiPort.dll listTargets` - * Example: `dotnet ApiPort.dll analyze -f Foo.dll -r HTML` -5. For convenience, create an alias command adding the following to your `~/.bash_profile`. Replace `{dotnet-apiport-folder}` with the path where you cloned the repo. +```ps1 +./init.ps1 +dotnet build src/ApiPort/ApiPort/ApiPort.csproj +dotnet bin/Debug/ApiPort/netcoreapp2.1/ApiPort.dll -- listTargets +dotnet bin/Debug/ApiPort/netcoreapp2.1/ApiPort.dll -- analyze -f Foo.dll -r HTML ``` + +If using bash for your shell, for convenience you may create an alias command adding the following to your `~/.bash_profile`. Replace `{dotnet-apiport-folder}` with the path where you cloned the repo. + +```bash alias apiport="dotnet {dotnet-apiport-folder}/bin/Debug/ApiPort/netcoreapp2.1/ApiPort.dll" ``` -This will alow you to use apiport globally from the command line: `apiport analyze -f Foo.dll -r HTML` + +This will allow you to use apiport globally from the command line: `apiport analyze -f Foo.dll -r HTML` ## Documentation @@ -43,7 +42,7 @@ This will alow you to use apiport globally from the command line: `apiport analy * [Platform Portability](docs/HowTo/PlatformPortability.md) * [Breaking Changes](docs/HowTo/BreakingChanges.md) * [.NET Portability Analyzer (Console application)](docs/Console) - * [.NET Core application](docs/Console/README.md#using-net-core-application) + * [.NET Core application](docs/Console/README.md#using-net-core-application) * [.NET Portability Analyzer (Visual Studio extension)](docs/VSExtension) ## Projects @@ -93,8 +92,6 @@ For an overview of all the .NET related projects, have a look at the This project is licensed under the [MIT license](LICENSE). -[BuildStatus-Master]: https://devdiv.visualstudio.com/_apis/public/build/definitions/0bdbc590-a062-4c3f-b0f6-9383f67865ee/484/badge -[BuildStatus-Dev]: https://devdiv.visualstudio.com/_apis/public/build/definitions/0bdbc590-a062-4c3f-b0f6-9383f67865ee/7913/badge [Channel 9 Video]: https://channel9.msdn.com/Blogs/Seth-Juarez/A-Brief-Look-at-the-NET-Portability-Analyzer [Contributing Guide]: https://github.com/dotnet/corefx/wiki/Contributing [Developer Guide]: https://github.com/dotnet/corefx/wiki/Developer-Guide @@ -105,5 +102,4 @@ This project is licensed under the [MIT license](LICENSE). [PR-Open]: https://github.com/Microsoft/dotnet-apiport/pulls?q=is%3Aopen+is%3Apr [myget]: https://dotnet.myget.org/gallery/dotnet-apiport [System.Reflection.Metadata]: https://github.com/dotnet/corefx/tree/master/src/System.Reflection.Metadata -[Visual Studio 2017]: https://docs.microsoft.com/dotnet/core/install/sdk?pivots=os-windows#install-with-visual-studio [VSIX Gallery]: http://vsixgallery.com/extension/55d15546-28ca-40dc-af23-dfa503e9c5fe diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cfcecac74..5ae36577f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,7 +17,7 @@ jobs: - checkout: self clean: true - - powershell: .\init.ps1 + - powershell: .\init.ps1 -UpgradePrerequisites -NoRestore displayName: init.ps1 script - task: securedevelopmentteam.vss-secure-development-tools.build-task-policheck.PoliCheck@1 @@ -169,6 +169,7 @@ jobs: tests/**/*[Tt]ests/*.csproj !tests/**/*/ApiPortVS.Tests.csproj arguments: '--configuration $(BuildConfiguration) --framework $(BuildNetCoreAppTarget) --no-build' + enabled: false # it wasn't testing anything anyway. - task: DotNetCoreCLI@2 displayName: dotnet publish diff --git a/azure-pipelines/Get-NuGetTool.ps1 b/azure-pipelines/Get-NuGetTool.ps1 new file mode 100644 index 000000000..4431adb91 --- /dev/null +++ b/azure-pipelines/Get-NuGetTool.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Downloads the NuGet.exe tool and returns the path to it. +.PARAMETER NuGetVersion + The version of the NuGet tool to acquire. +#> +Param( + [Parameter()] + [string]$NuGetVersion='5.2.0' +) + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath $NuGetVersion +if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } +$nugetPath = Join-Path $binaryToolsPath nuget.exe + +if (!(Test-Path $nugetPath)) { + Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow + (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) +} + +return (Resolve-Path $nugetPath).Path diff --git a/azure-pipelines/Get-ProcDump.ps1 b/azure-pipelines/Get-ProcDump.ps1 new file mode 100644 index 000000000..1493fe4b2 --- /dev/null +++ b/azure-pipelines/Get-ProcDump.ps1 @@ -0,0 +1,14 @@ +<# +.SYNOPSIS +Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. +#> +$version = '0.0.1' +$baseDir = "$PSScriptRoot\..\obj\tools" +$procDumpToolPath = "$baseDir\procdump.$version\bin" +if (-not (Test-Path $procDumpToolPath)) { + if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } + $baseDir = (Resolve-Path $baseDir).Path # Normalize it + & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null +} + +(Resolve-Path $procDumpToolPath).Path diff --git a/azure-pipelines/Get-TempToolsPath.ps1 b/azure-pipelines/Get-TempToolsPath.ps1 new file mode 100644 index 000000000..bb3da8e33 --- /dev/null +++ b/azure-pipelines/Get-TempToolsPath.ps1 @@ -0,0 +1,13 @@ +if ($env:AGENT_TEMPDIRECTORY) { + $path = "$env:AGENT_TEMPDIRECTORY\$env:BUILD_BUILDID" +} elseif ($env:localappdata) { + $path = "$env:localappdata\gitrepos\tools" +} else { + $path = "$PSScriptRoot\..\obj\tools" +} + +if (!(Test-Path $path)) { + New-Item -ItemType Directory -Path $Path | Out-Null +} + +(Resolve-Path $path).Path diff --git a/azure-pipelines/Set-EnvVars.ps1 b/azure-pipelines/Set-EnvVars.ps1 new file mode 100644 index 000000000..9d14d9aa0 --- /dev/null +++ b/azure-pipelines/Set-EnvVars.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Set environment variables in the environment. + Azure Pipeline and CMD environments are considered. +.PARAMETER Variables + A hashtable of variables to be set. +.OUTPUTS + A boolean indicating whether the environment variables can be expected to propagate to the caller's environment. +#> +[CmdletBinding(SupportsShouldProcess=$true)] +Param( + [Parameter(Mandatory=$true, Position=1)] + $Variables, + [string[]]$PrependPath +) + +if ($Variables.Count -eq 0) { + return $true +} + +$cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and $env:PS1UnderCmd -eq '1' +if ($cmdInstructions) { + Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" + Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue +} else { + Write-Host "Environment variables set:" -ForegroundColor Blue + Write-Host ($Variables | Out-String) + if ($PrependPath) { + Write-Host "Paths prepended to PATH: $PrependPath" + } +} + +if ($env:TF_BUILD) { + Write-Host "Azure Pipelines detected. Logging commands will be used to propagate environment variables and prepend path." +} + +if ($env:GITHUB_ACTIONS) { + Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." +} + +$Variables.GetEnumerator() |% { + Set-Item -Path env:$($_.Key) -Value $_.Value + + # If we're running in a cloud CI, set these environment variables so they propagate. + if ($env:TF_BUILD) { + Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" + } + if ($env:GITHUB_ACTIONS) { + Write-Host "::set-env name=$($_.Key)::$($_.Value)" + } + + if ($cmdInstructions) { + Write-Host "SET $($_.Key)=$($_.Value)" + } +} + +$pathDelimiter = ';' +if ($IsMacOS -or $IsLinux) { + $pathDelimiter = ':' +} + +if ($PrependPath) { + $PrependPath |% { + $newPathValue = "$_$pathDelimiter$env:PATH" + Set-Item -Path env:PATH -Value $newPathValue + if ($cmdInstructions) { + Write-Host "SET PATH=$newPathValue" + } + + if ($env:TF_BUILD) { + Write-Host "##vso[task.prependpath]$_" + } + if ($env:GITHUB_ACTIONS) { + Write-Host "::add-path::$_" + } + } +} + +return !$cmdInstructions diff --git a/azure-pipelines/variables/DotNetSdkVersion.ps1 b/azure-pipelines/variables/DotNetSdkVersion.ps1 new file mode 100644 index 000000000..b213fbc27 --- /dev/null +++ b/azure-pipelines/variables/DotNetSdkVersion.ps1 @@ -0,0 +1,2 @@ +$globalJson = Get-Content -Path "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json +$globalJson.sdk.version diff --git a/build.sh b/build.sh deleted file mode 100755 index 440b90c31..000000000 --- a/build.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env bash -set -e - -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true -export HOME=~ -export NUGET_PACKAGES=~/.nuget/packages -export NUGET_HTTP_CACHE_PATH=~/.local/share/NuGet/v3-cache - -Configuration=Debug - -RootDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DotNetSDKPath=$RootDir"/.tools/dotnet/"$DotNetSDKVersion -DotNetExe=$DotNetSDKPath"/dotnet" - -BuildApps=true -RunTests=false - -usage() { - echo "Usage: build.sh [-c|--configuration ] [--downloadCatalog] [--runTests] [--no-build]" -} - -downloadCatalog() { - local isForce=$1 - local catalog=$RootDir"/.data/catalog.bin" - local data=$(dirname $catalog) - - if [[ ! -e $data ]]; then - mkdir $data - fi - - if [[ $isForce == "true" && -e $catalog ]]; then - echo "Deleting existing catalog" - rm $catalog - fi - - if [[ ! -e $catalog ]]; then - echo "Downloading catalog.bin..." - curl --output $catalog "https://portability.blob.core.windows.net/catalog/catalog.bin" - fi -} - -installSDK() { - local dotnetPath=$(command -v dotnet) - - if [[ $dotnetPath != "" ]]; then - echo "dotnet is found on PATH. using that." - DotNetExe=$dotnetPath - return 0 - fi - - if [[ -e $DotNetExe ]]; then - echo $DotNetExe" exists. Skipping install..." - return 0 - fi - - local DotNetToolsPath=$(dirname $DotNetSDKPath) - - if [ ! -d $DotNetToolsPath ]; then - mkdir -p $DotNetToolsPath - fi - - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel Current --install-dir $DotNetSDKPath -} - -tryGetTargetFramework() { - local file=$1 - local targetFramework=$(awk -F: '/.*netcoreapp[1-9]\.[0-9].*<\/TargetFramework(s)?>/ { print $0 }' $file | sed 's/.*\(netcoreapp[1-9]\.[0-9]\).*/\1/' | tail -n 1) - echo $targetFramework -} - -build() { - echo "Building ApiPort... Configuration: ["$Configuration"]" - - pushd src/ApiPort/ApiPort >/dev/null - - local targetFramework=$(tryGetTargetFramework ApiPort.props) - echo "Building for: $targetFramework" - - $DotNetExe build ApiPort.csproj -f $targetFramework -c $Configuration - $DotNetExe build ApiPort.Offline.csproj -f $targetFramework -c $Configuration - popd >/dev/null -} - -runTest() { - ls $1/*.csproj | while read file; do - local targetFramework=$(tryGetTargetFramework $file) - - if [[ $targetFramework == "" ]]; then - echo "Skipping "$file - echo "--- Desktop .NET Framework testing is not currently supported on Unix." - else - echo "Testing "$file - $DotNetExe test $file -c $Configuration --logger trx --framework $targetFramework --results-directory $2 - fi - done -} - -findAndRunTests() { - local testResultsDirectory=$COMMON_TESTRESULTSDIRECTORY - - if [[ $testResultsDirectory == "" ]]; then - testResultsDirectory=$RootDir/TestResults - echo "Results directory not specified, using $testResultsDirectory." - else - echo "Using common one set by build agent." - fi - - find tests/ -type d -name "*\.Tests" | while read file; do - runTest $file $testResultsDirectory - done -} - -while [[ $# -gt 0 ]]; do - option="$(echo $1 | awk '{print tolower($0)}')" - case "$option" in - "-?" | "--help") - usage - exit 1 - ;; - "-c" | "--configuration") - Configuration="$2" - shift 2 - ;; - "--runtests") - RunTests=true - shift - ;; - "--no-build") - BuildApps=false - shift - ;; - "--downloadcatalog") - downloadCatalog "true" - exit 0 - ;; - *) - echo "Unknown option: "$option - usage - exit 1 - ;; - esac -done - -# Enable insensitive case-matching -shopt -s nocasematch - -if [[ "$Configuration" != "Debug" && "$Configuration" != "Release" ]]; then - echo "ERROR: Supported configuration types are Debug or Release. Invalid configuration: "$Configuration - usage - exit 3 -fi - -shopt -u nocasematch - -installSDK - -if [[ ! -e $DotNetExe ]]; then - echo "ERROR: It should have been installed from build/dotnet-install.sh" - exit 2 -fi - -if [[ $BuildApps == "true" ]]; then - downloadCatalog - build -fi - -if [[ $RunTests == "true" ]]; then - if [[$BuildApps != "true" ]]; then - echo "WARNING: Not building applications because --no-build is set." - fi - - findAndRunTests -fi - -echo "Finished!" diff --git a/init.cmd b/init.cmd new file mode 100644 index 000000000..970285c2f --- /dev/null +++ b/init.cmd @@ -0,0 +1,4 @@ +@echo off +SETLOCAL +set PS1UnderCmd=1 +powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }" diff --git a/init.ps1 b/init.ps1 old mode 100644 new mode 100755 index 341bd8b83..42c5a2fce --- a/init.ps1 +++ b/init.ps1 @@ -1,31 +1,81 @@ -$ErrorActionPreference = "Stop" +#!/usr/bin/env pwsh -function DownloadFile($url, $outputPath) { - Write-Host "Attempt to download to $outputPath" +<# +.SYNOPSIS +Installs dependencies required to build and test the projects in this repository. +.DESCRIPTION +This MAY not require elevation, as the SDK and runtimes are installed to a per-user location, +unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location. +See detailed help on that switch for more information. +.PARAMETER InstallLocality +A value indicating whether dependencies should be installed locally to the repo or at a per-user location. +Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. +Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. +Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. +When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. +Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. +Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. +.PARAMETER NoPrerequisites +Skips the installation of prerequisite software (e.g. SDKs, tools). +.PARAMETER UpgradePrerequisites +Takes time to install prerequisites even if they are already present in case they need to be upgraded. +No effect if -NoPrerequisites is specified. +.PARAMETER NoRestore +Skips the package restore step. +.PARAMETER AccessToken +An optional access token for authenticating to Azure Artifacts authenticated feeds. +#> +[CmdletBinding(SupportsShouldProcess=$true)] +Param ( + [ValidateSet('repo','user','machine')] + [string]$InstallLocality='user', + [Parameter()] + [switch]$NoPrerequisites, + [Parameter()] + [switch]$UpgradePrerequisites, + [Parameter()] + [switch]$NoRestore, + [Parameter()] + [string]$AccessToken +) - # If the file has been downloaded don't download again. An empty file implies a failed download - if(Test-Path $outputPath) { - $file = Get-ChildItem $outputPath +$EnvVars = @{} - if($file.Length -gt 0) { - Write-Host "$outputPath is already downloaded" - return; - } - } +if (!$NoPrerequisites) { + & "$PSScriptRoot\tools\Install-NuGetCredProvider.ps1" -AccessToken $AccessToken -Force:$UpgradePrerequisites + & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality - try { - # Create placeholder so directory exists - New-Item -Type File $OutputPath -Force | Out-Null + # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests. + # But it only works on Windows. + if ($env:OS -eq 'Windows_NT') { + $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1" + } +} - # Attempt to download. If fails, placeholder remains so msbuild won't complain - Invoke-WebRequest $url -OutFile $OutputPath | Out-Null +& "$PSScriptRoot\tools\Get-CatalogBin.ps1" - Write-Host "Downloaded $OutputPath" - } catch { - Write-Error "Failed to download '$url'. $($Error[0])" - } -} +# Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines +$env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS=20 +$env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS=20 + +Push-Location $PSScriptRoot +try { + $HeaderColor = 'Green' -$address = "https://portability.blob.core.windows.net/catalog/catalog.bin" + if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { + Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor + dotnet restore + if ($lastexitcode -ne 0) { + throw "Failure while restoring packages." + } + } -DownloadFile "$address" "$PSScriptRoot\.data\catalog.bin" + & "$PSScriptRoot\azure-pipelines\Set-EnvVars.ps1" -Variables $EnvVars | Out-Null +} +catch { + Write-Error $error[0] + exit $lastexitcode +} +finally { + Pop-Location +} diff --git a/tools/Get-CatalogBin.ps1 b/tools/Get-CatalogBin.ps1 new file mode 100644 index 000000000..b998c52af --- /dev/null +++ b/tools/Get-CatalogBin.ps1 @@ -0,0 +1,31 @@ +$ErrorActionPreference = "Stop" + +function DownloadFile($url, $outputPath) { + Write-Host "Attempt to download to $outputPath" + + # If the file has been downloaded don't download again. An empty file implies a failed download + if(Test-Path $outputPath) { + $file = Get-ChildItem $outputPath + + if($file.Length -gt 0) { + Write-Host "$outputPath is already downloaded" + return; + } + } + + try { + # Create placeholder so directory exists + New-Item -Type File $OutputPath -Force | Out-Null + + # Attempt to download. If fails, placeholder remains so msbuild won't complain + Invoke-WebRequest $url -OutFile $OutputPath | Out-Null + + Write-Host "Downloaded $OutputPath" + } catch { + Write-Error "Failed to download '$url'. $($Error[0])" + } +} + +$address = "https://portability.blob.core.windows.net/catalog/catalog.bin" + +DownloadFile "$address" "$PSScriptRoot\..\.data\catalog.bin" diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 new file mode 100644 index 000000000..2a141327d --- /dev/null +++ b/tools/Install-DotNetSdk.ps1 @@ -0,0 +1,159 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS +Installs the .NET SDK specified in the global.json file at the root of this repository, +along with supporting .NET Core runtimes used for testing. +.DESCRIPTION +This MAY not require elevation, as the SDK and runtimes are installed locally to this repo location, +unless `-InstallLocality machine` is specified. +.PARAMETER InstallLocality +A value indicating whether dependencies should be installed locally to the repo or at a per-user location. +Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. +Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. +Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. +When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. +Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. +Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. +#> +[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] +Param ( + [ValidateSet('repo','user','machine')] + [string]$InstallLocality='user' +) + +$DotNetInstallScriptRoot = "$PSScriptRoot/../obj/tools" +if (!(Test-Path $DotNetInstallScriptRoot)) { New-Item -ItemType Directory -Path $DotNetInstallScriptRoot -WhatIf:$false | Out-Null } +$DotNetInstallScriptRoot = Resolve-Path $DotNetInstallScriptRoot + +# Look up actual required .NET Core SDK version from global.json +$sdkVersion = & "$PSScriptRoot/../azure-pipelines/variables/DotNetSdkVersion.ps1" + +# Search for all .NET Core runtime versions referenced from MSBuild projects and arrange to install them. +$runtimeVersions = @() +Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\tests\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% { + $projXml = [xml](Get-Content -Path $_) + $targetFrameworks = $projXml.Project.PropertyGroup.TargetFramework + if (!$targetFrameworks) { + $targetFrameworks = $projXml.Project.PropertyGroup.TargetFrameworks + if ($targetFrameworks) { + $targetFrameworks = $targetFrameworks -Split ';' + } + } + $targetFrameworks |? { $_ -match 'netcoreapp(\d+\.\d+)' } |% { + $runtimeVersions += $Matches[1] + } +} + +Function Get-FileFromWeb([Uri]$Uri, $OutDir) { + $OutFile = Join-Path $OutDir $Uri.Segments[-1] + if (!(Test-Path $OutFile)) { + Write-Verbose "Downloading $Uri..." + try { + (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) + } finally { + # This try/finally causes the script to abort + } + } + + $OutFile +} + +Function Get-InstallerExe($Version, [switch]$Runtime) { + $sdkOrRuntime = 'Sdk' + if ($Runtime) { $sdkOrRuntime = 'Runtime' } + + # Get the latest/actual version for the specified one + if (([Version]$Version).Build -eq -1) { + $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/latest.version" -UseBasicParsing) + $Version = $versionInfo[-1] + } + + Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/dotnet-$($sdkOrRuntime.ToLowerInvariant())-$Version-win-x64.exe" -OutDir "$DotNetInstallScriptRoot" +} + +Function Install-DotNet($Version, [switch]$Runtime) { + if ($Runtime) { $sdkSubstring = '' } else { $sdkSubstring = 'SDK ' } + Write-Host "Downloading .NET Core $sdkSubstring$Version..." + $Installer = Get-InstallerExe -Version $Version -Runtime:$Runtime + Write-Host "Installing .NET Core $sdkSubstring$Version..." + cmd /c start /wait $Installer /install /quiet + if ($LASTEXITCODE -ne 0) { + throw "Failure to install .NET Core SDK" + } +} + +$switches = @( + '-Architecture','x64' +) +$envVars = @{ + # For locally installed dotnet, skip first time experience which takes a long time + 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' = 'true'; +} + +if ($InstallLocality -eq 'machine') { + if ($IsWindows) { + if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { + Install-DotNet -Version $sdkVersion + } + + $runtimeVersions | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { + Install-DotNet -Version $_ -Runtime + } + } + + return + } else { + $DotNetInstallDir = '/usr/share/dotnet' + } +} elseif ($InstallLocality -eq 'repo') { + $DotNetInstallDir = "$DotNetInstallScriptRoot/.dotnet" +} elseif ($env:AGENT_TOOLSDIRECTORY) { + $DotNetInstallDir = "$env:AGENT_TOOLSDIRECTORY/dotnet" +} else { + $DotNetInstallDir = Join-Path $HOME .dotnet +} + +Write-Host "Installing .NET Core SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue + +if ($DotNetInstallDir) { + $switches += '-InstallDir',$DotNetInstallDir + $envVars['DOTNET_MULTILEVEL_LOOKUP'] = '0' + $envVars['DOTNET_ROOT'] = $DotNetInstallDir +} + +if ($IsMacOS -or $IsLinux) { + $DownloadUri = "https://dot.net/v1/dotnet-install.sh" + $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh" +} else { + $DownloadUri = "https://dot.net/v1/dotnet-install.ps1" + $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1" +} + +if (-not (Test-Path $DotNetInstallScriptPath)) { + Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing + if ($IsMacOS -or $IsLinux) { + chmod +x $DotNetInstallScriptPath + } +} + +if ($PSCmdlet.ShouldProcess(".NET Core SDK $sdkVersion", "Install")) { + Invoke-Expression -Command "$DotNetInstallScriptPath -Version $sdkVersion $switches" +} else { + Invoke-Expression -Command "$DotNetInstallScriptPath -Version $sdkVersion $switches -DryRun" +} + +$switches += '-Runtime','dotnet' + +$runtimeVersions | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { + Invoke-Expression -Command "$DotNetInstallScriptPath -Channel $_ $switches" + } else { + Invoke-Expression -Command "$DotNetInstallScriptPath -Channel $_ $switches -DryRun" + } +} + +if ($PSCmdlet.ShouldProcess("Set DOTNET environment variables to discover these installed runtimes?")) { + & "$PSScriptRoot/../azure-pipelines/Set-EnvVars.ps1" -Variables $envVars -PrependPath $DotNetInstallDir | Out-Null +} diff --git a/tools/Install-NuGetCredProvider.ps1 b/tools/Install-NuGetCredProvider.ps1 new file mode 100644 index 000000000..2b3fb6fb4 --- /dev/null +++ b/tools/Install-NuGetCredProvider.ps1 @@ -0,0 +1,74 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Downloads and installs the Microsoft Artifacts Credential Provider + from https://github.com/microsoft/artifacts-credprovider + to assist in authenticating to Azure Artifact feeds in interactive development + or unattended build agents. +.PARAMETER Force + Forces install of the CredProvider plugin even if one already exists. This is useful to upgrade an older version. +.PARAMETER AccessToken + An optional access token for authenticating to Azure Artifacts authenticated feeds. +#> +[CmdletBinding()] +Param ( + [Parameter()] + [switch]$Force, + [Parameter()] + [string]$AccessToken +) + +$toolsPath = & "$PSScriptRoot\..\azure-pipelines\Get-TempToolsPath.ps1" + +if ($IsMacOS -or $IsLinux) { + $installerScript = "installcredprovider.sh" + $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" +} else { + $installerScript = "installcredprovider.ps1" + $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1" +} + +$installerScript = Join-Path $toolsPath $installerScript + +if (!(Test-Path $installerScript)) { + Invoke-WebRequest $sourceUrl -OutFile $installerScript +} + +$installerScript = (Resolve-Path $installerScript).Path + +if ($IsMacOS -or $IsLinux) { + chmod u+x $installerScript +} + +& $installerScript -Force:$Force + +if ($AccessToken) { + $endpoints = @() + + $endpointURIs = @() + Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% { + $nugetConfig = [xml](Get-Content -Path $_) + + $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% { + if ($endpointURIs -notcontains $_.Value) { + $endpointURIs += $_.Value + $endpoint = New-Object -TypeName PSObject + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name endpoint -Value $_.value + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name username -Value ado + Add-Member -InputObject $endpoint -MemberType NoteProperty -Name password -Value $AccessToken + $endpoints += $endpoint + } + } + } + + $auth = New-Object -TypeName PSObject + Add-Member -InputObject $auth -MemberType NoteProperty -Name endpointCredentials -Value $endpoints + + $authJson = ConvertTo-Json -InputObject $auth + $envVars = @{ + 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'=$authJson; + } + + & "$PSScriptRoot\..\azure-pipelines\Set-EnvVars.ps1" -Variables $envVars | Out-Null +}