diff --git a/README.md b/README.md index 12cc323ee1c97..9456ab44056be 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,20 @@ Although the Windows Package Manager YAML Generator can create YAML files with m ## Test your manifest Now that you have authored your manifest, you should make sure it works as expected. + +### Locally 1) Verify the syntax. You can do that by typing the following command: `winget validate ` 2) Test the install. You can do that by installing the manifest: `winget install -m ` For more details, see [packages](https://docs.microsoft.com/windows/package-manager/package). +### In Windows Sandbox +You can use the [`Tools\SandboxTest.ps1`](Tools/SandboxTest.ps1) script for testing a manifest installation in [Windows Sandbox](https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-sandbox/windows-sandbox-overview). The manifest will be also validated. + +Just provide the path to manifest as parameter: +```powershell +.\Tools\SandboxTest.ps1 +``` + ## Submit your PR With the manifest verified, you will need to submit a PR. Your manifest should be located in the folder path matching `manifests\\\\.yaml` diff --git a/Tools/SandboxTest.ps1 b/Tools/SandboxTest.ps1 new file mode 100644 index 0000000000000..803923f95c296 --- /dev/null +++ b/Tools/SandboxTest.ps1 @@ -0,0 +1,284 @@ +# Parse arguments + +Param( + [Parameter(Position = 0, HelpMessage = "The Manifest to install in the Sandbox.")] + [String] $Manifest, + [Parameter(Position = 1, HelpMessage = "The script to run in the Sandbox.")] + [ScriptBlock] $Script, + [Parameter(HelpMessage = "The folder to map in the Sandbox.")] + [String] $MapFolder = $pwd, + [switch] $SkipManifestValidation +) + +$ErrorActionPreference = "Stop" + +$mapFolder = (Resolve-Path -Path $MapFolder).Path + +if (-Not (Test-Path -Path $mapFolder -PathType Container)) { + Write-Error -Category InvalidArgument -Message 'The provided MapFolder is not a folder.' +} + +# Validate manifest file + +if (-Not $SkipManifestValidation -And -Not [String]::IsNullOrWhiteSpace($Manifest)) { + Write-Host '--> Validating Manifest' + + if (-Not (Test-Path -Path $Manifest)) { + throw 'The Manifest does not exist.' + } + + winget.exe validate $Manifest + if (-Not $?) { + throw 'Manifest validation failed.' + } + + Write-Host +} + +# Check if Windows Sandbox is enabled + +if (-Not (Get-Command 'WindowsSandbox' -ErrorAction SilentlyContinue)) { + Write-Error -Category NotInstalled -Message @' +Windows Sandbox does not seem to be available. Check the following URL for prerequisites and further details: +https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-sandbox/windows-sandbox-overview + +You can run the following command in an elevated PowerShell for enabling Windows Sandbox: +$ Enable-WindowsOptionalFeature -Online -FeatureName 'Containers-DisposableClientVM' +'@ +} + +# Close Windows Sandbox + +$sandbox = Get-Process 'WindowsSandboxClient' -ErrorAction SilentlyContinue +if ($sandbox) { + Write-Host '--> Closing Windows Sandbox' + + $sandbox | Stop-Process + Start-Sleep -Seconds 5 + + Write-Host +} +Remove-Variable sandbox + +# Initialize Temp Folder + +$tempFolderName = 'SandboxTest' +$tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $tempFolderName + +New-Item $tempFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + +# Set dependencies + +$apiLatestUrl = 'https://api.github.com/repos/microsoft/winget-cli/releases/latest' + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$WebClient = New-Object System.Net.WebClient + +function Get-LatestUrl { + ((Invoke-WebRequest $apiLatestUrl -UseBasicParsing | ConvertFrom-Json).assets | Where-Object { $_.name -match '^Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.appxbundle$' }).browser_download_url +} + +function Get-LatestHash { + $shaUrl = ((Invoke-WebRequest $apiLatestUrl -UseBasicParsing | ConvertFrom-Json).assets | Where-Object { $_.name -match '^Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.txt$' }).browser_download_url + + $shaFile = Join-Path -Path $tempFolder -ChildPath 'Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.txt' + $WebClient.DownloadFile($shaUrl, $shaFile) + + Get-Content $shaFile +} + +# Hide the progress bar of Invoke-WebRequest +$oldProgressPreference = $ProgressPreference +$ProgressPreference = 'SilentlyContinue' + +$desktopAppInstaller = @{ + fileName = 'Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.appxbundle' + url = $(Get-LatestUrl) + hash = $(Get-LatestHash) +} + +$ProgressPreference = $oldProgressPreference + +$vcLibsUwp = @{ + fileName = 'Microsoft.VCLibs.x64.14.00.Desktop.appx' + url = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' + hash = '6602159c341bafea747d0edf15669ac72df8817299fbfaa90469909e06794256' +} + +$dependencies = @($desktopAppInstaller, $vcLibsUwp) + +# Clean temp directory + +Get-ChildItem $tempFolder -Recurse -Exclude $dependencies.fileName | Remove-Item -Force -Recurse + +if (-Not [String]::IsNullOrWhiteSpace($Manifest)) { + Copy-Item -Path $Manifest -Recurse -Destination $tempFolder +} + +# Download dependencies + +Write-Host '--> Checking dependencies' + +$desktopInSandbox = 'C:\Users\WDAGUtilityAccount\Desktop' + +foreach ($dependency in $dependencies) { + $dependency.file = Join-Path -Path $tempFolder -ChildPath $dependency.fileName + $dependency.pathInSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $dependency.fileName) + + # Only download if the file does not exist, or its hash does not match. + if (-Not ((Test-Path -Path $dependency.file -PathType Leaf) -And $dependency.hash -eq $(get-filehash $dependency.file).Hash)) { + Write-Host @" + - Downloading: + $($dependency.url) +"@ + + try { + $WebClient.DownloadFile($dependency.url, $dependency.file) + } + catch { + throw "Error downloading $($dependency.url)." + } + if (-not ($dependency.hash -eq $(get-filehash $dependency.file).Hash)) { + throw 'Hashes do not match, try gain.' + } + } +} + +Write-Host + +# Create Bootstrap script + +# See: https://stackoverflow.com/a/22670892/12156188 +$bootstrapPs1Content = @' +function Update-EnvironmentVariables { + foreach($level in "Machine","User") { + [Environment]::GetEnvironmentVariables($level).GetEnumerator() | % { + # For Path variables, append the new values, if they're not already in there + if($_.Name -match 'Path$') { + $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -split ';' | Select -unique) -join ';' + } + $_ + } | Set-Content -Path { "Env:$($_.Name)" } + } +} + + +'@ + +$bootstrapPs1Content += @" +Write-Host @' +--> Installing WinGet + +'@ +Add-AppxPackage -Path '$($desktopAppInstaller.pathInSandbox)' -DependencyPath '$($vcLibsUwp.pathInSandbox)' + +Write-Host @' + +Tip: you can type 'Update-EnvironmentVariables' to update your environment variables, such as after installing a new software. + +'@ + + +"@ + +if (-Not [String]::IsNullOrWhiteSpace($Manifest)) { + $manifestFileName = Split-Path $Manifest -Leaf + $manifestPathInSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $manifestFileName) + + $bootstrapPs1Content += @" +Write-Host @' + +--> Installing the Manifest $manifestFileName + +'@ +winget install -m '$manifestPathInSandbox' + +Write-Host @' + +--> Refreshing environment variables +'@ +Update-EnvironmentVariables + + +"@ +} + +if (-Not [String]::IsNullOrWhiteSpace($Script)) { + $bootstrapPs1Content += @" +Write-Host @' + +--> Running the following script: + +{ +$Script +} + +'@ + +$Script + + +"@ +} + +$bootstrapPs1Content += @" +Write-Host +"@ + +$bootstrapPs1FileName = 'Bootstrap.ps1' +$bootstrapPs1Content | Out-File (Join-Path -Path $tempFolder -ChildPath $bootstrapPs1FileName) + +# Create Wsb file + +$bootstrapPs1InSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Join-Path -Path $tempFolderName -ChildPath $bootstrapPs1FileName) +$mapFolderInSandbox = Join-Path -Path $desktopInSandbox -ChildPath (Split-Path -Path $mapFolder -Leaf) + +$sandboxTestWsbContent = @" + + + + $tempFolder + true + + + $mapFolder + + + + PowerShell Start-Process PowerShell -WindowStyle Maximized -WorkingDirectory '$mapFolderInSandbox' -ArgumentList '-ExecutionPolicy Bypass -NoExit -NoLogo -File $bootstrapPs1InSandbox' + + +"@ + +$sandboxTestWsbFileName = 'SandboxTest.wsb' +$sandboxTestWsbFile = Join-Path -Path $tempFolder -ChildPath $sandboxTestWsbFileName +$sandboxTestWsbContent | Out-File $sandboxTestWsbFile + +Write-Host @" +--> Starting Windows Sandbox, and: + - Mounting the following directories: + - $tempFolder as read-only + - $mapFolder as read-and-write + - Installing WinGet +"@ + +if (-Not [String]::IsNullOrWhiteSpace($Manifest)) { + Write-Host @" + - Installing the Manifest $manifestFileName + - Refreshing environment variables +"@ +} + +if (-Not [String]::IsNullOrWhiteSpace($Script)) { + Write-Host @" + - Running the following script: + +{ +$Script +} +"@ +} + +Write-Host + +WindowsSandbox $SandboxTestWsbFile