[Windows] Implement new directories hierarchy (#8616)

This commit is contained in:
Vasilii Polikarpov
2023-11-15 11:24:45 +01:00
committed by GitHub
parent 84a7deae24
commit d1f2c9a3be
165 changed files with 1146 additions and 1139 deletions

View File

@@ -0,0 +1,65 @@
function Choco-Install {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $PackageName,
[string[]] $ArgumentList,
[int] $RetryCount = 5
)
process {
$count = 1
while($true)
{
Write-Host "Running [#$count]: choco install $packageName -y $argumentList"
choco install $packageName -y @argumentList --no-progress
$pkg = choco list --localonly $packageName --exact --all --limitoutput
if ($pkg) {
Write-Host "Package installed: $pkg"
break
}
else {
$count++
if ($count -ge $retryCount) {
Write-Host "Could not install $packageName after $count attempts"
exit 1
}
Start-Sleep -Seconds 30
}
}
}
}
function Send-RequestToChocolateyPackages {
param(
[Parameter(Mandatory)]
[string] $FilterQuery,
[string] $Url = "https://community.chocolatey.org/api",
[int] $ApiVersion = 2
)
$response = Invoke-RestMethod "$Url/v$ApiVersion/Packages()?$filterQuery"
return $response
}
function Get-LatestChocoPackageVersion {
param(
[Parameter(Mandatory)]
[string] $PackageName,
[Parameter(Mandatory)]
[string] $TargetVersion
)
$versionNumbers = $TargetVersion.Split(".")
[int]$versionNumbers[-1] += 1
$incrementedVersion = $versionNumbers -join "."
$filterQuery = "`$filter=(Id eq '$PackageName') and (IsPrerelease eq false) and (Version ge '$TargetVersion') and (Version lt '$incrementedVersion')"
$latestVersion = (Send-RequestToChocolateyPackages -FilterQuery $filterQuery).properties.Version |
Where-Object {$_ -Like "$TargetVersion.*" -or $_ -eq $TargetVersion} |
Sort-Object {[version]$_} |
Select-Object -Last 1
return $latestVersion
}

View File

@@ -0,0 +1,107 @@
@{
# Script module or binary module file associated with this manifest.
RootModule = 'ImageHelpers.psm1'
# Version number of this module.
ModuleVersion = '0.0.1'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = 'c9334909-16a1-48f1-a94a-c7baf1b961d9'
# Description of the functionality provided by this module
Description = 'Helper functions for creating vsts images'
# Minimum version of the Windows PowerShell engine required by this module
# PowerShellVersion = ''
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = '*'
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
# ProjectUri = ''
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}

View File

@@ -0,0 +1,61 @@
[CmdletBinding()]
param()
. $PSScriptRoot\PathHelpers.ps1
. $PSScriptRoot\InstallHelpers.ps1
. $PSScriptRoot\ChocoHelpers.ps1
. $PSScriptRoot\TestsHelpers.ps1
. $PSScriptRoot\VisualStudioHelpers.ps1
Export-ModuleMember -Function @(
'Connect-Hive'
'Disconnect-Hive'
'Test-MachinePath'
'Get-MachinePath'
'Get-DefaultPath'
'Set-MachinePath'
'Set-DefaultPath'
'Add-MachinePathItem'
'Add-DefaultPathItem'
'Add-DefaultItem'
'Get-SystemVariable'
'Get-DefaultVariable'
'Set-SystemVariable'
'Set-DefaultVariable'
'Install-Binary'
'Install-VisualStudio'
'Get-ToolsetContent'
'Get-ToolsetToolFullPath'
'Stop-SvcWithErrHandling'
'Set-SvcWithErrHandling'
'Start-DownloadWithRetry'
'Get-VsixExtenstionFromMarketplace'
'Install-VsixExtension'
'Get-VSExtensionVersion'
'Get-WinVersion'
'Test-IsWin22'
'Test-IsWin19'
'Choco-Install'
'Send-RequestToCocolateyPackages'
'Get-LatestChocoPackageVersion'
'Get-GitHubPackageDownloadUrl'
'Extract-7Zip'
'Get-CommandResult'
'Get-WhichTool'
'Get-EnvironmentVariable'
'Invoke-PesterTests'
'Invoke-SBWithRetry'
'Get-VsCatalogJsonPath'
'Install-AndroidSDKPackages'
'Get-AndroidPackages'
'Get-AndroidPackagesByName'
'Get-AndroidPackagesByVersion'
'Get-VisualStudioInstance'
'Get-VisualStudioComponents'
'Get-WindowsUpdatesHistory'
'New-ItemPath'
'Get-ModuleVersionAsJob'
'Use-ChecksumComparison'
'Get-HashFromGitHubReleaseBody'
'Test-FileSignature'
)

View File

@@ -0,0 +1,736 @@
function Install-Binary
{
<#
.SYNOPSIS
A helper function to install executables.
.DESCRIPTION
Download and install .exe or .msi binaries from specified URL.
.PARAMETER Url
The URL from which the binary will be downloaded. Required parameter.
.PARAMETER Name
The Name with which binary will be downloaded. Required parameter.
.PARAMETER ArgumentList
The list of arguments that will be passed to the installer. Required for .exe binaries.
.EXAMPLE
Install-Binary -Url "https://go.microsoft.com/fwlink/p/?linkid=2083338" -Name "winsdksetup.exe" -ArgumentList ("/features", "+", "/quiet") -ExpectedSignature "XXXXXXXXXXXXXXXXXXXXXXXXXX"
#>
Param
(
[Parameter(Mandatory, ParameterSetName="Url")]
[String] $Url,
[Parameter(Mandatory, ParameterSetName="Url")]
[String] $Name,
[Parameter(Mandatory, ParameterSetName="LocalPath")]
[String] $FilePath,
[String[]] $ArgumentList,
[String[]] $ExpectedSignature
)
if ($PSCmdlet.ParameterSetName -eq "LocalPath")
{
$name = Split-Path -Path $FilePath -Leaf
}
else
{
Write-Host "Downloading $Name..."
$filePath = Start-DownloadWithRetry -Url $Url -Name $Name
}
if ($PSBoundParameters.ContainsKey('ExpectedSignature'))
{
if ($ExpectedSignature)
{
Test-FileSignature -FilePath $filePath -ExpectedThumbprint $ExpectedSignature
}
else
{
throw "ExpectedSignature parameter is specified, but no signature is provided."
}
}
# MSI binaries should be installed via msiexec.exe
$fileExtension = ([System.IO.Path]::GetExtension($Name)).Replace(".", "")
if ($fileExtension -eq "msi")
{
if (-not $ArgumentList)
{
$ArgumentList = ('/i', $filePath, '/QN', '/norestart')
}
$filePath = "msiexec.exe"
}
try
{
$installStartTime = Get-Date
Write-Host "Starting Install $Name..."
$process = Start-Process -FilePath $filePath -ArgumentList $ArgumentList -Wait -PassThru
$exitCode = $process.ExitCode
$installCompleteTime = [math]::Round(($(Get-Date) - $installStartTime).TotalSeconds, 2)
if ($exitCode -eq 0 -or $exitCode -eq 3010)
{
Write-Host "Installation successful in $installCompleteTime seconds"
}
else
{
Write-Host "Non zero exit code returned by the installation process: $exitCode"
Write-Host "Total time elapsed: $installCompleteTime seconds"
exit $exitCode
}
}
catch
{
$installCompleteTime = [math]::Round(($(Get-Date) - $installStartTime).TotalSeconds, 2)
Write-Host "Failed to install the $fileExtension ${Name}: $($_.Exception.Message)"
Write-Host "Installation failed after $installCompleteTime seconds"
exit 1
}
}
function Stop-SvcWithErrHandling {
<#
.DESCRIPTION
Function for stopping the Windows Service with error handling
.PARAMETER ServiceName
The name of stopping service
.PARAMETER StopOnError
Switch for stopping the script and exit from PowerShell if one service is absent
#>
Param (
[Parameter(Mandatory, ValueFromPipeLine = $true)]
[string] $ServiceName,
[switch] $StopOnError
)
Process {
$service = Get-Service $ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Warning "[!] Service [$ServiceName] is not found"
if ($StopOnError) {
exit 1
}
} else {
Write-Host "Try to stop service [$ServiceName]"
try {
Stop-Service -Name $ServiceName -Force
$service.WaitForStatus("Stopped", "00:01:00")
Write-Host "Service [$ServiceName] has been stopped successfuly"
} catch {
Write-Error "[!] Failed to stop service [$ServiceName] with error:"
$_ | Out-String | Write-Error
}
}
}
}
function Set-SvcWithErrHandling {
<#
.DESCRIPTION
Function for setting the Windows Service parameter with error handling
.PARAMETER ServiceName
The name of stopping service
.PARAMETER Arguments
Hashtable for service arguments
.PARAMETER StopOnError
Switch for stopping the script and exit from PowerShell if one service is absent
#>
Param (
[Parameter(Mandatory, ValueFromPipeLine = $true)]
[string] $ServiceName,
[Parameter(Mandatory)]
[hashtable] $Arguments,
[switch] $StopOnError
)
Process {
$service = Get-Service $ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Warning "[!] Service [$ServiceName] is not found"
if ($StopOnError) {
exit 1
}
} else {
try {
Set-Service $serviceName @Arguments
} catch {
Write-Error "[!] Failed to set service [$ServiceName] arguments with error:"
$_ | Out-String | Write-Error
}
}
}
}
function Start-DownloadWithRetry
{
Param
(
[Parameter(Mandatory)]
[string] $Url,
[string] $Name,
[string] $DownloadPath = "${env:Temp}",
[int] $Retries = 20
)
if ([String]::IsNullOrEmpty($Name)) {
$Name = [IO.Path]::GetFileName($Url)
}
$filePath = Join-Path -Path $DownloadPath -ChildPath $Name
$downloadStartTime = Get-Date
# Default retry logic for the package.
while ($Retries -gt 0)
{
try
{
$downloadAttemptStartTime = Get-Date
Write-Host "Downloading package from: $Url to path $filePath ."
(New-Object System.Net.WebClient).DownloadFile($Url, $filePath)
break
}
catch
{
$failTime = [math]::Round(($(Get-Date) - $downloadStartTime).TotalSeconds, 2)
$attemptTime = [math]::Round(($(Get-Date) - $downloadAttemptStartTime).TotalSeconds, 2)
Write-Host "There is an error encounterd after $attemptTime seconds during package downloading:`n $_"
$Retries--
if ($Retries -eq 0)
{
Write-Host "File can't be downloaded. Please try later or check that file exists by url: $Url"
Write-Host "Total time elapsed $failTime"
exit 1
}
Write-Host "Waiting 30 seconds before retrying. Retries left: $Retries"
Start-Sleep -Seconds 30
}
}
$downloadCompleteTime = [math]::Round(($(Get-Date) - $downloadStartTime).TotalSeconds, 2)
Write-Host "Package downloaded successfully in $downloadCompleteTime seconds"
return $filePath
}
function Get-VsixExtenstionFromMarketplace {
Param
(
[string] $ExtensionMarketPlaceName,
[string] $MarketplaceUri = "https://marketplace.visualstudio.com/items?itemName="
)
$extensionUri = $MarketplaceUri + $ExtensionMarketPlaceName
$request = Invoke-SBWithRetry -Command { Invoke-WebRequest -Uri $extensionUri -UseBasicParsing } -RetryCount 20 -RetryIntervalSeconds 30
$request -match 'UniqueIdentifierValue":"(?<extensionname>[^"]*)' | Out-Null
$extensionName = $Matches.extensionname
$request -match 'VsixId":"(?<vsixid>[^"]*)' | Out-Null
$vsixId = $Matches.vsixid
$request -match 'AssetUri":"(?<uri>[^"]*)' | Out-Null
$assetUri = $Matches.uri
$request -match 'Microsoft\.VisualStudio\.Services\.Payload\.FileName":"(?<filename>[^"]*)' | Out-Null
$fileName = $Matches.filename
$downloadUri = $assetUri + "/" + $fileName
# ProBITools.MicrosoftReportProjectsforVisualStudio2022 has different URL https://github.com/actions/runner-images/issues/5340
switch ($ExtensionMarketPlaceName) {
"ProBITools.MicrosoftReportProjectsforVisualStudio2022" {
$fileName = "Microsoft.DataTools.ReportingServices.vsix"
$downloadUri = "https://download.microsoft.com/download/b/b/5/bb57be7e-ae72-4fc0-b528-d0ec224997bd/Microsoft.DataTools.ReportingServices.vsix"
}
"ProBITools.MicrosoftAnalysisServicesModelingProjects2022" {
$fileName = "Microsoft.DataTools.AnalysisServices.vsix"
$downloadUri = "https://download.microsoft.com/download/c/8/9/c896a7f2-d0fd-45ac-90e6-ff61f67523cb/Microsoft.DataTools.AnalysisServices.vsix"
}
# Starting from version 4.1 SqlServerIntegrationServicesProjects extension is distributed as exe file
"SSIS.SqlServerIntegrationServicesProjects" {
$fileName = "Microsoft.DataTools.IntegrationServices.exe"
$downloadUri = $assetUri + "/" + $fileName
}
}
return [PSCustomObject] @{
"ExtensionName" = $extensionName
"VsixId" = $vsixId
"FileName" = $fileName
"DownloadUri" = $downloadUri
}
}
function Install-VsixExtension
{
Param
(
[string] $Url,
[Parameter(Mandatory = $true)]
[string] $Name,
[string] $FilePath,
[int] $Retries = 20,
[switch] $InstallOnly
)
if (-not $InstallOnly)
{
$FilePath = Start-DownloadWithRetry -Url $Url -Name $Name
}
$argumentList = ('/quiet', "`"$FilePath`"")
do
{
Write-Host "Starting Install $Name..."
try
{
$installPath = ${env:ProgramFiles(x86)}
# There are 2 types of packages at the moment - exe and vsix
if ($Name -match "vsix")
{
$process = Start-Process -FilePath "${installPath}\Microsoft Visual Studio\Installer\resources\app\ServiceHub\Services\Microsoft.VisualStudio.Setup.Service\VSIXInstaller.exe" -ArgumentList $argumentList -Wait -PassThru
}
else
{
$process = Start-Process -FilePath ${env:Temp}\$Name /Q -Wait -PassThru
}
}
catch
{
Write-Host "There is an error during $Name installation"
$_
exit 1
}
$exitCode = $process.ExitCode
if ($exitCode -eq 0 -or $exitCode -eq 1001) # 1001 means the extension is already installed
{
Write-Host "$Name installed successfully"
}
else
{
Write-Host "Unsuccessful exit code returned by the installation process: $exitCode."
$Retries--
if ($Retries -eq 0) {
Write-Host "The $Name couldn't be installed after 20 attempts."
exit 1
} else {
Write-Host "Waiting 10 seconds before retrying. Retries left: $Retries"
Start-Sleep -Seconds 10
}
}
} until ($exitCode -eq 0 -or $exitCode -eq 1001 -or $Retries -eq 0 )
#Cleanup downloaded installation files
if (-not $InstallOnly)
{
Remove-Item -Force -Confirm:$false $FilePath
}
}
function Get-VSExtensionVersion
{
Param
(
[Parameter(Mandatory=$true)]
[string] $packageName
)
$instanceFolders = Get-ChildItem -Path "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances"
if ($instanceFolders -is [array])
{
Write-Host ($instanceFolders | Out-String)
Write-Host ($instanceFolders | Get-ChildItem | Out-String)
Write-Host "More than one instance installed"
exit 1
}
$stateContent = Get-Content -Path (Join-Path $instanceFolders.FullName '\state.packages.json')
$state = $stateContent | ConvertFrom-Json
$packageVersion = ($state.packages | Where-Object { $_.id -eq $packageName }).version
if (-not $packageVersion)
{
Write-Host "Installed package $packageName for Visual Studio was not found"
exit 1
}
return $packageVersion
}
function Get-ToolsetContent
{
$toolsetPath = Join-Path "C:\\image" "toolset.json"
$toolsetJson = Get-Content -Path $toolsetPath -Raw
ConvertFrom-Json -InputObject $toolsetJson
}
function Get-ToolcacheToolDirectory {
Param ([string] $ToolName)
$toolcacheRootPath = Resolve-Path $env:AGENT_TOOLSDIRECTORY
return Join-Path $toolcacheRootPath $ToolName
}
function Get-ToolsetToolFullPath
{
<#
.DESCRIPTION
Function that return full path to specified toolset tool.
.PARAMETER Name
The name of required tool.
.PARAMETER Version
The version of required tool.
.PARAMETER Arch
The architecture of required tool.
#>
Param
(
[Parameter(Mandatory=$true)]
[string] $Name,
[Parameter(Mandatory=$true)]
[string] $Version,
[string] $Arch = "x64"
)
$toolPath = Get-ToolcacheToolDirectory -ToolName $Name
# Add wildcard if missing
if ($Version.Split(".").Length -lt 3) {
$Version += ".*"
}
$versionPath = Join-Path $toolPath $Version
# Take latest installed version in case if toolset version contains wildcards
$foundVersion = Get-Item $versionPath `
| Sort-Object -Property {[version]$_.name} -Descending `
| Select-Object -First 1
if (-not $foundVersion) {
return $null
}
return Join-Path $foundVersion $Arch
}
function Get-WinVersion
{
(Get-CimInstance -ClassName Win32_OperatingSystem).Caption
}
function Test-IsWin22
{
(Get-WinVersion) -match "2022"
}
function Test-IsWin19
{
(Get-WinVersion) -match "2019"
}
function Extract-7Zip {
Param
(
[Parameter(Mandatory=$true)]
[string]$Path,
[Parameter(Mandatory=$true)]
[string]$DestinationPath,
[ValidateSet("x", "e")]
[char]$ExtractMethod = "x"
)
Write-Host "Expand archive '$PATH' to '$DestinationPath' directory"
7z.exe $ExtractMethod "$Path" -o"$DestinationPath" -y | Out-Null
if ($LASTEXITCODE -ne 0)
{
Write-Host "There is an error during expanding '$Path' to '$DestinationPath' directory"
exit 1
}
}
function Install-AndroidSDKPackages {
Param
(
[Parameter(Mandatory=$true)]
[string]$AndroidSDKManagerPath,
[Parameter(Mandatory=$true)]
[string]$AndroidSDKRootPath,
[Parameter(Mandatory=$true)]
[AllowEmptyCollection()]
[string[]]$AndroidPackages,
[string] $PrefixPackageName
)
foreach ($package in $AndroidPackages) {
& $AndroidSDKManagerPath --sdk_root=$AndroidSDKRootPath "$PrefixPackageName$package"
}
}
function Get-AndroidPackages {
Param
(
[Parameter(Mandatory=$true)]
[string]$AndroidSDKManagerPath
)
$packagesListFile = "C:\Android\android-sdk\packages-list.txt"
if (-Not (Test-Path -Path $packagesListFile -PathType Leaf)) {
(cmd /c "$AndroidSDKManagerPath --list --verbose 2>&1") |
Where-Object { $_ -Match "^[^\s]" } |
Where-Object { $_ -NotMatch "^(Loading |Info: Parsing |---|\[=+|Installed |Available )" } |
Where-Object { $_ -NotMatch "^[^;]*$" } |
Out-File -FilePath $packagesListFile
}
return Get-Content $packagesListFile
}
function Get-AndroidPackagesByName {
Param (
[Parameter(Mandatory=$true)]
[string[]]$AndroidPackages,
[Parameter(Mandatory=$true)]
[string]$PrefixPackageName
)
return $AndroidPackages | Where-Object { "$_".StartsWith($PrefixPackageName) }
}
function Get-AndroidPackagesByVersion {
Param (
[Parameter(Mandatory=$true)]
[string[]]$AndroidPackages,
[Parameter(Mandatory=$true)]
[string]$PrefixPackageName,
[object]$MinimumVersion,
[char]$Delimiter,
[int]$Index = 0
)
$Type = $MinimumVersion.GetType()
$packagesByName = Get-AndroidPackagesByName -AndroidPackages $AndroidPackages -PrefixPackageName $PrefixPackageName
$packagesByVersion = $packagesByName | Where-Object { ($_.Split($Delimiter)[$Index] -as $Type) -ge $MinimumVersion }
return $packagesByVersion | Sort-Object -Unique
}
function Get-WindowsUpdatesHistory {
$allEvents = @{}
# 19 - Installation Successful: Windows successfully installed the following update
# 20 - Installation Failure: Windows failed to install the following update with error
# 43 - Installation Started: Windows has started installing the following update
$filter = @{
LogName = "System"
Id = 19, 20, 43
ProviderName = "Microsoft-Windows-WindowsUpdateClient"
}
$events = Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue | Sort-Object Id
foreach ( $event in $events ) {
switch ( $event.Id ) {
19 {
$status = "Successful"
$title = $event.Properties[0].Value
$allEvents[$title] = ""
break
}
20 {
$status = "Failure"
$title = $event.Properties[1].Value
$allEvents[$title] = ""
break
}
43 {
$status = "InProgress"
$title = $event.Properties[0].Value
break
}
}
if ( $status -eq "InProgress" -and $allEvents.ContainsKey($title) ) {
continue
}
[PSCustomObject]@{
Status = $status
Title = $title
}
}
}
function Invoke-SBWithRetry {
param (
[scriptblock] $Command,
[int] $RetryCount = 10,
[int] $RetryIntervalSeconds = 5
)
while ($RetryCount -gt 0) {
try {
& $Command
return
}
catch {
Write-Host "There is an error encountered:`n $_"
$RetryCount--
if ($RetryCount -eq 0) {
exit 1
}
Write-Host "Waiting $RetryIntervalSeconds seconds before retrying. Retries left: $RetryCount"
Start-Sleep -Seconds $RetryIntervalSeconds
}
}
}
function Get-GitHubPackageDownloadUrl {
param (
[string]$RepoOwner,
[string]$RepoName,
[string]$BinaryName,
[string]$Version,
[string]$UrlFilter,
[boolean]$IsPrerelease = $false,
[boolean]$LatestReleaseOnly = $true,
[int]$SearchInCount = 100
)
if ($Version -eq "latest") {
$Version = "*"
}
$json = Invoke-RestMethod -Uri "https://api.github.com/repos/${RepoOwner}/${RepoName}/releases?per_page=${SearchInCount}"
$tags = $json.Where{ $_.prerelease -eq $IsPrerelease -and $_.assets }.tag_name
$availableVersions = $tags |
Select-String -Pattern "\d+.\d+.\d+" |
ForEach-Object { $_.Matches.Value } |
Where-Object { $_ -like "$Version.*" -or $_ -eq $Version } |
Sort-Object -Descending { [version]$_ }
if (-not $availableVersions) {
throw "Failed to get available versions from ${RepoOwner}/${RepoName} releases"
}
if ($LatestReleaseOnly) {
$latestVersion = $availableVersions | Select-Object -First 1
$urlFilterReplaced = $UrlFilter -replace "{BinaryName}", $BinaryName -replace "{Version}", $latestVersion
$downloadUrl = $json.assets.browser_download_url -like $urlFilterReplaced
} else {
foreach ($version in $availableVersions) {
$urlFilterReplaced = $UrlFilter -replace "{BinaryName}", $BinaryName -replace "{Version}", $version
$downloadUrl = $json.assets.browser_download_url -like $urlFilterReplaced
if ($downloadUrl) {
Write-Host "Found download url for ${RepoOwner}/${RepoName} ${BinaryName} ${version}"
break
}
}
}
if (-not $downloadUrl) {
throw "Failed to get download url for ${RepoOwner}/${RepoName} ${BinaryName}"
}
return $downloadUrl
}
function Use-ChecksumComparison {
param (
[Parameter(Mandatory=$true)]
[string]$LocalFileHash,
[Parameter(Mandatory=$true)]
[string]$DistributorFileHash
)
Write-Verbose "Performing checksum verification"
if ($LocalFileHash -ne $DistributorFileHash) {
throw "Checksum verification failed. Expected hash: $DistributorFileHash; Actual hash: $LocalFileHash."
} else {
Write-Verbose "Checksum verification passed"
}
}
function Get-HashFromGitHubReleaseBody {
param (
[string]$RepoOwner,
[string]$RepoName,
[Parameter(Mandatory=$true)]
[string]$FileName,
[string]$Url,
[string]$Version = "latest",
[boolean]$IsPrerelease = $false,
[int]$SearchInCount = 100,
[string]$Delimiter = '|',
[int]$WordNumber = 1
)
if ($Url) {
$releaseUrl = $Url
} else {
if ($Version -eq "latest") {
$releaseUrl = "https://api.github.com/repos/${RepoOwner}/${RepoName}/releases/latest"
} else {
$json = Invoke-RestMethod -Uri "https://api.github.com/repos/${RepoOwner}/${RepoName}/releases?per_page=${SearchInCount}"
$tags = $json.Where{ $_.prerelease -eq $IsPrerelease }.tag_name
$tag = $tags -match $Version
if (-not $tag) {
throw "Failed to get a tag name for version $Version."
}
$releaseUrl = "https://api.github.com/repos/${RepoOwner}/${RepoName}/releases/tag/$tag"
}
}
$body = (Invoke-RestMethod -Uri $releaseUrl).body -replace('`', "") -join "`n"
$matchingLine = $body.Split("`n") | Where-Object { $_ -like "*$FileName*" }
if ([string]::IsNullOrEmpty($matchingLine)) {
throw "File name '$FileName' not found in release body."
}
$result = $matchingLine.Split($Delimiter)[$WordNumber] -replace "[^a-zA-Z0-9]", ""
if ([string]::IsNullOrEmpty($result)) {
throw "Empty result. Check Split method parameters (delimiter and/or word number) for the matching line."
}
return $result
}
function Test-FileSignature {
param(
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$true)]
[string[]]$ExpectedThumbprint
)
$signature = Get-AuthenticodeSignature $FilePath
if ($signature.Status -ne "Valid") {
throw "Signature status is not valid. Status: $($signature.Status)"
}
foreach ($thumbprint in $ExpectedThumbprint) {
if ($signature.SignerCertificate.Thumbprint.Contains($thumbprint)) {
Write-Output "Signature for $FilePath is valid"
$signatureMatched = $true
return
}
}
if ($signatureMatched) {
Write-Output "Signature for $FilePath is valid"
}
else {
throw "Signature thumbprint do not match expected."
}
}

View File

@@ -0,0 +1,165 @@
function Connect-Hive {
param(
[string]$FileName = "C:\Users\Default\NTUSER.DAT",
[string]$SubKey = "HKLM\DEFAULT"
)
Write-Host "Loading the file $FileName to the Key $SubKey"
if (Test-Path $SubKey.Replace("\",":")) {
return
}
$result = reg load $SubKey $FileName *>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to load hive: $result"
exit 1
}
}
function Disconnect-Hive {
param(
[string]$SubKey = "HKLM\DEFAULT"
)
Write-Host "Unloading the hive $SubKey"
if (-not (Test-Path $SubKey.Replace("\",":"))) {
return
}
$result = reg unload $SubKey *>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to unload hive: $result"
exit 1
}
}
function Get-SystemVariable {
param(
[string]$SystemVariable
)
[System.Environment]::GetEnvironmentVariable($SystemVariable, "Machine")
}
function Get-DefaultVariable {
param(
[string]$DefaultVariable,
[string]$Name = "DEFAULT\Environment",
[bool]$Writable = $false
)
$key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($Name, $Writable)
$key.GetValue($DefaultVariable, "", "DoNotExpandEnvironmentNames")
$key.Handle.Close()
[System.GC]::Collect()
}
function Set-SystemVariable {
param(
[string]$SystemVariable,
[string]$Value
)
[System.Environment]::SetEnvironmentVariable($SystemVariable, $Value, "Machine")
Get-SystemVariable $SystemVariable
}
function Set-DefaultVariable {
param(
[string]$DefaultVariable,
[string]$Value,
[string]$Name = "DEFAULT\Environment",
[string]$Kind = "ExpandString",
[bool]$Writable = $true
)
$key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($Name, $Writable)
$key.SetValue($DefaultVariable, $Value, $Kind)
Get-DefaultVariable -DefaultVariable $DefaultVariable -Name $Name
$key.Handle.Close()
[System.GC]::Collect()
}
function Get-MachinePath {
Get-SystemVariable PATH
}
function Get-DefaultPath {
Get-DefaultVariable Path
}
function Set-MachinePath {
param(
[string]$NewPath
)
Set-SystemVariable PATH $NewPath
}
function Set-DefaultPath {
param(
[string]$NewPath
)
Set-DefaultVariable PATH $NewPath
}
function Test-MachinePath {
param(
[string]$PathItem
)
$pathItems = (Get-MachinePath).Split(';')
$pathItems.Contains($PathItem)
}
function Add-MachinePathItem {
param(
[string]$PathItem
)
$currentPath = Get-MachinePath
$newPath = $PathItem + ';' + $currentPath
Set-MachinePath -NewPath $newPath
}
function Add-DefaultPathItem {
param(
[string]$PathItem
)
Connect-Hive
$currentPath = Get-DefaultPath
$newPath = $PathItem + ';' + $currentPath
Set-DefaultPath -NewPath $newPath
Disconnect-Hive
}
function Add-DefaultItem {
param(
[string]$DefaultVariable,
[string]$Value,
[string]$Name = "DEFAULT\Environment",
[string]$Kind = "ExpandString",
[bool]$Writable = $true
)
Connect-Hive
$regPath = Join-Path "HKLM:\" $Name
if (-not (Test-Path $Name)) {
Write-Host "Creating $regPath key"
New-Item -Path $regPath -Force | Out-Null
}
Set-DefaultVariable -DefaultVariable $DefaultVariable -Value $Value -Name $Name -Kind $Kind -Writable $Writable
Disconnect-Hive
}
function New-ItemPath {
param (
[string]$Path
)
if (-not (Test-Path $Path)) {
New-Item -Path $Path -Force -ErrorAction Ignore | Out-Null
}
}

View File

@@ -0,0 +1,200 @@
function Get-CommandResult {
Param (
[Parameter(Mandatory)][string] $Command
)
# CMD trick to suppress and show error output because some commands write to stderr (for example, "python --version")
[string[]]$output = & $env:comspec /c "$Command 2>&1"
$exitCode = $LASTEXITCODE
return @{
Output = $output
ExitCode = $exitCode
}
}
# Gets path to the tool, analogue of 'which tool'
function Get-WhichTool($tool) {
return (Get-Command $tool).Path
}
# Gets value of environment variable by the name
function Get-EnvironmentVariable($variable) {
return [System.Environment]::GetEnvironmentVariable($variable, "Machine")
}
# Update environment variables without reboot
function Update-Environment {
$variables = [Environment]::GetEnvironmentVariables("Machine")
$variables.Keys | ForEach-Object {
$key = $_
$value = $variables[$key]
Set-Item -Path "env:$key" -Value $value
}
# We need to refresh PATH the latest one because it could include other variables "%M2_HOME%/bin"
$env:PATH = [Environment]::GetEnvironmentVariable("PATH", "Machine")
}
# Run Pester tests for specific tool
function Invoke-PesterTests {
Param(
[Parameter(Mandatory)][string] $TestFile,
[string] $TestName
)
$testPath = "C:\image\tests\${TestFile}.Tests.ps1"
if (-not (Test-Path $testPath)) {
throw "Unable to find test file '$TestFile' on '$testPath'."
}
$configuration = [PesterConfiguration] @{
Run = @{ Path = $testPath; PassThru = $true }
Output = @{ Verbosity = "Detailed"; RenderMode = "Plaintext" }
}
if ($TestName) {
$configuration.Filter.FullName = $TestName
}
if ($TestFile -eq "*") {
$configuration.TestResult.Enabled = $true
$configuration.TestResult.OutputPath = "C:\image\tests\testResults.xml"
}
# Update environment variables without reboot
Update-Environment
# Switch ErrorActionPreference to Stop temporary to make sure that tests will on silent errors too
$backupErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Stop"
$results = Invoke-Pester -Configuration $configuration
$ErrorActionPreference = $backupErrorActionPreference
# Fail in case if no tests are run
if (-not ($results -and ($results.FailedCount -eq 0) -and ($results.PassedCount -gt 0))) {
$results
throw "Test run has failed"
}
}
# Pester Assert to check exit code of command
function ShouldReturnZeroExitCode {
Param(
[String] $ActualValue,
[switch] $Negate,
[string] $Because
)
$result = Get-CommandResult $ActualValue
[bool]$succeeded = $result.ExitCode -eq 0
if ($Negate) { $succeeded = -not $succeeded }
if (-not $succeeded)
{
$commandOutputIndent = " " * 4
$commandOutput = ($result.Output | ForEach-Object { "${commandOutputIndent}${_}" }) -join "`n"
$failureMessage = "Command '${ActualValue}' has finished with exit code ${actualExitCode}`n${commandOutput}"
}
return [PSCustomObject] @{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
# Pester Assert to check exit code of command with given parameter, the assertion performed up to 3 checks (without '-', with 1 and 2 '-') until succeeded
function ShouldReturnZeroExitCodeWithParam {
param (
[Parameter(Mandatory)] [string] $ActualValue,
[switch] $Negate,
[string] $CallParameter = "version",
[string] $CallerSessionState
)
$delimiterCharacter = ""
while ($delimiterCharacter.Length -le 2)
{
$callParameterWithDelimiter = $delimiterCharacter + $CallParameter
$commandToCheck = "$ActualValue $callParameterWithDelimiter"
[bool]$succeeded = (ShouldReturnZeroExitCode -ActualValue $commandToCheck).Succeeded
if ($succeeded)
{
break
}
$delimiterCharacter += '-'
}
if ($Negate) { $succeeded = -not $succeeded }
if (-not $succeeded)
{
$failureMessage = "Tool '$ActualValue' has not returned 0 exit code for any of these flags: '$CallParameter' or '-$CallParameter' or '--$CallParameter'"
}
return [PSCustomObject] @{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
# Pester Assert to match output of command
function ShouldMatchCommandOutput {
Param(
[String] $ActualValue,
[String] $RegularExpression,
[switch] $Negate
)
$output = (Get-CommandResult $ActualValue).Output | Out-String
[bool] $succeeded = $output -cmatch $RegularExpression
if ($Negate) {
$succeeded = -not $succeeded
}
$failureMessage = ''
if (-not $succeeded) {
if ($Negate) {
$failureMessage = "Expected regular expression '$RegularExpression' for '$ActualValue' command to not match '$output', but it did match."
}
else {
$failureMessage = "Expected regular expression '$RegularExpression' for '$ActualValue' command to match '$output', but it did not match."
}
}
return [PSCustomObject] @{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
If (Get-Command -Name Add-ShouldOperator -ErrorAction SilentlyContinue) {
Add-ShouldOperator -Name ReturnZeroExitCode -InternalName ShouldReturnZeroExitCode -Test ${function:ShouldReturnZeroExitCode}
Add-ShouldOperator -Name ReturnZeroExitCodeWithParam -InternalName ShouldReturnZeroExitCodeWithParam -Test ${function:ShouldReturnZeroExitCodeWithParam}
Add-ShouldOperator -Name MatchCommandOutput -InternalName ShouldMatchCommandOutput -Test ${function:ShouldMatchCommandOutput}
}
Function Get-ModuleVersionAsJob {
Param (
[Parameter(Mandatory)]
[String] $modulePath,
[Parameter(Mandatory)]
[String] $moduleName
)
# Script block to run commands in separate PowerShell environment
$testJob = Start-Job -ScriptBlock {
param (
$modulePath,
$moduleName
)
# Disable warning messages to prevent additional warnings about Az and Azurerm modules in the same session
$WarningPreference = "SilentlyContinue"
$env:PsModulePath = "$modulePath;$env:PsModulePath"
Import-Module -Name $moduleName
(Get-Module -Name $moduleName).Version.ToString()
} -ArgumentList $modulePath, $moduleName
$testJob | Wait-Job | Receive-Job | Out-File -FilePath "${env:TEMP}\module-version.txt"
Remove-Job $testJob
}

View File

@@ -0,0 +1,131 @@
Function Install-VisualStudio {
<#
.SYNOPSIS
A helper function to install Visual Studio.
.DESCRIPTION
Prepare system environment, and install Visual Studio bootstrapper with selected workloads.
.PARAMETER Version
The version of Visual Studio that will be installed. Required parameter.
.PARAMETER Edition
The edition of Visual Studio that will be installed. Required parameter.
.PARAMETER Channel
The channel of Visual Studio that will be installed. Required parameter.
.PARAMETER RequiredComponents
The list of required components. Required parameter.
.PARAMETER ExtraArgs
The extra arguments to pass to the bootstrapper. Optional parameter.
#>
Param
(
[Parameter(Mandatory)] [String] $Version,
[Parameter(Mandatory)] [String] $Edition,
[Parameter(Mandatory)] [String] $Channel,
[Parameter(Mandatory)] [String[]] $RequiredComponents,
[String] $ExtraArgs = "",
[Parameter(Mandatory)] [String] $SignatureThumbprint
)
$bootstrapperUrl = "https://aka.ms/vs/${Version}/${Channel}/vs_${Edition}.exe"
$channelUri = "https://aka.ms/vs/${Version}/${Channel}/channel"
$channelId = "VisualStudio.${Version}.Release"
$productId = "Microsoft.VisualStudio.Product.${Edition}"
Write-Host "Downloading Bootstrapper ..."
$BootstrapperName = [IO.Path]::GetFileName($BootstrapperUrl)
$bootstrapperFilePath = Start-DownloadWithRetry -Url $BootstrapperUrl -Name $BootstrapperName
# Verify that the bootstrapper is signed by Microsoft
Test-FileSignature -FilePath $bootstrapperFilePath -ExpectedThumbprint $SignatureThumbprint
try {
Write-Host "Enable short name support on Windows needed for Xamarin Android AOT, defaults appear to have been changed in Azure VMs"
$shortNameEnableProcess = Start-Process -FilePath fsutil.exe -ArgumentList ('8dot3name', 'set', '0') -Wait -PassThru
$shortNameEnableExitCode = $shortNameEnableProcess.ExitCode
if ($shortNameEnableExitCode -ne 0) {
Write-Host "Enabling short name support on Windows failed. This needs to be enabled prior to VS 2017 install for Xamarin Andriod AOT to work."
exit $shortNameEnableExitCode
}
$responseData = @{
"channelUri" = $channelUri
"channelId" = $channelId
"productId" = $productId
"arch" = "x64"
"add" = $RequiredComponents | ForEach-Object { "$_;includeRecommended" }
}
# Create json file with response data
$responseDataPath = "$env:TEMP\vs_install_response.json"
$responseData | ConvertTo-Json | Out-File -FilePath $responseDataPath
Write-Host "Starting Install ..."
$bootstrapperArgumentList = ('/c', $bootstrapperFilePath, '--in', $responseDataPath, $ExtraArgs, '--quiet', '--norestart', '--wait', '--nocache' )
Write-Host "Bootstrapper arguments: $bootstrapperArgumentList"
$process = Start-Process -FilePath cmd.exe -ArgumentList $bootstrapperArgumentList -Wait -PassThru
$exitCode = $process.ExitCode
if ($exitCode -eq 0 -or $exitCode -eq 3010) {
Write-Host "Installation successful"
return $exitCode
} else {
Write-Host "Non zero exit code returned by the installation process : $exitCode"
# Try to download tool to collect logs
$collectExeUrl = "https://aka.ms/vscollect.exe"
$collectExeName = [IO.Path]::GetFileName($collectExeUrl)
$collectExePath = Start-DownloadWithRetry -Url $collectExeUrl -Name $collectExeName
# Collect installation logs using the collect.exe tool and check if it is successful
& "$collectExePath"
if ($LastExitCode -ne 0) {
Write-Host "Failed to collect logs using collect.exe tool. Exit code : $LastExitCode"
exit $exitCode
}
# Expand the zip file
Expand-Archive -Path "$env:TEMP\vslogs.zip" -DestinationPath "$env:TEMP\vslogs"
# Print logs
$vsLogsPath = "$env:TEMP\vslogs"
$vsLogs = Get-ChildItem -Path $vsLogsPath -Recurse | Where-Object { -not $_.PSIsContainer } | Select-Object -ExpandProperty FullName
foreach ($log in $vsLogs) {
Write-Host "============================"
Write-Host "== Log file : $log "
Write-Host "============================"
Get-Content -Path $log -ErrorAction Continue
}
exit $exitCode
}
}
catch
{
Write-Host "Failed to install Visual Studio; $($_.Exception.Message)"
exit -1
}
}
function Get-VsCatalogJsonPath {
$instanceFolder = Get-Item "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances\*" | Select-Object -First 1
return Join-Path $instanceFolder.FullName "catalog.json"
}
function Get-VisualStudioInstance {
# Use -Prerelease and -All flags to make sure that Preview versions of VS are found correctly
$vsInstance = Get-VSSetupInstance -Prerelease -All | Where-Object { $_.DisplayName -match "Visual Studio" } | Select-Object -First 1
$vsInstance | Select-VSSetupInstance -Product *
}
function Get-VisualStudioComponents {
(Get-VisualStudioInstance).Packages | Where-Object type -in 'Component', 'Workload' |
Sort-Object Id, Version | Select-Object @{n = 'Package'; e = {$_.Id}}, Version |
Where-Object { $_.Package -notmatch "[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}" }
}

View File

@@ -0,0 +1,13 @@
$ModuleManifestName = 'ImageHelpers.psd1'
$ModuleManifestPath = "$PSScriptRoot\..\$ModuleManifestName"
Describe 'Module Manifest Tests' {
It 'Passes Test-ModuleManifest' {
Test-ModuleManifest -Path $ModuleManifestPath | Should Not BeNullOrEmpty
$? | Should Be $true
}
}

View File

@@ -0,0 +1,34 @@
. $PSScriptRoot\..\PathHelpers.ps1
Describe 'Test-MachinePath Tests' {
Mock Get-MachinePath {return "C:\foo;C:\bar"}
It 'Path contains item' {
Test-MachinePath -PathItem "C:\foo" | Should Be $true
}
It 'Path does not containe item' {
Test-MachinePath -PathItem "C:\baz" | Should Be $false
}
}
Describe 'Set-MachinePath Tests' {
Mock Get-MachinePath {return "C:\foo;C:\bar"}
Mock Set-ItemProperty {return}
It 'Set-MachinePath should return new path' {
Set-MachinePath -NewPath "C:\baz" | Should Be "C:\baz"
}
}
Describe "Add-MachinePathItem Tests"{
Mock Get-MachinePath {return "C:\foo;C:\bar"}
Mock Set-ItemProperty {return}
It 'Add-MachinePathItem should return complete path' {
Add-MachinePathItem -PathItem 'C:\baz' | Should Be 'C:\baz;C:\foo;C:\bar'
}
}
Describe 'Set-SystemVariable Tests' {
Mock Set-ItemProperty {return}
It 'Set-SystemVariable should return new path' {
Set-SystemVariable -SystemVariable "NewPathVar" -Value "C:\baz" | Should Be "C:\baz"
}
}