quickstarts/microsoft.devcenter/devbox-ready-to-code-image/tools/artifacts/_common/windows-visual-studio-marketplace-utils.psm1 (302 lines of code) (raw):

$ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $ProgressPreference = 'SilentlyContinue' Import-Module -Force (Join-Path $(Split-Path -Parent $PSScriptRoot) '_common/windows-retry-utils.psm1') # Function: Get-VisualStudioExtension # Description: Downloads a specified extension and all dependencies (Visual Studio or VS Code) from # the Marketplace and returns a list of local paths. function Get-VisualStudioExtension { param ( [Parameter(Mandatory = $true)] [string]$ExtensionReference, [Parameter(Mandatory = $false)] [string]$VersionNumber, [Parameter(Mandatory = $true)] [string]$DownloadLocation, [Parameter(Mandatory = $false)] [bool]$DownloadDependencies = $true, [Parameter(Mandatory = $false)] [bool]$DownloadPreRelease = $false ) $localPaths = [System.Collections.ArrayList]@() $targetPlatform = Get-CurrentPlatform try { Get-ExtensionMetadataDependencyTree -ExtensionReference $ExtensionReference ` -VersionNumber $VersionNumber ` -TargetPlatform $targetPlatform ` -DownloadDependencies $DownloadDependencies ` -DownloadPreRelease $DownloadPreRelease | ForEach-Object { $localPaths.Add((Import-ExtensionByMetadata -ExtensionMetadata $_ -DownloadLocation $DownloadLocation)) | Out-Null } return $localPaths } catch { throw "Failed to retrieve or save VSIX file for extension '$ExtensionReference' or its dependencies: $_" } } # Function: Get-ApiHeaders # Description: Constructs the headers required for the Marketplace API request. function Get-ApiHeaders { return @{ "Accept" = "application/json;api-version=3.0-preview.1" "Content-Type" = "application/json" } } # Function: Get-ApiFlags # Description: Constructs the flags for the Marketplace API request. # Flags: # - 2: Include Extension Metadata # - 16: Include Extension Properties # - 128: Include Asset URI # - 256: Include Files in the Response function Get-ApiFlags { $flags = 2 -bor 16 -bor 128 -bor 256 # Base flags using bitwise return $flags } # Function: Get-RequestBody # Description: Constructs the request body for the Marketplace API. function Get-RequestBody { param ( [Parameter(Mandatory = $true)] [string]$ExtensionReference, [Parameter(Mandatory = $true)] [int]$Flags ) return @{ filters = @(@{ criteria = @(@{ filterType = 7 # Filter type 7: Search by extension id value = $ExtensionReference }) }) flags = $Flags } | ConvertTo-Json -Depth 10 } # Function: Invoke-MarketplaceApi # Description: Sends a POST request to the Marketplace API and returns the response. function Invoke-MarketplaceApi { param ( [Parameter(Mandatory = $true)] [string]$ApiUrl, [Parameter(Mandatory = $true)] [hashtable]$Headers, [Parameter(Mandatory = $true)] [string]$Body ) return RunWithRetries -runBlock { return Invoke-RestMethod -Uri $ApiUrl -Method Post -Headers $Headers -Body $Body -MaximumRedirection 10 } -retryAttempts 3 -waitBeforeRetrySeconds 5 -exponentialBackoff } # Function: Get-ExtensionMetadataDependencyTree # Description: Provided with a VSIX name and version, find all metadata for the extension and its dependencies. function Get-ExtensionMetadataDependencyTree { param ( [Parameter(Mandatory = $true)] [string]$ExtensionReference, [Parameter(Mandatory = $false)] [string]$VersionNumber, [Parameter(Mandatory = $true)] [string]$TargetPlatform, [Parameter(Mandatory = $false)] [bool]$DownloadDependencies = $false, [Parameter(Mandatory = $false)] [bool]$DownloadPreRelease = $false ) $processedDependencies = @{} $toProcess = @($ExtensionReference) $extensionMetadataList = [System.Collections.ArrayList]@() while ($toProcess.Count -gt 0) { # Dequeue the next extension reference $currentDependency = $toProcess[0] $toProcess = if ($toProcess.Count -gt 1) { , @($toProcess[1..($toProcess.Count - 1)]) } else { , @() } # Skip if already processed if ($processedDependencies.ContainsKey($currentDependency)) { continue } # Mark as processed $processedDependencies[$currentDependency] = $true # Fetch extension metadata $extensionMetadata = Get-ExtensionMetadata -ExtensionReference $currentDependency ` -VersionNumber $VersionNumber ` -TargetPlatform $TargetPlatform ` -DownloadPreRelease $DownloadPreRelease if ($extensionMetadata) { $extensionMetadataList += $extensionMetadata } if (-not $DownloadDependencies) { break; } # Add dependencies to the processing queue if ($extensionMetadata) { foreach ($dependency in $extensionMetadata.dependencies) { if ($dependency -and (-not $processedDependencies.ContainsKey($dependency))) { $toProcess += $dependency } } } } return $extensionMetadataList } # Function: Import-RemoteVisualStudioPackageToPath # Description: Download a remote VSIX to the local machine. function Import-RemoteVisualStudioPackageToPath { param ( [Parameter(Mandatory = $true)] [string]$VsixUrl, [Parameter(Mandatory = $true)] [string]$LocalFilePath ) Write-Host "Downloading VSIX from URL: $vsixUrl" Invoke-WebRequest -Uri $VsixUrl -OutFile $LocalFilePath -MaximumRedirection 10 Write-Host "Downloaded VSIX to: $localFilePath" # Validate the downloaded file if (-not (Test-Path $LocalFilePath)) { throw "The file was not downloaded. Ensure the URL is correct and accessible: $VsixUrl" } $fileInfo = Get-Item -Path $LocalFilePath $fileSizeBytes = $fileInfo.Length if ($fileSizeBytes -le 0) { throw "The downloaded file is empty or corrupt (size: 0 bytes): $LocalFilePath" } $fileSizeKB = [math]::Round($fileSizeBytes / 1KB, 2) Write-Host "Downloaded file size: $fileSizeKB KB" } # Function: Get-CurrentPlatform # Determine the target platform of the current machine function Get-CurrentPlatform { $processorArch = $null try { $processorArch = (Get-CimInstance -ClassName Win32_Processor).Architecture } catch { Write-Host "Processor architecture could not be determined, assuming x64." } if ($processorArch -eq 12) { $targetPlatform = "win32-arm64" } else { $targetPlatform = "win32-x64" } Write-Host "Current machine target platform: $targetPlatform" return $targetPlatform } # Function: Get-ExtensionMetadata # Description: Fetches extension metadata for a given extension. function Get-ExtensionMetadata { param ( [Parameter(Mandatory = $true)] [string]$ExtensionReference, [Parameter(Mandatory = $false)] [string]$VersionNumber, [Parameter(Mandatory = $true)] [string]$TargetPlatform, [Parameter(Mandatory = $false)] [bool]$DownloadPreRelease = $false ) # Define base API URL (same for both Visual Studio and VS Code) $baseApiUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" # Call helper methods to construct headers and request body $headers = Get-ApiHeaders $flags = Get-ApiFlags $body = Get-RequestBody -ExtensionReference $ExtensionReference -Flags $flags $response = Invoke-MarketplaceApi -ApiUrl $baseApiUrl -Headers $headers -Body $body if ((!($response.results)) -or (!$response.results[0].extensions)) { Write-Host "Skipping presumably built-in extension $ExtensionReference" return $null } if ($response.results.Count -gt 1) { throw "Expected to receive only a single result in the metadata for '$ExtensionReference'." } $extensionVersions = @() try { # Extract versions of the extension from the response $extensionVersions = @($response.results[0].extensions[0].versions) if (-not $extensionVersions) { throw "No versions found for extension '$ExtensionReference'." } } catch { throw "Property 'versions' is missing or inaccessible in the Marketplace API response. Ensure you have provided a valid extension id. $_" } # Filter for the versions matching the current machine's platform (e.g. x64 or ARM64) # Empty or missing target platforms are considered "Universal" versions $extensionVersions = $extensionVersions | Where-Object { # Check if 'targetPlatform' exists dynamically $hasTargetPlatform = $_.PSObject.Properties.Name -contains 'targetPlatform' # If 'targetPlatform' exists, evaluate it; otherwise, treat as "Universal" (-not $hasTargetPlatform) -or ($_.targetPlatform -eq $TargetPlatform) -or [string]::IsNullOrWhiteSpace($_.targetPlatform) } # Filter by version number if provided, else use the latest version $versionInfos = if ($VersionNumber) { $extensionVersions | Where-Object { $_.version -eq $VersionNumber } } else { $extensionVersions } $foundVersionInfo = $null; foreach ($versionInfo in $versionInfos) { # Find the Vsix file URL in the response try { $vsixUrl = ($versionInfo.files | Where-Object { $_.assetType -eq "Microsoft.VisualStudio.Services.VSIXPackage" } | Select-Object -First 1).source } catch { throw "No VSIXPackage was found in the file list for the extension metadata. Verify the extension and version specified are correct. $_" } if ([string]::IsNullOrWhiteSpace($VersionNumber)) { $VersionNumber = "Not specified" } if (-not $vsixUrl) { throw "VSIX download URL not found for extension '$ExtensionReference' version '$VersionNumber'. Please validate this is a VS Code extension." } $isPreReleaseRef = ($versionInfo.properties | Where-Object { $_.key -eq "Microsoft.VisualStudio.Code.PreRelease" } | Select-Object -First 1) $isPreRelease = if ($isPreReleaseRef) { $isPreReleaseRef.value -eq "true" } else { $false } if ($isPreRelease -and (-not $DownloadPreRelease)) { continue; } $vsixDependenciesRef = ($versionInfo.properties | Where-Object { $_.key -eq "Microsoft.VisualStudio.Code.ExtensionDependencies" } | Select-Object -First 1) $vsixDependencies = if ($vsixDependenciesRef) { $vsixDependenciesRef.value -split ',' } else { @() } $vsixExtensionPackRef = ($versionInfo.properties | Where-Object { $_.key -eq "Microsoft.VisualStudio.Code.ExtensionPack" } | Select-Object -First 1) $vsixExtensionPack = if ($vsixExtensionPackRef) { $vsixExtensionPackRef.value -split ',' } else { @() } $allDependencies = ($vsixDependencies + $vsixExtensionPack) | Select-Object -Unique $foundVersionInfo = $versionInfo; break; } if (-not $foundVersionInfo) { $foundVersions = ($extensionVersions | Select-Object -First 10 | ForEach-Object { '({0})' -f $_.version }) -join ", " throw "Extension '$ExtensionReference' version '$VersionNumber' not found for '$TargetPlatform'. Latest 10 versions found: $foundVersions" } # Log the version being used $foundVersion = $foundVersionInfo.version $foundTargetPlatform = if ($foundVersionInfo.PSObject.Properties.Match("targetPlatform").Count -gt 0) { $foundVersionInfo.targetPlatform -join "," } else { "universal" } Write-Host "Found $ExtensionReference version $foundVersion, target platform: $foundTargetPlatform" return @{ name = $ExtensionReference; vsixUrl = $vsixUrl; dependencies = $allDependencies; } } # Function: Import-ExtensionByMetadata # Description: Processes the Marketplace API response returns a local path for the downloaded file. function Import-ExtensionByMetadata { param ( [Parameter(Mandatory = $true)] [object]$ExtensionMetadata, [Parameter(Mandatory = $true)] [string]$DownloadLocation ) $tempFolder = [IO.Path]::GetTempPath() # Rename the file during the copy process to ensure its extension is `.vsix`. # For example, files downloaded from the Visual Studio Marketplace often have a `.VSIXPackage` extension, # which must be renamed to `.vsix` for the VS Code bootstrapper to recognize them correctly. $localFileName = $ExtensionMetadata.name + ".vsix" $localFilePath = Join-Path $tempFolder $localFileName $destinationFile = Join-Path -Path $DownloadLocation -ChildPath $localFileName if (-not (Test-Path $destinationFile)) { RunWithRetries -runBlock { Import-RemoteVisualStudioPackageToPath -VsixUrl $ExtensionMetadata.vsixUrl -LocalFilePath $localFilePath } -retryAttempts 3 -waitBeforeRetrySeconds 5 -exponentialBackoff # Copy to the final location RunWithRetries -runBlock { Write-Host "Copying $localFilePath to $destinationFile" Copy-Item -Path $localFilePath -Destination $destinationFile -Force } -retryAttempts 3 -waitBeforeRetrySeconds 5 -exponentialBackoff } else { Write-Host "VSIX already exists: $destinationFile" } return $destinationFile } if ((Test-Path variable:global:IsUnderTest) -and $global:IsUnderTest) { Export-ModuleMember -Function * } else { Export-ModuleMember -Function Get-VisualStudioExtension Export-ModuleMember -Function Import-RemoteVisualStudioPackageToPath }