aboutsummaryrefslogtreecommitdiff
path: root/scripts/azure-pipelines/windows
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/azure-pipelines/windows')
-rw-r--r--scripts/azure-pipelines/windows/azure-pipelines.yml66
-rw-r--r--scripts/azure-pipelines/windows/ci-step.ps1163
-rw-r--r--scripts/azure-pipelines/windows/create-vmss.ps1458
-rw-r--r--scripts/azure-pipelines/windows/initialize-environment.ps193
-rw-r--r--scripts/azure-pipelines/windows/provision-image.ps1447
-rw-r--r--scripts/azure-pipelines/windows/sysprep.ps117
6 files changed, 1244 insertions, 0 deletions
diff --git a/scripts/azure-pipelines/windows/azure-pipelines.yml b/scripts/azure-pipelines/windows/azure-pipelines.yml
new file mode 100644
index 000000000..1913f0c9b
--- /dev/null
+++ b/scripts/azure-pipelines/windows/azure-pipelines.yml
@@ -0,0 +1,66 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+
+jobs:
+- job: ${{ parameters.jobName }}
+ pool:
+ name: PrWin-2020-04-21-1
+
+ variables:
+ triplet: '${{ parameters.triplet }}'
+
+ timeoutInMinutes: 1440 # 1 day
+
+ steps:
+ - task: PowerShell@2
+ displayName: 'Initialize Environment'
+ inputs:
+ filePath: 'scripts/azure-pipelines/windows/initialize-environment.ps1'
+
+ - powershell: |
+ $baselineFile = "$(System.DefaultWorkingDirectory)\scripts\ci.baseline.txt"
+ $skipList = $(System.DefaultWorkingDirectory)\scripts\azure-pipelines\generate-skip-list.ps1 -Triplet "$(triplet)" -BaselineFile $baselineFile
+ Write-Host "baseline file: $baselineFile"
+ Write-Host "skip list: $skipList"
+ $(System.DefaultWorkingDirectory)\scripts\azure-pipelines\windows\ci-step.ps1 -Triplet "$(triplet)" -ExcludePorts $skipList
+ Write-Host "CI test script is complete"
+ errorActionPreference: continue
+ displayName: '** Build vcpkg and test ports **'
+
+ - powershell: |
+ $baseName = "$(triplet)"
+ $outputPathRoot = "$(System.ArtifactsDirectory)\raw xml results"
+ if(-not (Test-Path $outputPathRoot))
+ {
+ Write-Host "creating $outputPathRoot"
+ mkdir $outputPathRoot | Out-Null
+ }
+
+ $xmlPath = "$(System.DefaultWorkingDirectory)\test-full-ci.xml"
+ $outputXmlPath = "$outputPathRoot\$baseName.xml"
+
+ cp $xmlPath $(Build.ArtifactStagingDirectory)
+ Move-Item $xmlPath -Destination $outputXmlPath
+
+ # already in DevOps, no need for extra copies
+ rm $(System.DefaultWorkingDirectory)\console-out.txt -ErrorAction Ignore
+
+ Remove-Item "$(System.DefaultWorkingDirectory)\buildtrees\*" -Recurse -errorAction silentlycontinue
+ Remove-Item "$(System.DefaultWorkingDirectory)\packages\*" -Recurse -errorAction silentlycontinue
+ Remove-Item "$(System.DefaultWorkingDirectory)\installed\*" -Recurse -errorAction silentlycontinue
+ displayName: 'Collect logs and cleanup build'
+
+ - task: PowerShell@2
+ displayName: 'Analyze results and prepare test logs'
+ inputs:
+ failOnStderr: true
+ filePath: 'scripts/azure-pipelines/analyze-test-results.ps1'
+ arguments: '-baselineFile ''$(System.DefaultWorkingDirectory)\scripts\ci.baseline.txt'' -logDir ''$(System.ArtifactsDirectory)\raw xml results'' -failurelogDir ''archives\fail'' -outputDir ''$(Build.ArtifactStagingDirectory)'' -errorOnRegression -triplets ''$(triplet)'''
+
+ - task: PublishBuildArtifacts@1
+ displayName: 'Publish Artifact: $(triplet) port build failure logs'
+ inputs:
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)\failureLogs'
+ ArtifactName: '$(triplet) port build failure logs'
+ condition: failed()
diff --git a/scripts/azure-pipelines/windows/ci-step.ps1 b/scripts/azure-pipelines/windows/ci-step.ps1
new file mode 100644
index 000000000..0e07895e0
--- /dev/null
+++ b/scripts/azure-pipelines/windows/ci-step.ps1
@@ -0,0 +1,163 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+
+<#
+.SYNOPSIS
+Runs the bootstrap and port install parts of the vcpkg CI for Windows
+
+.DESCRIPTION
+There are multiple steps to the vcpkg CI; this is the most important one.
+First, it runs `boostrap-vcpkg.bat` in order to build the tool itself; it
+then installs either all of the ports specified, or all of the ports excluding
+those which are passed in $ExcludePorts. Then, it runs `vcpkg ci` to access the
+data, and prints all of the failures and successes to the Azure console.
+
+.PARAMETER Triplet
+The triplet to run the installs for -- one of the triplets known by vcpkg, like
+`x86-windows` and `x64-windows`.
+
+.PARAMETER OnlyIncludePorts
+The set of ports to install.
+
+.PARAMETER ExcludePorts
+If $OnlyIncludePorts is not passed, this set of ports is used to exclude ports to
+install from the set of all ports.
+
+.PARAMETER AdditionalVcpkgFlags
+Flags to pass to vcpkg in addition to the ports to install, and the triplet.
+#>
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)][string]$Triplet,
+ [string]$OnlyIncludePorts = '',
+ [string]$ExcludePorts = '',
+ [string]$AdditionalVcpkgFlags = ''
+)
+
+Set-StrictMode -Version Latest
+
+$scriptsDir = Split-Path -parent $script:MyInvocation.MyCommand.Definition
+
+<#
+.SYNOPSIS
+Gets the first parent directory D of $startingDir such that D/$filename is a file.
+
+.DESCRIPTION
+Get-FileRecursivelyUp Looks for a directory containing $filename, starting in
+$startingDir, and then checking each parent directory of $startingDir in turn.
+It returns the first directory it finds.
+If the file is not found, the empty string is returned - this is likely to be
+a bug.
+
+.PARAMETER startingDir
+The directory to start looking for $filename in.
+
+.PARAMETER filename
+The filename to look for.
+#>
+function Get-FileRecursivelyUp() {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)][string]$startingDir,
+ [Parameter(Mandatory = $true)][string]$filename
+ )
+
+ $currentDir = $startingDir
+
+ while ($currentDir.Length -gt 0 -and -not (Test-Path "$currentDir\$filename")) {
+ Write-Verbose "Examining $currentDir for $filename"
+ $currentDir = Split-Path $currentDir -Parent
+ }
+
+ if ($currentDir.Length -eq 0) {
+ Write-Warning "None of $startingDir's parent directories contain $filename. This is likely a bug."
+ }
+
+ Write-Verbose "Examining $currentDir for $filename - Found"
+ return $currentDir
+}
+
+<#
+.SYNOPSIS
+Removes a file or directory, with backoff in the directory case.
+
+.DESCRIPTION
+Remove-Item -Recurse occasionally fails spuriously; in order to get around this,
+we remove with backoff. At a maximum, we will wait 180s before giving up.
+
+.PARAMETER Path
+The path to remove.
+#>
+function Remove-VcpkgItem {
+ [CmdletBinding()]
+ param([Parameter(Mandatory = $true)][string]$Path)
+
+ if ([string]::IsNullOrEmpty($Path)) {
+ return
+ }
+
+ if (Test-Path $Path) {
+ # Remove-Item -Recurse occasionally fails. This is a workaround
+ if ((Get-Item $Path) -is [System.IO.DirectoryInfo]) {
+ Remove-Item $Path -Force -Recurse -ErrorAction SilentlyContinue
+ for ($i = 0; $i -le 60 -and (Test-Path $Path); $i++) { # ~180s max wait time
+ Start-Sleep -m (100 * $i)
+ Remove-Item $Path -Force -Recurse -ErrorAction SilentlyContinue
+ }
+
+ if (Test-Path $Path) {
+ Write-Error "$Path was unable to be fully deleted."
+ throw;
+ }
+ }
+ else {
+ Remove-Item $Path -Force
+ }
+ }
+}
+
+$vcpkgRootDir = Get-FileRecursivelyUp $scriptsDir .vcpkg-root
+
+Write-Host "Bootstrapping vcpkg ..."
+& "$vcpkgRootDir\bootstrap-vcpkg.bat" -Verbose
+if (!$?) { throw "bootstrap failed" }
+Write-Host "Bootstrapping vcpkg ... done."
+
+$ciXmlPath = "$vcpkgRootDir\test-full-ci.xml"
+$consoleOuputPath = "$vcpkgRootDir\console-out.txt"
+Remove-VcpkgItem $ciXmlPath
+
+$env:VCPKG_FEATURE_FLAGS = "binarycaching"
+
+if (![string]::IsNullOrEmpty($OnlyIncludePorts)) {
+ ./vcpkg install --triplet $Triplet $OnlyIncludePorts $AdditionalVcpkgFlags `
+ "--x-xunit=$ciXmlPath" | Tee-Object -FilePath "$consoleOuputPath"
+}
+else {
+ $exclusions = ""
+ if (![string]::IsNullOrEmpty($ExcludePorts)) {
+ $exclusions = "--exclude=$ExcludePorts"
+ }
+
+ if ( $Triplet -notmatch "x86-windows" -and $Triplet -notmatch "x64-windows" ) {
+ # WORKAROUND: the x86-windows flavors of these are needed for all
+ # cross-compilation, but they are not auto-installed.
+ # Install them so the CI succeeds
+ ./vcpkg install "protobuf:x86-windows" "boost-build:x86-windows" "sqlite3:x86-windows"
+ if (-not $?) { throw "Failed to install protobuf & boost-build & sqlite3" }
+ }
+
+ # Turn all error messages into strings for output in the CI system.
+ # This is needed due to the way the public Azure DevOps turns error output to pipeline errors,
+ # even when told to ignore error output.
+ ./vcpkg ci $Triplet $AdditionalVcpkgFlags "--x-xunit=$ciXmlPath" $exclusions 2>&1 `
+ | ForEach-Object {
+ if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
+ }
+
+ # Phasing out the console output (it is already saved in DevOps) Create a dummy file for now.
+ Set-Content -LiteralPath "$consoleOuputPath" -Value ''
+}
+
+Write-Host "CI test is complete"
diff --git a/scripts/azure-pipelines/windows/create-vmss.ps1 b/scripts/azure-pipelines/windows/create-vmss.ps1
new file mode 100644
index 000000000..099c7dbfb
--- /dev/null
+++ b/scripts/azure-pipelines/windows/create-vmss.ps1
@@ -0,0 +1,458 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+#
+
+<#
+.SYNOPSIS
+Creates a Windows virtual machine scale set, set up for vcpkg's CI.
+
+.DESCRIPTION
+create-vmss.ps1 creates an Azure Windows VM scale set, set up for vcpkg's CI
+system. See https://docs.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview
+for more information.
+
+This script assumes you have installed Azure tools into PowerShell by following the instructions
+at https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-3.6.1
+or are running from Azure Cloud Shell.
+#>
+
+$Location = 'SouthCentralUS'
+$Prefix = 'PrWin-' + (Get-Date -Format 'yyyy-MM-dd')
+$VMSize = 'Standard_F16s_v2'
+$ProtoVMName = 'PROTOTYPE'
+$LiveVMPrefix = 'BUILD'
+$WindowsServerSku = '2019-Datacenter'
+$InstalledDiskSizeInGB = 1024
+$ErrorActionPreference = 'Stop'
+
+$ProgressActivity = 'Creating Scale Set'
+$TotalProgress = 12
+$CurrentProgress = 1
+
+<#
+.SYNOPSIS
+Returns whether there's a name collision in the resource group.
+
+.DESCRIPTION
+Find-ResourceGroupNameCollision takes a list of resources, and checks if $Test
+collides names with any of the resources.
+
+.PARAMETER Test
+The name to test.
+
+.PARAMETER Resources
+The list of resources.
+#>
+function Find-ResourceGroupNameCollision {
+ [CmdletBinding()]
+ Param([string]$Test, $Resources)
+
+ foreach ($resource in $Resources) {
+ if ($resource.ResourceGroupName -eq $Test) {
+ return $true
+ }
+ }
+
+ return $false
+}
+
+<#
+.SYNOPSIS
+Attempts to find a name that does not collide with any resources in the resource group.
+
+.DESCRIPTION
+Find-ResourceGroupName takes a set of resources from Get-AzResourceGroup, and finds the
+first name in {$Prefix, $Prefix-1, $Prefix-2, ...} such that the name doesn't collide with
+any of the resources in the resource group.
+
+.PARAMETER Prefix
+The prefix of the final name; the returned name will be of the form "$Prefix(-[1-9][0-9]*)?"
+#>
+function Find-ResourceGroupName {
+ [CmdletBinding()]
+ Param([string] $Prefix)
+
+ $resources = Get-AzResourceGroup
+ $result = $Prefix
+ $suffix = 0
+ while (Find-ResourceGroupNameCollision -Test $result -Resources $resources) {
+ $suffix++
+ $result = "$Prefix-$suffix"
+ }
+
+ return $result
+}
+
+<#
+.SYNOPSIS
+Creates a randomly generated password.
+
+.DESCRIPTION
+New-Password generates a password, randomly, of length $Length, containing
+only alphanumeric characters (both uppercase and lowercase).
+
+.PARAMETER Length
+The length of the returned password.
+#>
+function New-Password {
+ Param ([int] $Length = 32)
+
+ $Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ $result = ''
+ for ($idx = 0; $idx -lt $Length; $idx++) {
+ # NOTE: this should probably use RNGCryptoServiceProvider
+ $result += $Chars[(Get-Random -Minimum 0 -Maximum $Chars.Length)]
+ }
+
+ return $result
+}
+
+<#
+.SYNOPSIS
+Waits for the shutdown of the specified resource.
+
+.DESCRIPTION
+Wait-Shutdown takes a VM, and checks if there's a 'PowerState/stopped'
+code; if there is, it returns. If there isn't, it waits ten seconds and
+tries again.
+
+.PARAMETER ResourceGroupName
+The name of the resource group to look up the VM in.
+
+.PARAMETER Name
+The name of the virtual machine to wait on.
+#>
+function Wait-Shutdown {
+ [CmdletBinding()]
+ Param([string]$ResourceGroupName, [string]$Name)
+
+ Write-Host "Waiting for $Name to stop..."
+ while ($true) {
+ $Vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -Status
+ $highestStatus = $Vm.Statuses.Count
+ for ($idx = 0; $idx -lt $highestStatus; $idx++) {
+ if ($Vm.Statuses[$idx].Code -eq 'PowerState/stopped') {
+ return
+ }
+ }
+
+ Write-Host "... not stopped yet, sleeping for 10 seconds"
+ Start-Sleep -Seconds 10
+ }
+}
+
+<#
+.SYNOPSIS
+Sanitizes a name to be used in a storage account.
+
+.DESCRIPTION
+Sanitize-Name takes a string, and removes all of the '-'s and
+lowercases the string, since storage account names must have no
+'-'s and must be completely lowercase alphanumeric. It then makes
+certain that the length of the string is not greater than 24,
+since that is invalid.
+
+.PARAMETER RawName
+The name to sanitize.
+#>
+function Sanitize-Name {
+ [CmdletBinding()]
+ Param(
+ [string]$RawName
+ )
+
+ $result = $RawName.Replace('-', '').ToLowerInvariant()
+ if ($result.Length -gt 24) {
+ Write-Error 'Sanitized name for storage account $result was too long.'
+ }
+
+ return $result
+}
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Creating resource group' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+$ResourceGroupName = Find-ResourceGroupName $Prefix
+$AdminPW = New-Password
+New-AzResourceGroup -Name $ResourceGroupName -Location $Location
+$AdminPWSecure = ConvertTo-SecureString $AdminPW -AsPlainText -Force
+$Credential = New-Object System.Management.Automation.PSCredential ("AdminUser", $AdminPWSecure)
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Creating virtual network' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+$allowHttp = New-AzNetworkSecurityRuleConfig `
+ -Name AllowHTTP `
+ -Description 'Allow HTTP(S)' `
+ -Access Allow `
+ -Protocol Tcp `
+ -Direction Outbound `
+ -Priority 1008 `
+ -SourceAddressPrefix * `
+ -SourcePortRange * `
+ -DestinationAddressPrefix * `
+ -DestinationPortRange @(80, 443)
+
+$allowDns = New-AzNetworkSecurityRuleConfig `
+ -Name AllowDNS `
+ -Description 'Allow DNS' `
+ -Access Allow `
+ -Protocol * `
+ -Direction Outbound `
+ -Priority 1009 `
+ -SourceAddressPrefix * `
+ -SourcePortRange * `
+ -DestinationAddressPrefix * `
+ -DestinationPortRange 53
+
+$allowStorage = New-AzNetworkSecurityRuleConfig `
+ -Name AllowStorage `
+ -Description 'Allow Storage' `
+ -Access Allow `
+ -Protocol * `
+ -Direction Outbound `
+ -Priority 1010 `
+ -SourceAddressPrefix VirtualNetwork `
+ -SourcePortRange * `
+ -DestinationAddressPrefix Storage `
+ -DestinationPortRange *
+
+$denyEverythingElse = New-AzNetworkSecurityRuleConfig `
+ -Name DenyElse `
+ -Description 'Deny everything else' `
+ -Access Deny `
+ -Protocol * `
+ -Direction Outbound `
+ -Priority 1011 `
+ -SourceAddressPrefix * `
+ -SourcePortRange * `
+ -DestinationAddressPrefix * `
+ -DestinationPortRange *
+
+$NetworkSecurityGroupName = $ResourceGroupName + 'NetworkSecurity'
+$NetworkSecurityGroup = New-AzNetworkSecurityGroup `
+ -Name $NetworkSecurityGroupName `
+ -ResourceGroupName $ResourceGroupName `
+ -Location $Location `
+ -SecurityRules @($allowHttp, $allowDns, $allowStorage, $denyEverythingElse)
+
+$SubnetName = $ResourceGroupName + 'Subnet'
+$Subnet = New-AzVirtualNetworkSubnetConfig `
+ -Name $SubnetName `
+ -AddressPrefix "10.0.0.0/16" `
+ -NetworkSecurityGroup $NetworkSecurityGroup
+
+$VirtualNetworkName = $ResourceGroupName + 'Network'
+$VirtualNetwork = New-AzVirtualNetwork `
+ -Name $VirtualNetworkName `
+ -ResourceGroupName $ResourceGroupName `
+ -Location $Location `
+ -AddressPrefix "10.0.0.0/16" `
+ -Subnet $Subnet
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Creating archives storage account' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+$StorageAccountName = Sanitize-Name $ResourceGroupName
+
+New-AzStorageAccount `
+ -ResourceGroupName $ResourceGroupName `
+ -Location $Location `
+ -Name $StorageAccountName `
+ -SkuName 'Standard_LRS' `
+ -Kind StorageV2
+
+$StorageAccountKeys = Get-AzStorageAccountKey `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $StorageAccountName
+
+$StorageAccountKey = $StorageAccountKeys[0].Value
+
+$StorageContext = New-AzStorageContext `
+ -StorageAccountName $StorageAccountName `
+ -StorageAccountKey $StorageAccountKey
+
+$ArchivesFiles = New-AzStorageShare -Name 'archives' -Context $StorageContext
+Set-AzStorageShareQuota -ShareName 'archives' -Context $StorageContext -Quota 5120
+$LogFiles = New-AzStorageShare -Name 'logs' -Context $StorageContext
+Set-AzStorageShareQuota -ShareName 'logs' -Context $StorageContext -Quota 64
+
+####################################################################################################
+Write-Progress `
+ -Activity 'Creating prototype VM' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+$NicName = $ResourceGroupName + 'NIC'
+$Nic = New-AzNetworkInterface `
+ -Name $NicName `
+ -ResourceGroupName $ResourceGroupName `
+ -Location $Location `
+ -Subnet $VirtualNetwork.Subnets[0]
+
+$VM = New-AzVMConfig -Name $ProtoVMName -VMSize $VMSize
+$VM = Set-AzVMOperatingSystem `
+ -VM $VM `
+ -Windows `
+ -ComputerName $ProtoVMName `
+ -Credential $Credential `
+ -ProvisionVMAgent `
+ -EnableAutoUpdate
+
+$VM = Add-AzVMNetworkInterface -VM $VM -Id $Nic.Id
+$VM = Set-AzVMSourceImage `
+ -VM $VM `
+ -PublisherName 'MicrosoftWindowsServer' `
+ -Offer 'WindowsServer' `
+ -Skus $WindowsServerSku `
+ -Version latest
+
+$InstallDiskName = $ProtoVMName + "InstallDisk"
+$VM = Add-AzVMDataDisk `
+ -Vm $VM `
+ -Name $InstallDiskName `
+ -Lun 0 `
+ -Caching ReadWrite `
+ -CreateOption Empty `
+ -DiskSizeInGB $InstalledDiskSizeInGB `
+ -StorageAccountType 'StandardSSD_LRS'
+
+$VM = Set-AzVMBootDiagnostic -VM $VM -Disable
+New-AzVm `
+ -ResourceGroupName $ResourceGroupName `
+ -Location $Location `
+ -VM $VM
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Running provisioning script provision-image.ps1 in VM' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Invoke-AzVMRunCommand `
+ -ResourceGroupName $ResourceGroupName `
+ -VMName $ProtoVMName `
+ -CommandId 'RunPowerShellScript' `
+ -ScriptPath "$PSScriptRoot\provision-image.ps1" `
+ -Parameter @{AdminUserPassword = $AdminPW; `
+ StorageAccountName=$StorageAccountName; `
+ StorageAccountKey=$StorageAccountKey;}
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Restarting VM' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Restart-AzVM -ResourceGroupName $ResourceGroupName -Name $ProtoVMName
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Running provisioning script sysprep.ps1 in VM' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Invoke-AzVMRunCommand `
+ -ResourceGroupName $ResourceGroupName `
+ -VMName $ProtoVMName `
+ -CommandId 'RunPowerShellScript' `
+ -ScriptPath "$PSScriptRoot\sysprep.ps1"
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Waiting for VM to shut down' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Wait-Shutdown -ResourceGroupName $ResourceGroupName -Name $ProtoVMName
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Converting VM to Image' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Stop-AzVM `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $ProtoVMName `
+ -Force
+
+Set-AzVM `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $ProtoVMName `
+ -Generalized
+
+$VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $ProtoVMName
+$PrototypeOSDiskName = $VM.StorageProfile.OsDisk.Name
+$ImageConfig = New-AzImageConfig -Location $Location -SourceVirtualMachineId $VM.ID
+$Image = New-AzImage -Image $ImageConfig -ImageName $ProtoVMName -ResourceGroupName $ResourceGroupName
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Deleting unused VM and disk' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+Remove-AzVM -Id $VM.ID -Force
+Remove-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $PrototypeOSDiskName -Force
+Remove-AzDisk -ResourceGroupName $ResourceGroupName -DiskName $InstallDiskName -Force
+
+####################################################################################################
+Write-Progress `
+ -Activity $ProgressActivity `
+ -Status 'Creating scale set' `
+ -PercentComplete (100 / $TotalProgress * $CurrentProgress++)
+
+$VmssIpConfigName = $ResourceGroupName + 'VmssIpConfig'
+$VmssIpConfig = New-AzVmssIpConfig -SubnetId $Nic.IpConfigurations[0].Subnet.Id -Primary -Name $VmssIpConfigName
+$VmssName = $ResourceGroupName + 'Vmss'
+$Vmss = New-AzVmssConfig `
+ -Location $Location `
+ -SkuCapacity 6 `
+ -SkuName $VMSize `
+ -SkuTier 'Standard' `
+ -Overprovision $false `
+ -UpgradePolicyMode Manual
+
+$Vmss = Add-AzVmssNetworkInterfaceConfiguration `
+ -VirtualMachineScaleSet $Vmss `
+ -Primary $true `
+ -IpConfiguration $VmssIpConfig `
+ -NetworkSecurityGroupId $NetworkSecurityGroup.Id `
+ -Name $NicName
+
+$Vmss = Set-AzVmssOsProfile `
+ -VirtualMachineScaleSet $Vmss `
+ -ComputerNamePrefix $LiveVMPrefix `
+ -AdminUsername 'AdminUser' `
+ -AdminPassword $AdminPW `
+ -WindowsConfigurationProvisionVMAgent $true `
+ -WindowsConfigurationEnableAutomaticUpdate $true
+
+$Vmss = Set-AzVmssStorageProfile `
+ -VirtualMachineScaleSet $Vmss `
+ -OsDiskCreateOption 'FromImage' `
+ -OsDiskCaching ReadWrite `
+ -ImageReferenceId $Image.Id
+
+New-AzVmss `
+ -ResourceGroupName $ResourceGroupName `
+ -Name $VmssName `
+ -VirtualMachineScaleSet $Vmss
+
+####################################################################################################
+Write-Progress -Activity $ProgressActivity -Completed
+Write-Host "Location: $Location"
+Write-Host "Resource group name: $ResourceGroupName"
+Write-Host "User name: AdminUser"
+Write-Host "Using generated password: $AdminPW"
+Write-Host 'Finished!'
diff --git a/scripts/azure-pipelines/windows/initialize-environment.ps1 b/scripts/azure-pipelines/windows/initialize-environment.ps1
new file mode 100644
index 000000000..b86006a9c
--- /dev/null
+++ b/scripts/azure-pipelines/windows/initialize-environment.ps1
@@ -0,0 +1,93 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+<#
+.SYNOPSIS
+Sets up the environment to run other vcpkg CI steps in an Azure Pipelines job.
+
+.DESCRIPTION
+This script maps network drives from infrastructure and cleans out anything that
+might have been leftover from a previous run.
+
+.PARAMETER ForceAllPortsToRebuildKey
+A subdirectory / key to use to force a build without any previous run caching,
+if necessary.
+#>
+
+[CmdletBinding()]
+Param(
+ [string]$ForceAllPortsToRebuildKey = ''
+)
+
+$StorageAccountName = $env:StorageAccountName
+$StorageAccountKey = $env:StorageAccountKey
+
+function Remove-DirectorySymlink {
+ Param([string]$Path)
+ if (Test-Path $Path) {
+ [System.IO.Directory]::Delete($Path)
+ }
+}
+
+Write-Host 'Setting up archives mount'
+if (-Not (Test-Path W:)) {
+ net use W: "\\$StorageAccountName.file.core.windows.net\archives" /u:"AZURE\$StorageAccountName" $StorageAccountKey
+}
+
+Write-Host 'Setting up logs mount'
+if (-Not (Test-Path L:)) {
+ net use L: "\\$StorageAccountName.file.core.windows.net\logs" /u:"AZURE\$StorageAccountName" $StorageAccountKey
+}
+
+Write-Host 'Creating downloads directory'
+mkdir D:\downloads -ErrorAction SilentlyContinue
+
+# Delete entries in the downloads folder, except:
+# those in the 'tools' folder
+# those last accessed in the last 30 days
+Get-ChildItem -Path D:\downloads -Exclude "tools" `
+ | Where-Object{ $_.LastAccessTime -lt (get-Date).AddDays(-30) } `
+ | ForEach-Object{Remove-Item -Path $_ -Recurse -Force}
+
+# Msys sometimes leaves a database lock file laying around, especially if there was a failed job
+# which causes unrelated failures in jobs that run later on the machine.
+# work around this by just removing the vcpkg installed msys2 if it exists
+if( Test-Path D:\downloads\tools\msys2 )
+{
+ Write-Host "removing previously installed msys2"
+ Remove-Item D:\downloads\tools\msys2 -Recurse -Force
+}
+
+Write-Host 'Setting up archives path...'
+if ([string]::IsNullOrWhiteSpace($ForceAllPortsToRebuildKey))
+{
+ $archivesPath = 'W:\'
+}
+else
+{
+ $archivesPath = "W:\force\$ForceAllPortsToRebuildKey"
+ if (-Not (Test-Path $fullPath)) {
+ Write-Host 'Creating $archivesPath'
+ mkdir $archivesPath
+ }
+}
+
+Write-Host "Linking archives => $archivesPath"
+Remove-DirectorySymlink archives
+cmd /c "mklink /D archives $archivesPath"
+
+Write-Host 'Linking installed => E:\installed'
+Remove-DirectorySymlink installed
+Remove-Item E:\installed -Recurse -Force -ErrorAction SilentlyContinue
+mkdir E:\installed
+cmd /c "mklink /D installed E:\installed"
+
+Write-Host 'Linking downloads => D:\downloads'
+Remove-DirectorySymlink downloads
+cmd /c "mklink /D downloads D:\downloads"
+
+Write-Host 'Cleaning buildtrees'
+Remove-Item buildtrees\* -Recurse -Force -errorAction silentlycontinue
+
+Write-Host 'Cleaning packages'
+Remove-Item packages\* -Recurse -Force -errorAction silentlycontinue
diff --git a/scripts/azure-pipelines/windows/provision-image.ps1 b/scripts/azure-pipelines/windows/provision-image.ps1
new file mode 100644
index 000000000..9a33461ee
--- /dev/null
+++ b/scripts/azure-pipelines/windows/provision-image.ps1
@@ -0,0 +1,447 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+
+<#
+.SYNOPSIS
+Sets up a machine to be an image for a scale set.
+
+.DESCRIPTION
+provision-image.ps1 runs on an existing, freshly provisioned virtual machine,
+and sets that virtual machine up as a vcpkg build machine. After this is done,
+(outside of this script), we take that machine and make it an image to be copied
+for setting up new VMs in the scale set.
+
+This script must either be run as admin, or one must pass AdminUserPassword;
+if the script is run with AdminUserPassword, it runs itself again as an
+administrator.
+
+.PARAMETER AdminUserPassword
+The administrator user's password; if this is $null, or not passed, then the
+script assumes it's running on an administrator account.
+
+.PARAMETER StorageAccountName
+The name of the storage account. Stored in the environment variable %StorageAccountName%.
+Used by the CI system to access the global storage.
+
+.PARAMETER StorageAccountKey
+The key of the storage account. Stored in the environment variable %StorageAccountKey%.
+Used by the CI system to access the global storage.
+#>
+param(
+ [string]$AdminUserPassword = $null,
+ [string]$StorageAccountName = $null,
+ [string]$StorageAccountKey = $null
+)
+
+$ErrorActionPreference = 'Stop'
+
+<#
+.SYNOPSIS
+Gets a random file path in the temp directory.
+
+.DESCRIPTION
+Get-TempFilePath takes an extension, and returns a path with a random
+filename component in the temporary directory with that extension.
+
+.PARAMETER Extension
+The extension to use for the path.
+#>
+Function Get-TempFilePath {
+ Param(
+ [String]$Extension
+ )
+
+ if ([String]::IsNullOrWhiteSpace($Extension)) {
+ throw 'Missing Extension'
+ }
+
+ $tempPath = [System.IO.Path]::GetTempPath()
+ $tempName = [System.IO.Path]::GetRandomFileName() + '.' + $Extension
+ return Join-Path $tempPath $tempName
+}
+
+if (-not [string]::IsNullOrEmpty($AdminUserPassword)) {
+ Write-Host "AdminUser password supplied; switching to AdminUser"
+ $PsExecPath = Get-TempFilePath -Extension 'exe'
+ Write-Host "Downloading psexec to $PsExecPath"
+ & curl.exe -L -o $PsExecPath -s -S https://live.sysinternals.com/PsExec64.exe
+ $PsExecArgs = @(
+ '-u',
+ 'AdminUser',
+ '-p',
+ $AdminUserPassword,
+ '-accepteula',
+ '-h',
+ 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe',
+ '-ExecutionPolicy',
+ 'Unrestricted',
+ '-File',
+ $PSCommandPath
+ )
+
+ if (-Not ([string]::IsNullOrWhiteSpace($StorageAccountName))) {
+ $PsExecArgs += '-StorageAccountName'
+ $PsExecArgs += $StorageAccountName
+ }
+
+ if (-Not ([string]::IsNullOrWhiteSpace($StorageAccountKey))) {
+ $PsExecArgs += '-StorageAccountKey'
+ $PsExecArgs += $StorageAccountKey
+ }
+
+ Write-Host "Executing $PsExecPath " + @PsExecArgs
+
+ $proc = Start-Process -FilePath $PsExecPath -ArgumentList $PsExecArgs -Wait -PassThru
+ Write-Host 'Cleaning up...'
+ Remove-Item $PsExecPath
+ exit $proc.ExitCode
+}
+
+$VisualStudioBootstrapperUrl = 'https://aka.ms/vs/16/release/vs_enterprise.exe'
+$Workloads = @(
+ 'Microsoft.VisualStudio.Workload.NativeDesktop',
+ 'Microsoft.VisualStudio.Workload.Universal',
+ 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
+ 'Microsoft.VisualStudio.Component.VC.Tools.ARM',
+ 'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
+ 'Microsoft.VisualStudio.Component.VC.ATL',
+ 'Microsoft.VisualStudio.Component.VC.ATLMFC',
+ 'Microsoft.VisualStudio.Component.VC.v141.x86.x64.Spectre',
+ 'Microsoft.VisualStudio.Component.Windows10SDK.18362',
+ 'Microsoft.Net.Component.4.8.SDK',
+ 'Microsoft.Component.NetFX.Native'
+)
+
+$MpiUrl = 'https://download.microsoft.com/download/A/E/0/AE002626-9D9D-448D-8197-1EA510E297CE/msmpisetup.exe'
+
+$CudaUrl = 'https://developer.download.nvidia.com/compute/cuda/10.1/Prod/local_installers/cuda_10.1.243_426.00_win10.exe'
+$CudaFeatures = 'nvcc_10.1 cuobjdump_10.1 nvprune_10.1 cupti_10.1 gpu_library_advisor_10.1 memcheck_10.1 ' + `
+ 'nvdisasm_10.1 nvprof_10.1 visual_profiler_10.1 visual_studio_integration_10.1 cublas_10.1 cublas_dev_10.1 ' + `
+ 'cudart_10.1 cufft_10.1 cufft_dev_10.1 curand_10.1 curand_dev_10.1 cusolver_10.1 cusolver_dev_10.1 cusparse_10.1 ' + `
+ 'cusparse_dev_10.1 nvgraph_10.1 nvgraph_dev_10.1 npp_10.1 npp_dev_10.1 nvrtc_10.1 nvrtc_dev_10.1 nvml_dev_10.1 ' + `
+ 'occupancy_calculator_10.1 fortran_examples_10.1'
+
+$BinSkimUrl = 'https://www.nuget.org/api/v2/package/Microsoft.CodeAnalysis.BinSkim/1.6.0'
+
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+
+<#
+.SYNOPSIS
+Writes a message to the screen depending on ExitCode.
+
+.DESCRIPTION
+Since msiexec can return either 0 or 3010 successfully, in both cases
+we write that installation succeeded, and which exit code it exited with.
+If msiexec returns anything else, we write an error.
+
+.PARAMETER ExitCode
+The exit code that msiexec returned.
+#>
+Function PrintMsiExitCodeMessage {
+ Param(
+ $ExitCode
+ )
+
+ # 3010 is probably ERROR_SUCCESS_REBOOT_REQUIRED
+ if ($ExitCode -eq 0 -or $ExitCode -eq 3010) {
+ Write-Host "Installation successful! Exited with $ExitCode."
+ }
+ else {
+ Write-Error "Installation failed! Exited with $ExitCode."
+ }
+}
+
+<#
+.SYNOPSIS
+Install Visual Studio.
+
+.DESCRIPTION
+InstallVisualStudio takes the $Workloads array, and installs it with the
+installer that's pointed at by $BootstrapperUrl.
+
+.PARAMETER Workloads
+The set of VS workloads to install.
+
+.PARAMETER BootstrapperUrl
+The URL of the Visual Studio installer, i.e. one of vs_*.exe.
+
+.PARAMETER InstallPath
+The path to install Visual Studio at.
+
+.PARAMETER Nickname
+The nickname to give the installation.
+#>
+Function InstallVisualStudio {
+ Param(
+ [String[]]$Workloads,
+ [String]$BootstrapperUrl,
+ [String]$InstallPath = $null,
+ [String]$Nickname = $null
+ )
+
+ try {
+ Write-Host 'Downloading Visual Studio...'
+ [string]$bootstrapperExe = Get-TempFilePath -Extension 'exe'
+ curl.exe -L -o $bootstrapperExe -s -S $BootstrapperUrl
+ Write-Host "Installing Visual Studio..."
+ $args = @('/c', $bootstrapperExe, '--quiet', '--norestart', '--wait', '--nocache')
+ foreach ($workload in $Workloads) {
+ $args += '--add'
+ $args += $workload
+ }
+
+ if (-not ([String]::IsNullOrWhiteSpace($InstallPath))) {
+ $args += '--installpath'
+ $args += $InstallPath
+ }
+
+ if (-not ([String]::IsNullOrWhiteSpace($Nickname))) {
+ $args += '--nickname'
+ $args += $Nickname
+ }
+
+ $proc = Start-Process -FilePath cmd.exe -ArgumentList $args -Wait -PassThru
+ PrintMsiExitCodeMessage $proc.ExitCode
+ }
+ catch {
+ Write-Error "Failed to install Visual Studio! $($_.Exception.Message)"
+ }
+}
+
+<#
+.SYNOPSIS
+Install a .msi file.
+
+.DESCRIPTION
+InstallMSI takes a url where an .msi lives, and installs that .msi to the system.
+
+.PARAMETER Name
+The name of the thing to install.
+
+.PARAMETER Url
+The URL at which the .msi lives.
+#>
+Function InstallMSI {
+ Param(
+ [String]$Name,
+ [String]$Url
+ )
+
+ try {
+ Write-Host "Downloading $Name..."
+ [string]$msiPath = Get-TempFilePath -Extension 'msi'
+ curl.exe -L -o $msiPath -s -S $Url
+ Write-Host "Installing $Name..."
+ $args = @('/i', $msiPath, '/norestart', '/quiet', '/qn')
+ $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $args -Wait -PassThru
+ PrintMsiExitCodeMessage $proc.ExitCode
+ }
+ catch {
+ Write-Error "Failed to install $Name! $($_.Exception.Message)"
+ }
+}
+
+<#
+.SYNOPSIS
+Unpacks a zip file to $Dir.
+
+.DESCRIPTION
+InstallZip takes a URL of a zip file, and unpacks the zip file to the directory
+$Dir.
+
+.PARAMETER Name
+The name of the tool being installed.
+
+.PARAMETER Url
+The URL of the zip file to unpack.
+
+.PARAMETER Dir
+The directory to unpack the zip file to.
+#>
+Function InstallZip {
+ Param(
+ [String]$Name,
+ [String]$Url,
+ [String]$Dir
+ )
+
+ try {
+ Write-Host "Downloading $Name..."
+ [string]$zipPath = Get-TempFilePath -Extension 'zip'
+ curl.exe -L -o $zipPath -s -S $Url
+ Write-Host "Installing $Name..."
+ Expand-Archive -Path $zipPath -DestinationPath $Dir -Force
+ }
+ catch {
+ Write-Error "Failed to install $Name! $($_.Exception.Message)"
+ }
+}
+
+<#
+.SYNOPSIS
+Installs MPI
+
+.DESCRIPTION
+Downloads the MPI installer located at $Url, and installs it with the
+correct flags.
+
+.PARAMETER Url
+The URL of the installer.
+#>
+Function InstallMpi {
+ Param(
+ [String]$Url
+ )
+
+ try {
+ Write-Host 'Downloading MPI...'
+ [string]$installerPath = Get-TempFilePath -Extension 'exe'
+ curl.exe -L -o $installerPath -s -S $Url
+ Write-Host 'Installing MPI...'
+ $proc = Start-Process -FilePath $installerPath -ArgumentList @('-force', '-unattend') -Wait -PassThru
+ $exitCode = $proc.ExitCode
+ if ($exitCode -eq 0) {
+ Write-Host 'Installation successful!'
+ }
+ else {
+ Write-Error "Installation failed! Exited with $exitCode."
+ }
+ }
+ catch {
+ Write-Error "Failed to install MPI! $($_.Exception.Message)"
+ }
+}
+
+<#
+.SYNOPSIS
+Installs NVIDIA's CUDA Toolkit.
+
+.DESCRIPTION
+InstallCuda installs the CUDA Toolkit with the features specified as a
+space separated list of strings in $Features.
+
+.PARAMETER Url
+The URL of the CUDA installer.
+
+.PARAMETER Features
+A space-separated list of features to install.
+#>
+Function InstallCuda {
+ Param(
+ [String]$Url,
+ [String]$Features
+ )
+
+ try {
+ Write-Host 'Downloading CUDA...'
+ [string]$installerPath = Get-TempFilePath -Extension 'exe'
+ curl.exe -L -o $installerPath -s -S $Url
+ Write-Host 'Installing CUDA...'
+ $proc = Start-Process -FilePath $installerPath -ArgumentList @('-s ' + $Features) -Wait -PassThru
+ $exitCode = $proc.ExitCode
+ if ($exitCode -eq 0) {
+ Write-Host 'Installation successful!'
+ }
+ else {
+ Write-Error "Installation failed! Exited with $exitCode."
+ }
+ }
+ catch {
+ Write-Error "Failed to install CUDA! $($_.Exception.Message)"
+ }
+}
+
+<#
+.SYNOPSIS
+Partitions a new physical disk.
+
+.DESCRIPTION
+Takes the disk $DiskNumber, turns it on, then partitions it for use with label
+$Label and drive letter $Letter.
+
+.PARAMETER DiskNumber
+The number of the disk to set up.
+
+.PARAMETER Letter
+The drive letter at which to mount the disk.
+
+.PARAMETER Label
+The label to give the disk.
+#>
+Function New-PhysicalDisk {
+ Param(
+ [int]$DiskNumber,
+ [string]$Letter,
+ [string]$Label
+ )
+
+ if ($Letter.Length -ne 1) {
+ throw "Bad drive letter $Letter, expected only one letter. (Did you accidentially add a : ?)"
+ }
+
+ try {
+ Write-Host "Attempting to online physical disk $DiskNumber"
+ [string]$diskpartScriptPath = Get-TempFilePath -Extension 'txt'
+ [string]$diskpartScriptContent =
+ "SELECT DISK $DiskNumber`r`n" +
+ "ONLINE DISK`r`n"
+
+ Write-Host "Writing diskpart script to $diskpartScriptPath with content:"
+ Write-Host $diskpartScriptContent
+ Set-Content -Path $diskpartScriptPath -Value $diskpartScriptContent
+ Write-Host 'Invoking DISKPART...'
+ & diskpart.exe /s $diskpartScriptPath
+
+ Write-Host "Provisioning physical disk $DiskNumber as drive $Letter"
+ [string]$diskpartScriptContent =
+ "SELECT DISK $DiskNumber`r`n" +
+ "ATTRIBUTES DISK CLEAR READONLY`r`n" +
+ "CREATE PARTITION PRIMARY`r`n" +
+ "FORMAT FS=NTFS LABEL=`"$Label`" QUICK`r`n" +
+ "ASSIGN LETTER=$Letter`r`n"
+ Write-Host "Writing diskpart script to $diskpartScriptPath with content:"
+ Write-Host $diskpartScriptContent
+ Set-Content -Path $diskpartScriptPath -Value $diskpartScriptContent
+ Write-Host 'Invoking DISKPART...'
+ & diskpart.exe /s $diskpartScriptPath
+ }
+ catch {
+ Write-Error "Failed to provision physical disk $DiskNumber as drive $Letter! $($_.Exception.Message)"
+ }
+}
+
+Write-Host "AdminUser password not supplied; assuming already running as AdminUser"
+
+New-PhysicalDisk -DiskNumber 2 -Letter 'E' -Label 'install disk'
+
+Write-Host 'Disabling pagefile...'
+wmic computersystem set AutomaticManagedPagefile=False
+wmic pagefileset delete
+
+Write-Host 'Configuring AntiVirus exclusions...'
+Add-MPPreference -ExclusionPath C:\
+Add-MPPreference -ExclusionPath D:\
+Add-MPPreference -ExclusionPath E:\
+Add-MPPreference -ExclusionProcess ninja.exe
+Add-MPPreference -ExclusionProcess clang-cl.exe
+Add-MPPreference -ExclusionProcess cl.exe
+Add-MPPreference -ExclusionProcess link.exe
+Add-MPPreference -ExclusionProcess python.exe
+
+InstallVisualStudio -Workloads $Workloads -BootstrapperUrl $VisualStudioBootstrapperUrl -Nickname 'Stable'
+InstallMpi -Url $MpiUrl
+InstallCuda -Url $CudaUrl -Features $CudaFeatures
+InstallZip -Url $BinSkimUrl -Name 'BinSkim' -Dir 'C:\BinSkim'
+if (-Not ([string]::IsNullOrWhiteSpace($StorageAccountName))) {
+ Write-Host 'Storing storage account name to environment'
+ Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' `
+ -Name StorageAccountName `
+ -Value $StorageAccountName
+}
+if (-Not ([string]::IsNullOrWhiteSpace($StorageAccountKey))) {
+ Write-Host 'Storing storage account key to environment'
+ Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' `
+ -Name StorageAccountKey `
+ -Value $StorageAccountKey
+}
diff --git a/scripts/azure-pipelines/windows/sysprep.ps1 b/scripts/azure-pipelines/windows/sysprep.ps1
new file mode 100644
index 000000000..c0965350d
--- /dev/null
+++ b/scripts/azure-pipelines/windows/sysprep.ps1
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: MIT
+#
+
+<#
+.SYNOPSIS
+Prepares the virtual machine for imaging.
+
+.DESCRIPTION
+Runs the `sysprep` utility to prepare the system for imaging.
+See https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/sysprep--system-preparation--overview
+for more information.
+#>
+
+$ErrorActionPreference = 'Stop'
+Write-Host 'Running sysprep'
+& C:\Windows\system32\sysprep\sysprep.exe /oobe /generalize /shutdown