1 [CmdletBinding(PositionalBinding=$false)]
2 Param(
3   [switch] $build,
4   [switch] $ci,
5   [string] $configuration = "Debug",
6   [switch] $help,
7   [switch] $nolog,
8   [switch] $pack,
9   [switch] $prepareMachine,
10   [switch] $rebuild,
11   [switch] $norestore,
12   [switch] $sign,
13   [switch] $skiptests,
14   [switch] $bootstrapOnly,
15   [string] $verbosity = "minimal",
16   [string] $hostType,
17   [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties
18 )
19 
20 Set-StrictMode -Version 2.0
21 $ErrorActionPreference = "Stop"
22 
Print-Usage()23 function Print-Usage() {
24     Write-Host "Common settings:"
25     Write-Host "  -configuration <value>  Build configuration Debug, Release"
26     Write-Host "  -verbosity <value>      Msbuild verbosity (q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic])"
27     Write-Host "  -help                   Print help and exit"
28     Write-Host ""
29     Write-Host "Actions:"
30     Write-Host "  -norestore              Don't automatically run restore"
31     Write-Host "  -build                  Build solution"
32     Write-Host "  -rebuild                Rebuild solution"
33     Write-Host "  -skipTests              Don't run tests"
34     Write-Host "  -bootstrapOnly          Don't run build again with bootstrapped MSBuild"
35     Write-Host "  -sign                   Sign build outputs"
36     Write-Host "  -pack                   Package build outputs into NuGet packages and Willow components"
37     Write-Host ""
38     Write-Host "Advanced settings:"
39     Write-Host "  -ci                     Set when running on CI server"
40     Write-Host "  -nolog                  Disable logging"
41     Write-Host "  -prepareMachine         Prepare machine for CI run"
42     Write-Host "  -hostType                   Host / MSBuild flavor to use.  Possible values: full, core"
43     Write-Host ""
44     Write-Host "Command line arguments not listed above are passed through to MSBuild."
45     Write-Host "The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.)."
46 }
47 
Create-Directory([string[]] $Path)48 function Create-Directory([string[]] $Path) {
49   if (!(Test-Path -Path $Path)) {
50     New-Item -Path $Path -Force -ItemType "Directory" | Out-Null
51   }
52 }
53 
GetVersionsPropsVersion([string[]] $Name)54 function GetVersionsPropsVersion([string[]] $Name) {
55   [xml]$Xml = Get-Content $VersionsProps
56 
57   foreach ($PropertyGroup in $Xml.Project.PropertyGroup) {
58     if (Get-Member -InputObject $PropertyGroup -name $Name) {
59         return $PropertyGroup.$Name
60     }
61   }
62 
63   throw "Failed to locate the $Name property"
64 }
65 
InstallDotNetCli()66 function InstallDotNetCli {
67   $DotNetCliVersion = GetVersionsPropsVersion -Name "DotNetCliVersion"
68   $DotNetInstallVerbosity = ""
69 
70   if (!$env:DOTNET_INSTALL_DIR) {
71     $env:DOTNET_INSTALL_DIR = Join-Path $RepoRoot "artifacts\.dotnet\$DotNetCliVersion\"
72   }
73 
74   $DotNetRoot = $env:DOTNET_INSTALL_DIR
75   $DotNetInstallScript = Join-Path $DotNetRoot "dotnet-install.ps1"
76 
77   if (!(Test-Path $DotNetInstallScript)) {
78     Create-Directory $DotNetRoot
79     Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -UseBasicParsing -OutFile $DotNetInstallScript
80   }
81 
82   if ($verbosity -eq "diagnostic") {
83     $DotNetInstallVerbosity = "-Verbose"
84   }
85 
86   # Install a stage 0
87   $SdkInstallDir = Join-Path $DotNetRoot "sdk\$DotNetCliVersion"
88 
89   if (!(Test-Path $SdkInstallDir)) {
90     # Use Invoke-Expression so that $DotNetInstallVerbosity is not positionally bound when empty
91     Invoke-Expression -Command "& '$DotNetInstallScript' -Version $DotNetCliVersion $DotNetInstallVerbosity"
92 
93     if($LASTEXITCODE -ne 0) {
94       throw "Failed to install stage0"
95     }
96   }
97 
98   # Put the stage 0 on the path
99   $env:PATH = "$DotNetRoot;$env:PATH"
100 
101   # Disable first run since we want to control all package sources
102   $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
103 
104   # Don't resolve runtime, shared framework, or SDK from other locations
105   $env:DOTNET_MULTILEVEL_LOOKUP=0
106 }
107 
InstallNuGet()108 function InstallNuGet {
109   $NugetInstallDir = Join-Path $RepoRoot "artifacts\.nuget"
110   $NugetExe = Join-Path $NugetInstallDir "nuget.exe"
111 
112   if (!(Test-Path -Path $NugetExe)) {
113     Create-Directory $NugetInstallDir
114     Invoke-WebRequest "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -UseBasicParsing -OutFile $NugetExe
115   }
116 }
117 
InstallRepoToolset()118 function InstallRepoToolset {
119   $RepoToolsetVersion = GetVersionsPropsVersion -Name "RoslynToolsRepoToolsetVersion"
120   $RepoToolsetDir = Join-Path $NuGetPackageRoot "roslyntools.repotoolset\$RepoToolsetVersion\tools"
121   $RepoToolsetBuildProj = Join-Path $RepoToolsetDir "Build.proj"
122 
123   if (!(Test-Path -Path $RepoToolsetBuildProj)) {
124     $ToolsetProj = Join-Path $PSScriptRoot "Toolset.proj"
125     $msbuildArgs = "/t:restore", "/m", "/clp:Summary", "/warnaserror", "/v:$verbosity"
126     $msbuildArgs = AddLogCmd "Toolset" $msbuildArgs
127     # Piping to Out-Null is important here, as otherwise the MSBuild output will be included in the return value
128     # of the function (Powershell handles return values a bit... weirdly)
129     CallMSBuild $ToolsetProj @msbuildArgs | Out-Null
130 
131     if($LASTEXITCODE -ne 0) {
132       throw "Failed to build $ToolsetProj"
133     }
134   }
135 
136   return $RepoToolsetBuildProj
137 }
138 
LocateVisualStudio()139 function LocateVisualStudio {
140   $VSWhereVersion = GetVersionsPropsVersion -Name "VSWhereVersion"
141   $VSWhereDir = Join-Path $ArtifactsDir ".tools\vswhere\$VSWhereVersion"
142   $VSWhereExe = Join-Path $vsWhereDir "vswhere.exe"
143 
144   if (!(Test-Path $VSWhereExe)) {
145     Create-Directory $VSWhereDir
146     Invoke-WebRequest "http://github.com/Microsoft/vswhere/releases/download/$VSWhereVersion/vswhere.exe" -UseBasicParsing -OutFile $VSWhereExe
147   }
148 
149   $VSInstallDir = & $VSWhereExe -latest -property installationPath -requires Microsoft.Component.MSBuild -requires Microsoft.VisualStudio.Component.VSSDK -requires Microsoft.Net.Component.4.6.TargetingPack -requires Microsoft.VisualStudio.Component.Roslyn.Compiler -requires Microsoft.VisualStudio.Component.VSSDK
150 
151   if (!(Test-Path $VSInstallDir)) {
152     throw "Failed to locate Visual Studio (exit code '$LASTEXITCODE')."
153   }
154 
155   return $VSInstallDir
156 }
157 
KillProcessesFromRepo()158 function KillProcessesFromRepo {
159   # Jenkins does not allow taskkill
160   if (-not $ci) {
161     # Kill compiler server and MSBuild node processes from bootstrapped MSBuild (otherwise a second build will fail to copy files in use)
162     foreach ($process in Get-Process | Where-Object {'msbuild', 'dotnet', 'vbcscompiler' -contains $_.Name})
163     {
164 
165       if ([string]::IsNullOrEmpty($process.Path))
166       {
167         Write-Host "Process $($process.Id) $($process.Name) does not have a Path. Skipping killing it."
168         continue
169       }
170 
171       if ($process.Path.StartsWith($RepoRoot, [StringComparison]::InvariantCultureIgnoreCase))
172       {
173         Write-Host "Killing $($process.Name) from $($process.Path)"
174         taskkill /f /pid $process.Id
175       }
176     }
177   }
178 }
179 
Build()180 function Build {
181   InstallDotNetCli
182   $env:DOTNET_HOST_PATH = Join-Path $env:DOTNET_INSTALL_DIR "dotnet.exe"
183 
184   if ($prepareMachine) {
185     Create-Directory $NuGetPackageRoot
186     & "$env:DOTNET_HOST_PATH" nuget locals all --clear
187 
188     if($LASTEXITCODE -ne 0) {
189       throw "Failed to clear NuGet cache"
190     }
191   }
192 
193   InstallNuget
194 
195   if ($hostType -eq 'full')
196   {
197     $msbuildHost = $null
198   }
199   elseif ($hostType -eq 'core')
200   {
201     $msbuildHost = $env:DOTNET_HOST_PATH
202   }
203   else
204   {
205     throw "Unknown hostType parameter: $hostType"
206   }
207 
208   $RepoToolsetBuildProj = InstallRepoToolset
209 
210   $solution = Join-Path $RepoRoot "MSBuild.sln"
211 
212   $commonMSBuildArgs = "/m", "/clp:Summary", "/v:$verbosity", "/p:Configuration=$configuration", "/p:SolutionPath=$solution", "/p:CIBuild=$ci"
213   if ($ci)
214   {
215     # Only enable warnaserror on CI runs.  For local builds, we will generate a warning if we can't run EditBin because
216     # the C++ tools aren't installed, and we don't want this to fail the build
217     $commonMSBuildArgs = $commonMSBuildArgs + "/warnaserror"
218   }
219 
220   if ($hostType -ne 'full')
221   {
222     # Make signing a no-op if building not using full Framework MSBuild (as all of the assets that are signed won't be produced)
223     $emptySignToolDataPath = [io.path]::combine($RepoRoot, 'build', 'EmptySignToolData.json')
224     $commonMSBuildArgs = $commonMSBuildArgs + "/p:SignToolDataPath=`"$emptySignToolDataPath`""
225   }
226 
227   # Only test using stage 0 MSBuild if -bootstrapOnly is specified
228   $testStage0 = $false
229   if ($bootstrapOnly)
230   {
231     $testStage0 = $test
232   }
233 
234   $msbuildArgs = AddLogCmd "Build" $commonMSBuildArgs
235 
236   CallMSBuild $RepoToolsetBuildProj @msbuildArgs /p:Restore=$restore /p:Build=$build /p:Rebuild=$rebuild /p:Test=$testStage0 /p:Sign=$sign /p:Pack=$pack /p:CreateBootstrap=true @properties
237 
238   if (-not $bootstrapOnly)
239   {
240     $bootstrapRoot = Join-Path $ArtifactsConfigurationDir "bootstrap"
241 
242     if ($hostType -eq 'full')
243     {
244       $msbuildToUse = Join-Path $bootstrapRoot "net46\MSBuild\15.0\Bin\MSBuild.exe"
245 
246       if ($configuration -eq "Debug-MONO" -or $configuration -eq "Release-MONO")
247       {
248         # Copy MSBuild.dll to MSBuild.exe so we can run it without a host
249         $sourceDll = Join-Path $bootstrapRoot "net46\MSBuild\15.0\Bin\MSBuild.dll"
250         Copy-Item -Path $sourceDll -Destination $msbuildToUse
251       }
252     }
253     else
254     {
255       $msbuildToUse = Join-Path $bootstrapRoot "netcoreapp2.1\MSBuild\\MSBuild.dll"
256     }
257 
258     # Use separate artifacts folder for stage 2
259     $env:ArtifactsDir = Join-Path $ArtifactsDir "2\"
260 
261     $msbuildArgs = AddLogCmd "BuildWithBootstrap" $commonMSBuildArgs
262 
263     # When using bootstrapped MSBuild:
264     # - Turn off node reuse (so that bootstrapped MSBuild processes don't stay running and lock files)
265     # - Don't sign
266     # - Don't pack
267     # - Do run tests (if not skipped)
268     # - Don't try to create a bootstrap deployment
269     CallMSBuild $RepoToolsetBuildProj @msbuildArgs /nr:false /p:Restore=$restore /p:Build=$build /p:Rebuild=$rebuild /p:Test=$test /p:Sign=false /p:Pack=false /p:CreateBootstrap=false @properties
270   }
271 
272   if ($ci)
273   {
274 #    CallMSBuild $ToolsetProj /t:restore /m /clp:Summary /warnaserror /v:$verbosity @logCmd | Out-Null
275     git status | Out-Null
276     git --no-pager diff HEAD --word-diff=plain --exit-code | Out-Null
277 
278     if($LASTEXITCODE -ne 0) {
279       throw "[ERROR] After building, there are changed files.  Please build locally and include these changes in your pull request."
280     }
281   }
282 
283 }
CallMSBuild()284 function CallMSBuild
285 {
286   try
287   {
288     if ($msbuildHost)
289     {
290       & $msbuildHost $msbuildToUse $args
291     }
292     else
293     {
294       & $msbuildToUse $args
295     }
296 
297     if($LASTEXITCODE -ne 0) {
298         throw "Failed to build $args"
299     }
300   }
301   finally
302   {
303     KillProcessesFromRepo
304   }
305 }
306 
AddLogCmd([string] $logName, [string[]] $extraArgs)307 function AddLogCmd([string] $logName, [string[]] $extraArgs)
308 {
309 
310   if ($ci -or $log) {
311     Create-Directory $LogDir
312     $extraArgs = $extraArgs + ("/bl:" + (Join-Path $LogDir "$logName.binlog"))
313 
314     # When running under CI, also create a text log, so it can be viewed in the Jenkins UI
315     if ($ci) {
316       $extraArgs = $extraArgs + ("/flp:Verbosity=diag;LogFile=" + '"' + (Join-Path $LogDir "$logName.log") + '"')
317     }
318   }
319 
320   return $extraArgs;
321 }
322 
Stop-Processes()323 function Stop-Processes() {
324   Write-Host "Killing running build processes..."
325   Get-Process -Name "msbuild" -ErrorAction SilentlyContinue | Stop-Process
326   Get-Process -Name "vbcscompiler" -ErrorAction SilentlyContinue | Stop-Process
327 }
328 
329 if ($help -or (($properties -ne $null) -and ($properties.Contains("/help") -or $properties.Contains("/?")))) {
330   Print-Usage
331   exit 0
332 }
333 
334 $RepoRoot = Join-Path $PSScriptRoot "..\"
335 $RepoRoot = [System.IO.Path]::GetFullPath($RepoRoot);
336 $ArtifactsDir = Join-Path $RepoRoot "artifacts"
337 $ArtifactsConfigurationDir = Join-Path $ArtifactsDir $configuration
338 $LogDir = Join-Path $ArtifactsConfigurationDir "log"
339 $VersionsProps = Join-Path $PSScriptRoot "Versions.props"
340 
341 $log = -not $nolog
342 $restore = -not $norestore
343 $test = -not $skiptests
344 
345 if ($hostType -eq '')
346 {
347   $hostType = 'full'
348 }
349 
350 # TODO: If host type is full, either make sure we're running in a developer command prompt, or attempt to locate VS, or fail
351 
352 $msbuildHost = $null
353 $msbuildToUse = "msbuild"
354 
355 try {
356 
357   KillProcessesFromRepo
358 
359   if ($ci) {
360     $TempDir = Join-Path $ArtifactsConfigurationDir "tmp"
361     Create-Directory $TempDir
362 
363     $env:TEMP = $TempDir
364     $env:TMP = $TempDir
365     $env:MSBUILDDEBUGPATH = $TempDir
366   }
367 
368   if (!($env:NUGET_PACKAGES)) {
369     $env:NUGET_PACKAGES = Join-Path $env:UserProfile ".nuget\packages"
370   }
371 
372   $NuGetPackageRoot = $env:NUGET_PACKAGES
373 
374   Build
375   exit $lastExitCode
376 }
377 catch {
378   Write-Host $_
379   Write-Host $_.Exception
380   Write-Host $_.ScriptStackTrace
381   exit 1
382 }
383 finally {
384   Pop-Location
385   if ($ci -and $prepareMachine) {
386     Stop-Processes
387   }
388 }
389