diff --git a/Installer Scripts/Install VC Redist.ps1 b/Installer Scripts/Install VC Redist.ps1 deleted file mode 100644 index 9739994..0000000 --- a/Installer Scripts/Install VC Redist.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# URLs for the latest Visual C++ Redistributables -$urls = @( - "https://aka.ms/vs/17/release/vc_redist.x86.exe", - "https://aka.ms/vs/17/release/vc_redist.x64.exe" -) -if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { - $urls += "https://aka.ms/vs/17/release/vc_redist.arm64.exe" -} - -# Directory to save the downloads -$downloadPath = "$env:TEMP" - -# To improve download performance, the progress bar is suppressed. [2, 6] -$ProgressPreference = 'SilentlyContinue' - -foreach ($url in $urls) { - $fileName = $url.Split('/')[-1] - $filePath = Join-Path $downloadPath $fileName - - Write-Host "Downloading $fileName..." - # Download the file without a progress bar [1, 4] - Invoke-WebRequest -Uri $url -OutFile $filePath - - if (Test-Path $filePath) { - Write-Host "Installing $fileName..." - # Silently install the redistributable and wait for it to complete [3, 5, 9] - Start-Process -FilePath $filePath -ArgumentList "/install /quiet /norestart" -Wait - Write-Host "$fileName has been installed." - # Optional: Remove the installer after installation - # Remove-Item -Path $filePath - } else { - Write-Host "Error: Failed to download $fileName." - } -} - -# Restore the default progress preference -$ProgressPreference = 'Continue' - -Write-Host "Script execution finished." diff --git a/Installer Scripts/Install-Microsoft-Store.ps1 b/Installer Scripts/Install-Microsoft-Store.ps1 index 2624dc9..8909730 100644 --- a/Installer Scripts/Install-Microsoft-Store.ps1 +++ b/Installer Scripts/Install-Microsoft-Store.ps1 @@ -4,17 +4,40 @@ # Author: ThioJoe # Repo Url: https://github.com/ThioJoe/Windows-Sandbox-Tools -# Last Updated: August 10, 2025 +# Last Updated: February 12, 2026 param( # Optional switch to output the generated XML files to the working directory [switch]$debugSaveFiles, + # Optional switch to skip the installation of Microsoft Store, but still download the files [switch]$noInstall, + # Optional switch to skip the download and install, but still show the packages found - [switch]$noDownload + [switch]$noDownload, + + # Optional path to a local directory containing the installation files. If provided, the download steps will be skipped. + # - Just copy the entire "MSStore Install" folder with the files the script normally downloads, and put it in your mounted folder to avoid having to re-download it. + # - You can find the "MSStore Install" folder in the "Downloads" folder. + # - Make sure to use the mounted path from the perspective of within the sandbox. + [string]$ExistingInstallerFilesPath ) +# --- Parameter Usage Examples --- +# Standard run (Download & Install): +# .\Install-Microsoft-Store.ps1 +# +# Install from existing files instead of downloading: +# .\Install-Microsoft-Store.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\MSStore Install" +# +# Download only (Don't Install): +# .\Install-Microsoft-Store.ps1 -noInstall +# +# Debug mode (Save SOAP XML logs to disk): +# .\Install-Microsoft-Store.ps1 -debugSaveFiles + +# ======================================================= + # --- Configuration --- # Note: These defaults should work for the regular current build of Microsoft Store, but I haven't tested using any of the other values. So fetching insider builds of MS Store (if any) might not work. $flightRing = "Retail" # Apparently accepts 'Retail', 'Internal', and 'External' @@ -43,21 +66,26 @@ $subfolderName = "MSStore Install" # Category ID for the Microsoft Store app package $storeCategoryId = "64293252-5926-453c-9494-2d4021f1c78d" -# Combine them to create the full working directory path -$workingDir = Join-Path -Path $userDownloadsFolder -ChildPath $subfolderName -$LogDirectory = Join-Path -Path $workingDir -ChildPath "Logs" - -# Create the directory if it doesn't exist -if (-not (Test-Path -Path $workingDir)) { - New-Item -Path $workingDir -ItemType Directory -Force | Out-Null +if ($ExistingInstallerFilesPath) { + if (Test-Path -Path $ExistingInstallerFilesPath) { + $workingDir = $ExistingInstallerFilesPath + Write-Host "Using local source path: $workingDir" -ForegroundColor Yellow + } else { + Write-Error "The specified local source path does not exist: $ExistingInstallerFilesPath" + return + } +} else { + # Combine them to create the full working directory path + $workingDir = Join-Path -Path $userDownloadsFolder -ChildPath $subfolderName + + # Create the directory if it doesn't exist + if (-not (Test-Path -Path $workingDir)) { + New-Item -Path $workingDir -ItemType Directory -Force | Out-Null + } } If ($debugSaveFiles) { - # Create a subdirectory for logs if it doesn't exist - if (-not (Test-Path -Path $LogDirectory)) { - New-Item -Path $LogDirectory -ItemType Directory -Force | Out-Null - } - Write-Host "All files (logs, downloads) will be saved to: '$LogDirectory'" -ForegroundColor Yellow + Write-Host "All files (logs, downloads) will be saved to: '$workingDir'" -ForegroundColor Yellow } # --- XML Templates --- @@ -185,204 +213,206 @@ $headers = @{ "Content-Type" = "application/soap+xml; charset=utf-8" } $baseUri = "https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx" try { - # Step 1: Get Cookie - Write-Host "Step 1: Getting authentication cookie..." - $cookieRequestPayload = $cookieXmlTemplate - If ($debugSaveFiles) { $cookieRequestPayload | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Request.xml") } - - $cookieResponse = Invoke-WebRequest -Uri $baseUri -Method Post -Body $cookieRequestPayload -Headers $headers -UseBasicParsing - If ($debugSaveFiles) { $cookieResponse.Content | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Response.xml"); Write-Host " -> Saved request and response logs for Step 1." } - - $cookieResponseXml = [xml]$cookieResponse.Content - $encryptedCookieData = $cookieResponseXml.Envelope.Body.GetCookieResponse.GetCookieResult.EncryptedData - Write-Host "Success. Cookie received." -ForegroundColor Green - - # Step 2: Get File List - Write-Host "Step 2: Getting file list..." - $fileListRequestPayload = $fileListXmlTemplate -f $encryptedCookieData, $storeCategoryId, $currentBranch, $flightRing, $flightingBranchName - If ($debugSaveFiles) { [System.IO.File]::WriteAllText((Join-Path $LogDirectory "02_Step2_Request_AUTOMATED.xml"), $fileListRequestPayload, [System.Text.UTF8Encoding]::new($false)) } - - $fileListResponse = Invoke-WebRequest -Uri $baseUri -Method Post -Body $fileListRequestPayload -Headers $headers -UseBasicParsing - If ($debugSaveFiles) { $fileListResponse.Content | Set-Content -Path (Join-Path $LogDirectory "02_Step2_Response_SUCCESS.xml") } - - # The response contains XML fragments that are HTML-encoded. We must decode this before treating it as XML. - Add-Type -AssemblyName System.Web - $decodedContent = [System.Web.HttpUtility]::HtmlDecode($fileListResponse.Content) - $fileListResponseXml = [xml]$decodedContent - Write-Host "Successfully received and DECODED Step 2 response." -ForegroundColor Green - - $fileIdentityMap = @{} - - # Get the two main lists of updates from the now correctly-decoded response - $newUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.NewUpdates.UpdateInfo - $allExtendedUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.ExtendedUpdateInfo.Updates.Update - - Write-Host "--- Correlating Update Information ---" -ForegroundColor Magenta - - # Filter the 'NewUpdates' list to only include items that are actual downloadable files. - # These are identified by the presence of the tag inside their inner XML. - $downloadableUpdates = $newUpdates | Where-Object { $_.Xml.Properties.SecuredFragment } - - Write-Host "Found $($downloadableUpdates.Count) potentially downloadable packages." -ForegroundColor Cyan - - # Now, process each downloadable update - foreach ($update in $downloadableUpdates) { - $lookupId = $update.ID + if (-not $ExistingInstallerFilesPath) { + # Step 1: Get Cookie + Write-Host "Step 1: Getting authentication cookie..." + $cookieRequestPayload = $cookieXmlTemplate + If ($debugSaveFiles) { $cookieRequestPayload | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Request.xml") } - # Find the matching entry in the 'ExtendedUpdateInfo' list using the same numeric ID. - $extendedInfo = $allExtendedUpdates | Where-Object { $_.ID -eq $lookupId } | Select-Object -First 1 + $cookieResponse = Invoke-WebRequest -Uri $baseUri -Method Post -Body $cookieRequestPayload -Headers $headers -UseBasicParsing + If ($debugSaveFiles) { $cookieResponse.Content | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Response.xml"); Write-Host " -> Saved request and response logs for Step 1." } + + $cookieResponseXml = [xml]$cookieResponse.Content + $encryptedCookieData = $cookieResponseXml.Envelope.Body.GetCookieResponse.GetCookieResult.EncryptedData + Write-Host "Success. Cookie received." -ForegroundColor Green + + # Step 2: Get File List + Write-Host "Step 2: Getting file list..." + $fileListRequestPayload = $fileListXmlTemplate -f $encryptedCookieData, $storeCategoryId, $currentBranch, $flightRing, $flightingBranchName + If ($debugSaveFiles) { [System.IO.File]::WriteAllText((Join-Path $LogDirectory "02_Step2_Request_AUTOMATED.xml"), $fileListRequestPayload, [System.Text.UTF8Encoding]::new($false)) } + + $fileListResponse = Invoke-WebRequest -Uri $baseUri -Method Post -Body $fileListRequestPayload -Headers $headers -UseBasicParsing + If ($debugSaveFiles) { $fileListResponse.Content | Set-Content -Path (Join-Path $LogDirectory "02_Step2_Response_SUCCESS.xml") } + + # The response contains XML fragments that are HTML-encoded. We must decode this before treating it as XML. + Add-Type -AssemblyName System.Web + $decodedContent = [System.Web.HttpUtility]::HtmlDecode($fileListResponse.Content) + $fileListResponseXml = [xml]$decodedContent + Write-Host "Successfully received and DECODED Step 2 response." -ForegroundColor Green - if (-not $extendedInfo) { - Write-Warning "Could not find matching ExtendedInfo for downloadable update ID $lookupId. Skipping." - continue - } + $fileIdentityMap = @{} - # From the extended info, get the actual package file and ignore the metadata .cab files. - $fileNode = $extendedInfo.Xml.Files.File | Where-Object { $_.FileName -and $_.FileName -notlike "Abm_*" } | Select-Object -First 1 + # Get the two main lists of updates from the now correctly-decoded response + $newUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.NewUpdates.UpdateInfo + $allExtendedUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.ExtendedUpdateInfo.Updates.Update - if (-not $fileNode) { - Write-Warning "Found matching ExtendedInfo for ID $lookupId, but it contains no valid file node. Skipping." - continue - } + Write-Host "--- Correlating Update Information ---" -ForegroundColor Magenta - # Additional parsing - $fileName = $fileNode.FileName - $updateGuid = $update.Xml.UpdateIdentity.UpdateID - $revNum = $update.Xml.UpdateIdentity.RevisionNumber - $fullIdentifier = $fileNode.GetAttribute("InstallerSpecificIdentifier") + # Filter the 'NewUpdates' list to only include items that are actual downloadable files. + # These are identified by the presence of the tag inside their inner XML. + $downloadableUpdates = $newUpdates | Where-Object { $_.Xml.Properties.SecuredFragment } - # Define the regex based on the official package identity structure. - # ____ - $regex = "^(?.+?)_(?\d+\.\d+\.\d+\.\d+)_(?[a-zA-Z0-9]+)_(?.*?)_(?[a-hjkmnp-tv-z0-9]{13})$" - - $packageInfo = [PSCustomObject]@{ - FullName = $fullIdentifier - FileName = $fileName - UpdateID = $updateGuid - RevisionNumber = $revNum - } + Write-Host "Found $($downloadableUpdates.Count) potentially downloadable packages." -ForegroundColor Cyan - if ($fullIdentifier -match $regex) { - # If the regex matches, populate the object with the named capture groups - $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value $matches.Name - $packageInfo | Add-Member -MemberType NoteProperty -Name "Version" -Value $matches.Version - $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value $matches.Architecture - $packageInfo | Add-Member -MemberType NoteProperty -Name "ResourceId" -Value $matches.ResourceId - $packageInfo | Add-Member -MemberType NoteProperty -Name "PublisherId" -Value $matches.PublisherId - } else { - # Fallback for any identifiers that don't match the pattern - $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value "Unknown (Parsing Failed)" - $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value "unknown" - } - - # Use the full, unique identifier as the key in the map - $fileIdentityMap[$fullIdentifier] = $packageInfo - - Write-Host " -> CORRELATED: '$($packageInfo.PackageName)' ($($packageInfo.Architecture))" -ForegroundColor Green - } - - Write-Host "--- Correlation Complete ---" -ForegroundColor Magenta - Write-Host "Found and prepared $($fileIdentityMap.Count) downloadable files." -ForegroundColor Green - - - # --- Step 3: Filter, Get URLs, and Download --- - try { - # Get the current system's processor architecture and map it to the script's naming convention - $systemArch = switch ($env:PROCESSOR_ARCHITECTURE) { - "AMD64" { "x64" } - "ARM64" { "arm64" } - "x86" { "x86" } - default { "unknown" } - } - - if ($systemArch -eq "unknown") { - throw "Could not determine system architecture from '$($env:PROCESSOR_ARCHITECTURE)'." - } - Write-Host "Step 3: Filtering packages for your system architecture ('$systemArch')..." -ForegroundColor Magenta - - # --- Filter the packages --- - - # 1. Isolate the Microsoft.WindowsStore packages and find the latest version - $latestStorePackage = $fileIdentityMap.Values | - Where-Object { $_.PackageName -eq 'Microsoft.WindowsStore' } | - Sort-Object { [version]$_.Version } -Descending | - Select-Object -First 1 - - # 2. Get all other dependencies that match the system architecture (or are neutral) - $filteredDependencies = $fileIdentityMap.Values | - Where-Object { - ($_.PackageName -ne 'Microsoft.WindowsStore') -and - ( ($_.Architecture -eq $systemArch) -or ($_.Architecture -eq 'neutral') ) - } - - # 3. Combine the lists for the final download queue - $packagesToDownload = @() - if ($latestStorePackage) { - $packagesToDownload += $latestStorePackage - Write-Host " -> Found latest Store package: $($latestStorePackage.FullName)" -ForegroundColor Green - } else { - Write-Warning "Could not find any Microsoft.WindowsStore package." - } - - $packagesToDownload += $filteredDependencies - Write-Host " -> Found $($filteredDependencies.Count) dependencies for '$systemArch' architecture." -ForegroundColor Green - Write-Host "Total files to download: $($packagesToDownload.Count)" -ForegroundColor Cyan - Write-Host "------------------------------------------------------------" - - - # --- Loop through the filtered list, get URLs, and download --- - Write-Host "Step 4: Fetching URLs and downloading files..." -ForegroundColor Magenta - - $originalPref = $ProgressPreference - $ProgressPreference = 'SilentlyContinue' - - foreach ($package in $packagesToDownload) { - Write-Host "Processing: $($package.FullName)" - - # Get the download URL for this specific package - $fileUrlRequestPayload = $fileUrlXmlTemplate -f $encryptedCookieData, $package.UpdateID, $package.RevisionNumber, $currentBranch, $flightRing, $flightingBranchName - $fileUrlResponse = Invoke-WebRequest -Uri "$baseUri/secured" -Method Post -Body $fileUrlRequestPayload -Headers $headers -UseBasicParsing - $fileUrlResponseXml = [xml]$fileUrlResponse.Content - - $fileLocations = $fileUrlResponseXml.Envelope.Body.GetExtendedUpdateInfo2Response.GetExtendedUpdateInfo2Result.FileLocations.FileLocation - $baseFileName = [System.IO.Path]::GetFileNameWithoutExtension($package.FileName) - $downloadUrl = ($fileLocations | Where-Object { $_.Url -like "*$baseFileName*" }).Url - - if (-not $downloadUrl) { - Write-Warning " -> Could not retrieve download URL for $($package.FileName). Skipping." - continue - } - if ($noDownload) { - Write-Host " -> Skipping download for $($package.FullName) because of -noDownload switch." -ForegroundColor Yellow - continue - } - - # Download the file - # Construct a more descriptive filename using the package's full name and its original extension - $fileExtension = [System.IO.Path]::GetExtension($package.FileName) - $newFileName = "$($package.FullName)$($fileExtension)" - $filePath = Join-Path $workingDir $newFileName + # Now, process each downloadable update + foreach ($update in $downloadableUpdates) { + $lookupId = $update.ID - Write-Host " -> Downloading from: $downloadUrl" -ForegroundColor Gray - Write-Host " -> Saving to: $filePath" - - try { - Invoke-WebRequest -Uri $downloadUrl -OutFile $filePath -UseBasicParsing - Write-Host " -> SUCCESS: Download complete." -ForegroundColor Green - } catch { - Write-Error " -> FAILED to download $($newFileName). Error: $($_.Exception.Message)" + # Find the matching entry in the 'ExtendedUpdateInfo' list using the same numeric ID. + $extendedInfo = $allExtendedUpdates | Where-Object { $_.ID -eq $lookupId } | Select-Object -First 1 + + if (-not $extendedInfo) { + Write-Warning "Could not find matching ExtendedInfo for downloadable update ID $lookupId. Skipping." + continue } - Write-Host "" + + # From the extended info, get the actual package file and ignore the metadata .cab files. + $fileNode = $extendedInfo.Xml.Files.File | Where-Object { $_.FileName -and $_.FileName -notlike "Abm_*" } | Select-Object -First 1 + + if (-not $fileNode) { + Write-Warning "Found matching ExtendedInfo for ID $lookupId, but it contains no valid file node. Skipping." + continue + } + + # Additional parsing + $fileName = $fileNode.FileName + $updateGuid = $update.Xml.UpdateIdentity.UpdateID + $revNum = $update.Xml.UpdateIdentity.RevisionNumber + $fullIdentifier = $fileNode.GetAttribute("InstallerSpecificIdentifier") + + # Define the regex based on the official package identity structure. + # ____ + $regex = "^(?.+?)_(?\d+\.\d+\.\d+\.\d+)_(?[a-zA-Z0-9]+)_(?.*?)_(?[a-hjkmnp-tv-z0-9]{13})$" + + $packageInfo = [PSCustomObject]@{ + FullName = $fullIdentifier + FileName = $fileName + UpdateID = $updateGuid + RevisionNumber = $revNum + } + + if ($fullIdentifier -match $regex) { + # If the regex matches, populate the object with the named capture groups + $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value $matches.Name + $packageInfo | Add-Member -MemberType NoteProperty -Name "Version" -Value $matches.Version + $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value $matches.Architecture + $packageInfo | Add-Member -MemberType NoteProperty -Name "ResourceId" -Value $matches.ResourceId + $packageInfo | Add-Member -MemberType NoteProperty -Name "PublisherId" -Value $matches.PublisherId + } else { + # Fallback for any identifiers that don't match the pattern + $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value "Unknown (Parsing Failed)" + $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value "unknown" + } + + # Use the full, unique identifier as the key in the map + $fileIdentityMap[$fullIdentifier] = $packageInfo + + Write-Host " -> CORRELATED: '$($packageInfo.PackageName)' ($($packageInfo.Architecture))" -ForegroundColor Green } - - $ProgressPreference = $originalPref - Write-Host "------------------------------------------------------------" - Write-Host "Finished downloading packages to: $workingDir" -ForegroundColor Green + Write-Host "--- Correlation Complete ---" -ForegroundColor Magenta + Write-Host "Found and prepared $($fileIdentityMap.Count) downloadable files." -ForegroundColor Green - } catch { - Write-Host "An error occurred during the filtering or downloading phase:" -ForegroundColor Red - Write-Host $_.Exception.ToString() + + # --- Step 3: Filter, Get URLs, and Download --- + try { + # Get the current system's processor architecture and map it to the script's naming convention + $systemArch = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x64" } + "ARM64" { "arm64" } + "x86" { "x86" } + default { "unknown" } + } + + if ($systemArch -eq "unknown") { + throw "Could not determine system architecture from '$($env:PROCESSOR_ARCHITECTURE)'." + } + Write-Host "Step 3: Filtering packages for your system architecture ('$systemArch')..." -ForegroundColor Magenta + + # --- Filter the packages --- + + # 1. Isolate the Microsoft.WindowsStore packages and find the latest version + $latestStorePackage = $fileIdentityMap.Values | + Where-Object { $_.PackageName -eq 'Microsoft.WindowsStore' } | + Sort-Object { [version]$_.Version } -Descending | + Select-Object -First 1 + + # 2. Get all other dependencies that match the system architecture (or are neutral) + $filteredDependencies = $fileIdentityMap.Values | + Where-Object { + ($_.PackageName -ne 'Microsoft.WindowsStore') -and + ( ($_.Architecture -eq $systemArch) -or ($_.Architecture -eq 'neutral') ) + } + + # 3. Combine the lists for the final download queue + $packagesToDownload = @() + if ($latestStorePackage) { + $packagesToDownload += $latestStorePackage + Write-Host " -> Found latest Store package: $($latestStorePackage.FullName)" -ForegroundColor Green + } else { + Write-Warning "Could not find any Microsoft.WindowsStore package." + } + + $packagesToDownload += $filteredDependencies + Write-Host " -> Found $($filteredDependencies.Count) dependencies for '$systemArch' architecture." -ForegroundColor Green + Write-Host "Total files to download: $($packagesToDownload.Count)" -ForegroundColor Cyan + Write-Host "------------------------------------------------------------" + + + # --- Loop through the filtered list, get URLs, and download --- + Write-Host "Step 4: Fetching URLs and downloading files..." -ForegroundColor Magenta + + $originalPref = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + foreach ($package in $packagesToDownload) { + Write-Host "Processing: $($package.FullName)" + + # Get the download URL for this specific package + $fileUrlRequestPayload = $fileUrlXmlTemplate -f $encryptedCookieData, $package.UpdateID, $package.RevisionNumber, $currentBranch, $flightRing, $flightingBranchName + $fileUrlResponse = Invoke-WebRequest -Uri "$baseUri/secured" -Method Post -Body $fileUrlRequestPayload -Headers $headers -UseBasicParsing + $fileUrlResponseXml = [xml]$fileUrlResponse.Content + + $fileLocations = $fileUrlResponseXml.Envelope.Body.GetExtendedUpdateInfo2Response.GetExtendedUpdateInfo2Result.FileLocations.FileLocation + $baseFileName = [System.IO.Path]::GetFileNameWithoutExtension($package.FileName) + $downloadUrl = ($fileLocations | Where-Object { $_.Url -like "*$baseFileName*" }).Url + + if (-not $downloadUrl) { + Write-Warning " -> Could not retrieve download URL for $($package.FileName). Skipping." + continue + } + if ($noDownload) { + Write-Host " -> Skipping download for $($package.FullName) because of -noDownload switch." -ForegroundColor Yellow + continue + } + + # Download the file + # Construct a more descriptive filename using the package's full name and its original extension + $fileExtension = [System.IO.Path]::GetExtension($package.FileName) + $newFileName = "$($package.FullName)$($fileExtension)" + $filePath = Join-Path $workingDir $newFileName + + Write-Host " -> Downloading from: $downloadUrl" -ForegroundColor Gray + Write-Host " -> Saving to: $filePath" + + try { + Invoke-WebRequest -Uri $downloadUrl -OutFile $filePath -UseBasicParsing + Write-Host " -> SUCCESS: Download complete." -ForegroundColor Green + } catch { + Write-Error " -> FAILED to download $($newFileName). Error: $($_.Exception.Message)" + } + Write-Host "" + } + + $ProgressPreference = $originalPref + + Write-Host "------------------------------------------------------------" + Write-Host "Finished downloading packages to: $workingDir" -ForegroundColor Green + + } catch { + Write-Host "An error occurred during the filtering or downloading phase:" -ForegroundColor Red + Write-Host $_.Exception.ToString() + } } If ($noDownload) { diff --git a/Installer Scripts/Install-VC-Redist.ps1 b/Installer Scripts/Install-VC-Redist.ps1 new file mode 100644 index 0000000..3b0b860 --- /dev/null +++ b/Installer Scripts/Install-VC-Redist.ps1 @@ -0,0 +1,186 @@ +# Downloads the latest VC Redistributables from Microsoft + +param( + # Optional path to a local directory containing the installation files. If provided, the download steps will be skipped. + # - Just copy the entire "VCRedist Install" folder with the files the script normally downloads, and put it in your mounted folder to avoid having to re-download it. + # - Make sure to use the mounted path from the perspective of within the sandbox. + [string]$ExistingInstallerFilesPath, + + # If set, forces using the installer files from ExistingInstallerFilesPath even if a new version exists. Still warns about there being a new version. + [switch]$ForceCachedFilesOnly, + + # If set, implies ForceCachedFilesOnly and uses cached installer files only, but does not even bother checking for a latest version. + [switch]$NoCheckLatestVersion +) + +# --- Parameter Usage Examples --- +# Standard run (Download & Install): +# .\Install-VC-Redist.ps1 +# +# Install from existing files instead of downloading: +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" +# +# Use cached files only, even if outdated (still warns about new versions): +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" -ForceCachedFilesOnly +# +# Use cached files only, skip all version checks: +# .\Install-VC-Redist.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\VCRedist Install" -NoCheckLatestVersion + +# ======================================================= + +# Validate parameter combinations +if ($NoCheckLatestVersion) { + $ForceCachedFilesOnly = [switch]::new($true) +} +if ($ForceCachedFilesOnly -and [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + Write-Host "Error: -ForceCachedFilesOnly requires -ExistingInstallerFilesPath to be specified." -ForegroundColor Red + Read-Host "Press Enter to exit" + exit 1 +} + +# URLs for the latest Visual C++ Redistributables +$urls = @( + "https://aka.ms/vs/17/release/vc_redist.x86.exe", + "https://aka.ms/vs/17/release/vc_redist.x64.exe" +) +if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { + $urls += "https://aka.ms/vs/17/release/vc_redist.arm64.exe" +} + +# Directory to save the downloads. This will save it into the user "Downloads" folder. +$folderName = "VCRedist Install" +$userDownloadsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path +$downloadPath = Join-Path -Path $userDownloadsFolder -ChildPath $folderName + +# To improve download performance, the progress bar is suppressed. +$ProgressPreference = 'SilentlyContinue' + +# Load stored version hashes from the pre-existing installer cache (read-only mount) +$storedHashes = @{} +if (-not [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + $hashFilePath = Join-Path $ExistingInstallerFilesPath "versions.txt" + if (Test-Path $hashFilePath) { + Get-Content $hashFilePath | ForEach-Object { + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { $storedHashes[$parts[0]] = $parts[1] } + } + } +} + +# Track current remote hashes to save at the end +$currentHashes = @{} +$pauseAtEnd = $false + +foreach ($url in $urls) { + $fileName = $url.Split('/')[-1] + $downloadFilePath = Join-Path $downloadPath $fileName + $installerToRun = $null + $updateDetected = $false + $remoteHash = $null + + Write-Host "`nChecking $fileName..." + + if (-not $NoCheckLatestVersion) { + try { + # Get the final redirected URL to extract the version hash + $headResponse = Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing -ErrorAction Stop + if ($headResponse.BaseResponse.ResponseUri) { + # PowerShell 5.1 + $finalUrl = $headResponse.BaseResponse.ResponseUri.AbsoluteUri + } else { + # PowerShell 7+ + $finalUrl = $headResponse.BaseResponse.RequestMessage.RequestUri.AbsoluteUri + } + $remoteHash = $finalUrl.Split('/')[-2] + $currentHashes[$fileName] = $remoteHash + Write-Host "Remote hash: $remoteHash" + } + catch { + Write-Host "Warning: Could not retrieve remote info for $fileName." -ForegroundColor Yellow + Write-Host "Error Info: $_" -ForegroundColor Yellow + } + } + + # Check if a pre-existing cached installer can be used + if (-not [string]::IsNullOrWhiteSpace($ExistingInstallerFilesPath)) { + $cachedFilePath = Join-Path $ExistingInstallerFilesPath $fileName + if (Test-Path $cachedFilePath) { + if ($null -ne $remoteHash -and $storedHashes.ContainsKey($fileName) -and $storedHashes[$fileName] -eq $remoteHash) { + Write-Host "Cached version is up to date. Using $cachedFilePath" + $installerToRun = $cachedFilePath + } elseif ($ForceCachedFilesOnly) { + if ($null -ne $remoteHash) { + Write-Host "WARNING: Cached installer is out of date, but using it anyway (-ForceCachedFilesOnly)." -ForegroundColor Yellow + Write-Host "Please update your cache at: $ExistingInstallerFilesPath" -ForegroundColor Yellow + $pauseAtEnd = $true + } else { + Write-Host "Using cached file (version check skipped). Using $cachedFilePath" + } + $installerToRun = $cachedFilePath + } else { + Write-Host "Newer version detected. Cached installer is out of date." -ForegroundColor Yellow + $updateDetected = $true + $pauseAtEnd = $true + } + } elseif ($ForceCachedFilesOnly) { + Write-Host "Error: Cached file not found at $cachedFilePath and -ForceCachedFilesOnly is set." -ForegroundColor Red + continue + } + } + + # Download if no valid cache was found or an update is needed + if ($null -eq $installerToRun) { + if ($ForceCachedFilesOnly) { + Write-Host "Error: No cached installer available for $fileName and -ForceCachedFilesOnly is set. Skipping." -ForegroundColor Red + continue + } + + Write-Host "Downloading $fileName..." + + # Create the directory if it doesn't exist + if (-not (Test-Path -Path $downloadPath)) { + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + } + + Invoke-WebRequest -Uri $url -OutFile $downloadFilePath -UseBasicParsing + $installerToRun = $downloadFilePath + + if ($updateDetected) { + Write-Host "ACTION REQUIRED: A new version of $fileName was downloaded." -ForegroundColor Yellow + Write-Host "Please update your cache at: $ExistingInstallerFilesPath" -ForegroundColor Yellow + Write-Host "Then update versions.txt with: $fileName=$remoteHash" -ForegroundColor Yellow + $pauseAtEnd = $true + } + } + + if (Test-Path $installerToRun) { + Write-Host "Installing $fileName from $installerToRun..." + # Silently install the redistributable and wait for it to complete + Start-Process -FilePath $installerToRun -ArgumentList "/install /quiet /norestart" -Wait + Write-Host "$fileName has been installed." + + # Optional: Remove the downloaded installer if it was downloaded to TEMP + if ($installerToRun -eq $downloadFilePath) { + # Remove-Item -Path $downloadFilePath + } + } else { + Write-Host "Error: Failed to locate installer for $fileName." + } +} + +# Save versions.txt to the download folder so it can be copied alongside the installers +if ($currentHashes.Count -gt 0) { + if (-not (Test-Path -Path $downloadPath)) { + New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null + } + $versionsFilePath = Join-Path $downloadPath "versions.txt" + $currentHashes.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" } | Set-Content $versionsFilePath +} + +# Restore the default progress preference +$ProgressPreference = 'Continue' + +Write-Host "`nScript execution finished." +if ($pauseAtEnd) { + Read-Host "`nPress Enter to exit" +} diff --git a/Installer Scripts/Install-Winget.ps1 b/Installer Scripts/Install-Winget.ps1 index 8124c02..89090ea 100644 --- a/Installer Scripts/Install-Winget.ps1 +++ b/Installer Scripts/Install-Winget.ps1 @@ -3,11 +3,24 @@ # Author: ThioJoe # Repo Url: https://github.com/ThioJoe/Windows-Sandbox-Tools -# Last Updated: August 4, 2025 +# Last Updated: February 12, 2026 param( - [switch]$removeMsStoreAsSource = $false # If switch is included, it will remove the 'msstore' source after installing winget, which doesn't work with Sandbox, unless the Microsoft Store is also installed - ) + # If switch is included, it will remove the 'msstore' source after installing winget, which doesn't work with Sandbox, unless the Microsoft Store is also installed + [switch]$removeMsStoreAsSource = $false, + + # Optional path to a local directory containing the installation files. If provided, the download steps will be skipped. + # - Just copy the entire "Winget Install" folder with the files the script normally downloads, and put it in your mounted folder. + # - Make sure to use the mounted path from the perspective of within the sandbox. + [string]$ExistingInstallerFilesPath +) + +# --- Parameter Usage Examples --- +# Standard run (Download & Install): +# .\Install-Winget.ps1 +# +# Install from existing files instead of downloading: +# .\Install-Winget.ps1 -ExistingInstallerFilesPath "C:\Users\WDAGUtilityAccount\Desktop\HostShared\Winget Install" function Get-LatestRelease { param( @@ -77,25 +90,33 @@ function Install-WingetDependencies { } } +# --- Define Working Directory --- +$userDownloadsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path +$subfolderName = "Winget Install" +$msixName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" + +if ($ExistingInstallerFilesPath) { + if (Test-Path -Path $ExistingInstallerFilesPath) { + $workingDir = $ExistingInstallerFilesPath + Write-Host "Using local source path: $workingDir" -ForegroundColor Yellow + } else { + Write-Error "The specified local source path does not exist: $ExistingInstallerFilesPath" + return + } +} else { + # Combine them to create the full working directory path + $workingDir = Join-Path -Path $userDownloadsFolder -ChildPath $subfolderName + + # Create the directory if it doesn't exist + if (-not (Test-Path -Path $workingDir)) { + New-Item -Path $workingDir -ItemType Directory -Force | Out-Null + } +} + # Prevents progress bar from showing (often speeds downloads) $ProgressPreference = 'SilentlyContinue' -$downloadPath = Join-Path $env:USERPROFILE "Downloads" -$latestRelease = Get-LatestRelease -if (-not $latestRelease) { Write-Error "Could not retrieve the latest release. Exiting."; return; } - -$latestTag = $latestRelease.tag_name -Write-Host "Latest winget version tag is: $latestTag" - -# Download the MSIX bundle -$msixName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -$msixUrl = Get-AssetUrl -release $latestRelease -assetName $msixName -if (-not $msixUrl) { Write-Error "Could not find $msixName in the latest release assets."; return; } - -Write-Host "Downloading $msixName..." -$msixPath = Join-Path $downloadPath $msixName -Invoke-WebRequest -Uri $msixUrl -OutFile $msixPath - +# --- Determine Architecture --- # Figure out the OS architecture using environment variable $procArch = $env:PROCESSOR_ARCHITECTURE switch -Wildcard ($procArch) { @@ -109,49 +130,85 @@ switch -Wildcard ($procArch) { } } -# Download the dependencies zip -$depsZipName = "DesktopAppInstaller_Dependencies.zip" -$depsZipUrl = Get-AssetUrl -release $latestRelease -assetName $depsZipName +# --- Download Steps (Skipped if using existing files) --- +if (-not $ExistingInstallerFilesPath) { + $latestRelease = Get-LatestRelease + if (-not $latestRelease) { Write-Error "Could not retrieve the latest release. Exiting."; return; } -# We'll expand to a base 'Dependencies' folder -$topDepsFolder = Join-Path $downloadPath "Dependencies" -# Then pick the sub-folder for the architecture -$depsFolder = Join-Path $topDepsFolder $arch + $latestTag = $latestRelease.tag_name + Write-Host "Latest winget version tag is: $latestTag" -if ($depsZipUrl) { - Write-Host "Downloading $depsZipName..." - $depsZipPath = Join-Path $downloadPath $depsZipName - Invoke-WebRequest -Uri $depsZipUrl -OutFile $depsZipPath + # Download the MSIX bundle + $msixUrl = Get-AssetUrl -release $latestRelease -assetName $msixName + if (-not $msixUrl) { Write-Error "Could not find $msixName in the latest release assets."; return; } - # Remove existing Dependencies folder and expand the zip - if (Test-Path $topDepsFolder) { Remove-Item -Path $topDepsFolder -Recurse -Force } - - # Use Expand-Archive cmdlet by default because it's safe for constrained language mode. Fall back to .NET assembly if it fails. - try { - Expand-Archive -LiteralPath $depsZipPath -DestinationPath $topDepsFolder -Force -ErrorAction Stop - } - catch { - Write-Warning "Standard extraction failed, attempting .NET fallback. The error was: $($_.Exception.Message)" - # Fallback using .NET System.IO.Compression (Fixes issues in non-EN Windows Sandbox) - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($depsZipPath, $topDepsFolder) - } -} -else { Write-Warning "No $depsZipName found in $latestTag, skipping dependency download."; } + Write-Host "Downloading $msixName..." + $msixPath = Join-Path $workingDir $msixName + Invoke-WebRequest -Uri $msixUrl -OutFile $msixPath + + # Download the dependencies zip + $depsZipName = "DesktopAppInstaller_Dependencies.zip" + $depsZipUrl = Get-AssetUrl -release $latestRelease -assetName $depsZipName + + # We'll expand to a base 'Dependencies' folder + $topDepsFolder = Join-Path $workingDir "Dependencies" + + if ($depsZipUrl) { + Write-Host "Downloading $depsZipName..." + $depsZipPath = Join-Path $workingDir $depsZipName + Invoke-WebRequest -Uri $depsZipUrl -OutFile $depsZipPath + + # Remove existing Dependencies folder and expand the zip + if (Test-Path $topDepsFolder) { Remove-Item -Path $topDepsFolder -Recurse -Force } + + # Use Expand-Archive cmdlet by default because it's safe for constrained language mode. Fall back to .NET assembly if it fails. + try { + Expand-Archive -LiteralPath $depsZipPath -DestinationPath $topDepsFolder -Force -ErrorAction Stop + } + catch { + Write-Warning "Standard extraction failed, attempting .NET fallback. The error was: $($_.Exception.Message)" + # Fallback using .NET System.IO.Compression (Fixes issues in non-EN Windows Sandbox) + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($depsZipPath, $topDepsFolder) + } + + # Cleanup the zip file + if (Test-Path $depsZipPath) { + Remove-Item -Path $depsZipPath -Force + } + } + else { Write-Warning "No $depsZipName found in $latestTag, skipping dependency download."; } +} # Restore progress preference $ProgressPreference = 'Continue' -# If dependencies exist for this architecture, install them +# --- Installation Steps --- + +# Define paths based on working directory +$msixPath = Join-Path $workingDir $msixName +$topDepsFolder = Join-Path $workingDir "Dependencies" +$depsFolder = Join-Path $topDepsFolder $arch + +# Install Dependencies if (Test-Path $depsFolder) { Install-WingetDependencies -depsFolder $depsFolder } else { - Write-Warning "No architecture-specific dependencies found at $depsFolder" + if ($ExistingInstallerFilesPath) { + Write-Error "Dependencies folder not found at: $depsFolder`nEnsure the 'Dependencies' folder is present in your source directory." + } else { + Write-Warning "No architecture-specific dependencies found at $depsFolder" + } } -# Finally, install the winget MSIX bundle -Write-Host "Installing $msixName..." -Add-AppxPackage -Path $msixPath +# Install Winget MSIX bundle +if (Test-Path $msixPath) { + Write-Host "Installing $msixName..." + Add-AppxPackage -Path $msixPath +} else { + Write-Error "Winget package not found at: $msixPath" + return +} # Remove msstore source if set to do so if ($removeMsStoreAsSource.IsPresent) { @@ -164,4 +221,4 @@ if ($removeMsStoreAsSource.IsPresent) { } else { # Automatically accept source agreements to avoid prompts. Mostly applies to msstore. winget list --accept-source-agreements | Out-Null -} +} \ No newline at end of file diff --git a/Startup Scripts/SandboxStartup.ps1 b/Startup Scripts/SandboxStartup.ps1 index 508ba3d..aad208c 100644 --- a/Startup Scripts/SandboxStartup.ps1 +++ b/Startup Scripts/SandboxStartup.ps1 @@ -78,9 +78,9 @@ reg add "HKEY_CLASSES_ROOT\.ps1\ShellNew" /v "ItemName" /t REG_SZ /d "script" /f # NotePad Tip: Go to C:\Windows on your main computer and copy Notepad.exe, then copy notepad.exe.mui from your main language folder, such as C:\Windows\en-US # Important: Notepad.exe.mui can't simply go next to notepad.exe. You need to actually create the language folder (like en-US) again next to notepad.exe and put it in that. Otherwise notepad won't run. -$notepadPath = "C:\Users\WDAGUtilityAccount\Desktop\HostShared\notepad.exe" +$notepadPath = "C:\Users\WDAGUtilityAccount\Desktop\HostShared\Apps\notepad.exe" # For Notepad++, use the portable version -$notepadPlusPlusPath = "C:\Users\WDAGUtilityAccount\Desktop\HostShared\Notepad++\Notepad++.exe" +$notepadPlusPlusPath = "C:\Users\WDAGUtilityAccount\Desktop\HostShared\Apps\Notepad++\Notepad++.exe" # Check if the Notepad and Notepad++ paths exist, if not, set them to null If (!(Test-Path $notepadPath)) { $notepadPath = $null; Write-Host "Notepad not found, context menu options will not be added." }