build-utils.ps1 15.5 KB
Newer Older
J
Jared Parsons 已提交
1 2 3 4 5 6 7 8
# Collection of powershell build utility functions that we use across our scripts.

Set-StrictMode -version 2.0
$ErrorActionPreference="Stop"

# Declare a number of useful variables for other scripts to use
[string]$repoDir = Resolve-Path (Join-Path $PSScriptRoot "..\..")
[string]$binariesDir = Join-Path $repoDir "Binaries"
9 10 11

# Handy function for executing a command in powershell and throwing if it 
# fails.  
J
Jared Parsons 已提交
12 13 14 15 16
#
# Use this when the full command is known at script authoring time and 
# doesn't require any dynamic argument build up.  Example:
#
#   Exec-Block { & $msbuild Test.proj }
17 18
# 
# Original sample came from: http://jameskovacs.com/2010/02/25/the-exec-problem/
J
Jared Parsons 已提交
19 20
function Exec-Block([scriptblock]$cmd) {
    & $cmd
21 22 23 24

    # Need to check both of these cases for errors as they represent different items
    # - $?: did the powershell script block throw an error
    # - $lastexitcode: did a windows command executed by the script block end in error
25
    if ((-not $?) -or ($lastexitcode -ne 0)) {
J
Jared Parsons 已提交
26
        throw "Command failed to execute: $cmd"
27 28 29
    } 
}

J
Jared Parsons 已提交
30
function Exec-CommandCore([string]$command, [string]$commandArgs, [switch]$useConsole = $true) {
J
Jared Parsons 已提交
31 32 33 34 35
    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.FileName = $command
    $startInfo.Arguments = $commandArgs

    $startInfo.UseShellExecute = $false
36
    $startInfo.WorkingDirectory = Get-Location
J
Jared Parsons 已提交
37

J
Jared Parsons 已提交
38 39 40 41 42
    if (-not $useConsole) {
       $startInfo.RedirectStandardOutput = $true
       $startInfo.CreateNoWindow = $true
    }

J
Jared Parsons 已提交
43 44 45 46 47 48
    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $startInfo
    $process.Start() | Out-Null

    $finished = $false
    try {
J
Jared Parsons 已提交
49 50 51 52 53 54 55 56 57 58 59
        if (-not $useConsole) { 
            # The OutputDataReceived event doesn't fire as events are sent by the 
            # process in powershell.  Possibly due to subtlties of how Powershell
            # manages the thread pool that I'm not aware of.  Using blocking
            # reading here as an alternative which is fine since this blocks 
            # on completion already.
            $out = $process.StandardOutput
            while (-not $out.EndOfStream) {
                $line = $out.ReadLine()
                Write-Output $line
            }
J
Jared Parsons 已提交
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
        }

        while (-not $process.WaitForExit(100)) { 
            # Non-blocking loop done to allow ctr-c interrupts
        }

        $finished = $true
        if ($process.ExitCode -ne 0) { 
            throw "Command failed to execute: $command $commandArgs" 
        }
    }
    finally {
        # If we didn't finish then an error occured or the user hit ctrl-c.  Either
        # way kill the process
        if (-not $finished) {
            $process.Kill()
        }
    }
J
Jared Parsons 已提交
78 79
}

J
Jared Parsons 已提交
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
# Handy function for executing a windows command which needs to go through 
# windows command line parsing.  
#
# Use this when the command arguments are stored in a variable.  Particularly 
# when the variable needs reparsing by the windows command line. Example:
#
#   $args = "/p:ManualBuild=true Test.proj"
#   Exec-Command $msbuild $args
# 
function Exec-Command([string]$command, [string]$commandArgs) {
    Exec-CommandCore -command $command -commandArgs $commandargs -useConsole:$false
}

# Functions exactly like Exec-Command but lets the process re-use the current 
# console. This means items like colored output will function correctly.
#
# In general this command should be used in place of
#   Exec-Command $msbuild $args | Out-Host
#
function Exec-Console([string]$command, [string]$commandArgs) {
    Exec-CommandCore -command $command -commandArgs $commandargs -useConsole:$true
}

J
Jared Parsons 已提交
103 104 105 106 107
# Handy function for executing a powershell script in a clean environment with 
# arguments.  Prefer this over & sourcing a script as it will both use a clean
# environment and do proper error checking
function Exec-Script([string]$script, [string]$scriptArgs = "") {
    Exec-Command "powershell" "-noprofile -executionPolicy RemoteSigned -file `"$script`" $scriptArgs"
108 109
}

J
Jared Parsons 已提交
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
# Ensure that NuGet is installed and return the path to the 
# executable to use.
function Ensure-NuGet() {
    $nugetVersion = Get-ToolVersion "nugetExe"
    $toolsDir = Join-Path $binariesDir "Tools"
    Create-Directory $toolsDir

    $destFile = Join-Path $toolsDir "NuGet.exe"
    $versionFile = Join-Path $toolsDir "NuGet.exe.version"

    # Check and see if we already have a NuGet.exe which exists and is the correct
    # version.
    if ((Test-Path $destFile) -and (Test-Path $versionFile)) {
        $scratchVersion = Get-Content $versionFile
        if ($scratchVersion -eq $nugetVersion) {
            return $destFile
        }
    }

    Write-Host "Downloading NuGet.exe"
    $webClient = New-Object -TypeName "System.Net.WebClient"
    $webClient.DownloadFile("https://dist.nuget.org/win-x86-commandline/v$nugetVersion/NuGet.exe", $destFile)
    $nugetVersion | Out-File $versionFile
    return $destFile
}

136
# Ensure the proper SDK in installed in our %PATH%. This is how MSBuild locates the 
137
# SDK. Returns the location to the dotnet exe
J
Jared Parsons 已提交
138 139 140 141 142 143 144 145
function Ensure-DotnetSdk() {

    # Check to see if the specified dotnet installations meets our build requirements
    function Test-DotnetDir([string]$dotnetDir, [string]$runtimeVersion, [string]$sdkVersion) {
        $sdkPath = Join-Path $dotnetDir "sdk\$sdkVersion"
        $runtimePath = Join-Path $dotnetDir "shared\Microsoft.NETCore.App\$runtimeVersion"
        return (Test-Path $sdkPath) -and (Test-Path $runtimePath)
    }
146

J
Jared Parsons 已提交
147
    $sdkVersion = Get-ToolVersion "dotnetSdk"
J
Jared Parsons 已提交
148
    $runtimeVersion = Get-ToolVersion "dotnetRuntime"
149 150 151 152 153 154 155 156 157 158 159 160

    # Get the path to dotnet.exe. This is the first path on %PATH% that contains the 
    # dotnet.exe instance. Many SDK tools use this to locate items like the SDK.
    function Get-DotnetDir() { 
        foreach ($part in ${env:PATH}.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) {
            $dotnetExe = Join-Path $part "dotnet.exe"
            if (Test-Path $dotnetExe) {
                return $part
            }
        }

        return $null
161 162
    }

163 164
    # First check that dotnet is already on the path with the correct SDK version
    $dotnetDir = Get-DotnetDir
J
Jared Parsons 已提交
165 166
    if (($dotnetDir -ne $null) -and (Test-DotnetDir $dotnetDir $runtimeVersion $sdkVersion)) { 
        return (Join-Path $dotnetDir "dotnet.exe")
167 168 169 170
    }

    # Ensure the downloaded dotnet of the appropriate version is located in the 
    # Binaries\Tools directory
171 172
    $toolsDir = Join-Path $binariesDir "Tools"
    $cliDir = Join-Path $toolsDir "dotnet"
J
Jared Parsons 已提交
173
    if (-not (Test-DotnetDir $cliDir $runtimeVersion $sdkVersion)) {
174 175 176 177 178 179
        Write-Host "Downloading CLI $sdkVersion"
        Create-Directory $cliDir
        Create-Directory $toolsDir
        $destFile = Join-Path $toolsDir "dotnet-install.ps1"
        $webClient = New-Object -TypeName "System.Net.WebClient"
        $webClient.DownloadFile("https://dot.net/v1/dotnet-install.ps1", $destFile)
J
Jared Parsons 已提交
180
        Exec-Block { & $destFile -Version $sdkVersion -InstallDir $cliDir } | Out-Null
J
Jared Parsons 已提交
181
        Exec-Block { & $destFile -Version $runtimeVersion -SharedRuntime -InstallDir $cliDir } | Out-Null
182 183
    }

J
Jared Parsons 已提交
184
    return (Join-Path $cliDir "dotnet.exe")
185 186
}

J
Jared Parsons 已提交
187 188
# Ensure a basic tool used for building our Repo is installed and 
# return the path to it.
J
Jared Parsons 已提交
189 190 191 192 193
function Ensure-BasicTool([string]$name, [string]$version = "") {
    if ($version -eq "") { 
        $version = Get-PackageVersion $name
    }

194
    $p = Join-Path (Get-PackagesDir) "$($name)\$($version)"
J
Jared Parsons 已提交
195
    if (-not (Test-Path $p)) {
196 197
        $toolsetProject = Join-Path $repoDir "build\ToolsetPackages\RoslynToolset.csproj"
        $dotnet = Ensure-DotnetSdk
198
        Write-Host "Downloading $name"
199
        Restore-Project $dotnet $toolsetProject
J
Jared Parsons 已提交
200 201 202 203 204
    }
    
    return $p
}

J
Jared Parsons 已提交
205 206
# Ensure that MSBuild is installed and return the path to the
# executable to use.
J
Jared Parsons 已提交
207 208 209 210
function Ensure-MSBuild([switch]$xcopy = $false) {
    $both = Get-MSBuildKindAndDir -xcopy:$xcopy
    $msbuildDir = $both[1]
    switch ($both[0]) {
211 212 213
        "xcopy" { break; }
        "vscmd" { break; }
        "vsinstall" { break; }
J
Jared Parsons 已提交
214 215 216 217 218 219
        default {
            throw "Unknown MSBuild installation type $($both[0])"
        }
    }

    $p = Join-Path $msbuildDir "msbuild.exe"
J
Jared Parsons 已提交
220
    $dotnetExe = Ensure-DotnetSdk
J
Jared Parsons 已提交
221 222 223
    return $p
}

J
Jared Parsons 已提交
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
# Returns the msbuild exe path and directory as a single return. This makes it easy 
# to do one line MSBuild configuration in scripts
#   $msbuild, $msbuildDir = Ensure-MSBuildAndDir
function Ensure-MSBuildAndDir([string]$msbuildDir) {
    if ($msbuildDir -eq "") {
        $msbuild = Ensure-MSBuild
        $msbuildDir = Split-Path -parent $msbuild
    }
    else {
        $msbuild = Join-Path $msbuildDir "msbuild.exe"
    }

    return $msbuild, $msbuildDir
}

J
Jared Parsons 已提交
239
function Create-Directory([string]$dir) {
240
    New-Item $dir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
J
Jared Parsons 已提交
241 242
}

J
Jared Parsons 已提交
243
function Get-VersionCore([string]$name, [string]$versionFile) {
J
Jared Parsons 已提交
244
    $name = $name.Replace(".", "")
J
Jared Parsons 已提交
245
    $name = $name.Replace("-", "")
J
Jared Parsons 已提交
246
    $nodeName = "$($name)Version"
J
Jared Parsons 已提交
247
    $x = [xml](Get-Content -raw $versionFile)
248 249 250
    $node = $x.SelectSingleNode("//Project/PropertyGroup/$nodeName")
    if ($node -ne $null) {
        return $node.InnerText
J
Jared Parsons 已提交
251 252
    }

J
Jared Parsons 已提交
253 254 255 256 257 258 259 260 261 262 263 264
    throw "Cannot find package $name in $versionFile"

}

# Return the version of the NuGet package as used in this repo
function Get-PackageVersion([string]$name) {
    return Get-VersionCore $name (Join-Path $repoDir "build\Targets\Packages.props")
}

# Return the version of the specified tool
function Get-ToolVersion([string]$name) {
    return Get-VersionCore $name (Join-Path $repoDir "build\Targets\Tools.props")
J
Jared Parsons 已提交
265 266 267 268
}

# Locate the directory where our NuGet packages will be deployed.  Needs to be kept in sync
# with the logic in Version.props
J
Jared Parsons 已提交
269
function Get-PackagesDir() {
J
Jared Parsons 已提交
270 271 272 273 274 275 276 277 278 279 280
    $d = $null
    if ($env:NUGET_PACKAGES -ne $null) {
        $d = $env:NUGET_PACKAGES
    }
    else {
        $d = Join-Path $env:UserProfile ".nuget\packages\"
    }

    Create-Directory $d
    return $d
}
281

J
Jared Parsons 已提交
282 283 284 285 286 287 288 289
# Locate the directory of a specific NuGet package which is restored via our main 
# toolset values.
function Get-PackageDir([string]$name, [string]$version = "") {
    if ($version -eq "") {
        $version = Get-PackageVersion $name
    }

    $p = Get-PackagesDir
290
    $p = Join-Path $p $name.ToLowerInvariant()
J
Jared Parsons 已提交
291 292 293 294
    $p = Join-Path $p $version
    return $p
}

295 296
# The intent of this script is to locate and return the path to the MSBuild directory that
# we should use for bulid operations.  The preference order for MSBuild to use is as 
J
Jared Parsons 已提交
297
# follows:
298 299
#
#   1. MSBuild from an active VS command prompt
J
Jared Parsons 已提交
300
#   2. MSBuild from a compatible VS installation
J
Jared Parsons 已提交
301
#   3. MSBuild from the xcopy toolset 
302 303
#
# This function will return two values: the kind of MSBuild chosen and the MSBuild directory.
J
Jared Parsons 已提交
304 305 306 307 308 309 310
function Get-MSBuildKindAndDir([switch]$xcopy = $false) {

    if ($xcopy) { 
        Write-Output "xcopy"
        Write-Output (Get-MSBuildDirXCopy)
        return
    }
311

312 313 314 315
    # MSBuild from an active VS command prompt. Use the MSBuild here so long as it's from a 
    # compatible Visual Studio. If not though throw and error out. Given the number of 
    # environment variable changes in a developer command prompt it's hard to make guarantees
    # about subbing in a new MSBuild instance
316
    if (${env:VSINSTALLDIR} -ne $null) {
J
Fixup  
Jared Parsons 已提交
317
        $command = (Get-Command msbuild -ErrorAction SilentlyContinue)
318
        if ((Test-SupportedVisualStudioVersion ${env:VSCMD_VER}) -and ($command -ne $null) ) {
J
Fixup  
Jared Parsons 已提交
319 320 321 322 323
            $p = Split-Path -parent $command.Path
            Write-Output "vscmd"
            Write-Output $p
            return
        }
324
        else {
J
Jared Parsons 已提交
325 326
            $vsMinimumVersion = Get-ToolVersion "vsMinimum"
            throw "Developer Command Prompt for VS $(${env:VSCMD_VER}) is not recent enough. Please upgrade to {$vsMinimumVersion} or build from a normal CMD window"
327
        }
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
    }

    # Look for a valid VS installation
    try {
        $p = Get-VisualStudioDir
        $p = Join-Path $p "MSBuild\15.0\Bin"
        Write-Output "vsinstall"
        Write-Output $p
        return
    }
    catch { 
        # Failures are expected here when no VS installation is present on the 
        # machine.
    }

J
Jared Parsons 已提交
343 344 345 346 347 348 349
    Write-Output "xcopy"
    Write-Output (Get-MSBuildDirXCopy)
    return
}

# Locate the xcopy version of MSBuild
function Get-MSBuildDirXCopy() {
J
Jared Parsons 已提交
350
    $p = Ensure-BasicTool "RoslynTools.MSBuild"
J
Jared Parsons 已提交
351 352
    $p = Join-Path $p "tools\msbuild"
    return $p
353 354
}

J
Jared Parsons 已提交
355 356
function Get-MSBuildDir([switch]$xcopy = $false) {
    $both = Get-MSBuildKindAndDir -xcopy:$xcopy
357 358 359
    return $both[1]
}

360 361 362

# Dose this version of Visual Studio meet our minimum requirements for building.
function Test-SupportedVisualStudioVersion([string]$version) { 
J
Jared Parsons 已提交
363 364 365
    # This regex allows us to strip off any pre-release info that gets attached 
    # to the version string. VS uses NuGet style pre-release by suffing version
    # with -<pre-release info>
366 367 368 369
    if (-not ($version -match "^([\d.]+)(\+|-)?.*$")) { 
        return $false
    }

J
Jared Parsons 已提交
370
    $vsMinimumVersion = Get-ToolVersion "vsMinimum"
371
    $V = New-Object System.Version $matches[1]
J
Jared Parsons 已提交
372
    $min = New-Object System.Version $vsMinimumVersion
373 374 375
    return $v -ge $min;
}

376 377 378
# Get the directory and instance ID of the first Visual Studio version which 
# meets our minimal requirements for the Roslyn repo.
function Get-VisualStudioDirAndId() {
J
Jared Parsons 已提交
379
    $vswhere = Join-Path (Ensure-BasicTool "vswhere") "tools\vswhere.exe"
J
Jared Parsons 已提交
380 381 382 383 384 385 386 387
    $output = Exec-Command $vswhere "-requires Microsoft.Component.MSBuild -format json" | Out-String
    $j = ConvertFrom-Json $output
    foreach ($obj in $j) { 

        # Need to be using at least Visual Studio 15.2 in order to have the appropriate
        # set of SDK fixes. Parsing the installationName is the only place where this is 
        # recorded in that form.
        $name = $obj.installationName
388 389
        if ($name -match "VisualStudio(Preview)?/(.*)") { 
            if (Test-SupportedVisualStudioVersion $matches[2]) {
J
Jared Parsons 已提交
390 391 392 393 394 395 396 397
                Write-Output $obj.installationPath
                Write-Output $obj.instanceId
                return
            }
        }
        else {
            Write-Host "Unrecognized installationName format $name"
        }
398 399
    }

J
Jared Parsons 已提交
400
    throw "Could not find a suitable Visual Studio Version"
401 402 403 404 405 406 407
}

# Get the directory of the first Visual Studio which meets our minimal 
# requirements for the Roslyn repo
function Get-VisualStudioDir() {
    $both = Get-VisualStudioDirAndId
    return $both[0]
408 409
}

J
Jared Parsons 已提交
410 411
# Clear out the NuGet package cache
function Clear-PackageCache() {
412
    $dotnet = Ensure-DotnetSdk
J
Jared Parsons 已提交
413
    Exec-Console $dotnet "nuget locals all --clear"
J
Jared Parsons 已提交
414 415 416
}

# Restore a single project
417
function Restore-Project([string]$dotnetExe, [string]$projectFileName) {
J
Jared Parsons 已提交
418
    $nugetConfig = Join-Path $repoDir "nuget.config"
J
Jared Parsons 已提交
419

420 421 422
    $projectFilePath = $projectFileName
    if (-not (Test-Path $projectFilePath)) {
        $projectFilePath = Join-Path $repoDir $projectFileName
J
Jared Parsons 已提交
423 424
    }

425
    Exec-Console $dotnet "restore --verbosity quiet --configfile $nugetConfig $projectFilePath"
J
Jared Parsons 已提交
426 427 428
}

# Restore all of the projects that the repo consumes
429 430 431
function Restore-Packages([string]$dotnetExe = "", [string]$project = "") {
    if ($dotnetExe -eq "") { 
        $dotnetExe = Ensure-DotnetSdk
J
Jared Parsons 已提交
432 433
    }

434
    Write-Host "Restore using dotnet at $dotnetExe"
J
Jared Parsons 已提交
435 436 437

    if ($project -ne "") {
        Write-Host "Restoring project $project"
438
        Restore-Project $dotnetExe $project
J
Jared Parsons 已提交
439 440 441
    }
    else {
        $all = @(
J
Jared Parsons 已提交
442
            "Roslyn Toolset:build\ToolsetPackages\RoslynToolset.csproj",
J
Jared Parsons 已提交
443
            "Roslyn:Roslyn.sln")
J
Jared Parsons 已提交
444 445 446 447

        foreach ($cur in $all) {
            $both = $cur.Split(':')
            Write-Host "Restoring $($both[0])"
448
            Restore-Project $dotnetExe $both[1]
J
Jared Parsons 已提交
449 450 451 452 453
        }
    }
}

# Restore all of the projects that the repo consumes
454 455
function Restore-All([string]$dotnetExe = "") {
    Restore-Packages -dotnetExe $dotnetExe
J
Jared Parsons 已提交
456
}
J
Jared Parsons 已提交
457