Files
windows-builder/includes/$OEM$/$$/OEM/scripts/install-win-apps.ps1
2026-06-02 03:37:09 -07:00

489 lines
17 KiB
PowerShell

# Check PowerShell version for use in path
if ($PSVersionTable.PSVersion.Major -ge 3) {
$currentDir = $PSScriptRoot
}
else {
$currentDir = (Get-Item .).FullName
}
# Import script-helper.ps1
. "$currentDir\script-helper.ps1"
# Check if winget is available
$wingetAvailable = $true
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Host "winget is not installed. Winget-based installs will be skipped." -ForegroundColor Yellow
$wingetAvailable = $false
}
# Get system information
$systemInfo = Get-SystemInfo
# Path to the UWP apps list file
$uwpFolder = "$windowsDrive\Windows\OEM\apps\uwp"
$uwpAppsFile = "$windowsDrive\Windows\OEM\apps\uwp-apps.json"
# Path to the winget apps list file
$wingetAppsFile = "$windowsDrive\Windows\OEM\apps\winget.json"
# Determine system architecture (normalize to x64/x86/arm64)
$systemArch = $systemInfo.Architecture.ToLower()
switch ($systemArch) {
"64-bit" { $systemArch = "x64" }
"32-bit" { $systemArch = "x86" }
}
#region Helper Functions
# Create a temporary download cache
$downloadCache = "$env:TEMP\uwp-cache"
New-Item -ItemType Directory -Path $downloadCache -Force | Out-Null
# Helper to download remote package if not already cached
function Get-RemotePackage {
param(
[string]$Url
)
$fileName = Split-Path $Url -Leaf
$dest = Join-Path $downloadCache $fileName
if (-not (Test-Path $dest)) {
Write-Host "Downloading $fileName..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $Url -OutFile $dest -UseBasicParsing
}
return $dest
}
# Function to check if a package is already installed
function Test-PackageInstalled {
param([string]$packageName)
try {
$package = Get-AppxPackage -Name $packageName -ErrorAction SilentlyContinue
return ($null -ne $package)
}
catch {
return $false
}
}
# Function to extract package name from filename
function Get-PackageNameFromFile {
param([string]$filename)
# Remove extension and architecture info to get base package name
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($filename)
# Extract package family name (everything before version)
if ($baseName -match '^([^_]+(?:_[^_]+)*)_\d') {
return $matches[1]
}
return $baseName
}
# Function to check if a newer version is already installed
function Test-NewerVersionInstalled {
param(
[string]$packageName,
[string]$fileVersion
)
try {
$installedPackage = Get-AppxPackage -Name $packageName -ErrorAction SilentlyContinue
if (-not $installedPackage) {
return $false
}
# Compare versions - if installed version is greater than file version, skip
$installedVersion = $installedPackage.Version
if ([System.Version]$installedVersion -gt [System.Version]$fileVersion) {
return $true
}
return $false
}
catch {
return $false
}
}
# Function to extract version from filename
function Get-VersionFromFile {
param([string]$filename)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($filename)
# Pattern: name_version_arch
if ($baseName -match '_(\d+\.\d+\.\d+\.\d+)_') {
return $matches[1]
}
elseif ($baseName -match '_(\d+\.\d+\.\d+\.\d+)\.') {
return $matches[1]
}
return "0.0.0.0"
}
function Install-FromStore {
param(
[string]$ProductId
)
Write-Host "Installing from Microsoft Store ($ProductId)..." -ForegroundColor Cyan
$result = winget install `
--id $ProductId `
--source msstore `
--accept-package-agreements `
--accept-source-agreements `
--silent
return ($LASTEXITCODE -eq 0)
}
function Install-WingetApp {
param(
[Parameter(Mandatory)]
[string[]]$Arguments
)
if (-not $wingetAvailable) {
return
}
Write-Host "winget $($Arguments -join ' ')" -ForegroundColor DarkGray
winget @Arguments
}
# Function to resolve dependency file path from the new JSON structure
function Get-DependencyFilePath {
param(
[string]$dependencyName,
[string]$version,
[string]$arch,
[object]$dependenciesTable
)
if (-not $dependenciesTable.$dependencyName) {
Write-Host "Dependency $dependencyName not found in dependencies table" -ForegroundColor Red
return $null
}
if (-not $dependenciesTable.$dependencyName.$version) {
Write-Host "Version $version of $dependencyName not found in dependencies table" -ForegroundColor Red
return $null
}
$archPath = $dependenciesTable.$dependencyName.$version.$arch
if (-not $archPath) {
Write-Host "Architecture $arch not available for $dependencyName version $version" -ForegroundColor Yellow
return $null
}
return Join-Path $uwpFolder $archPath
}
#endregion
#region UWP App Installation
if (-not (Test-Path $uwpAppsFile)) {
Write-Host "No UWP apps list file found at $uwpAppsFile. Skipping UWP installation." -ForegroundColor Yellow
}
else {
Write-Host "`n========================================" -ForegroundColor Magenta
Write-Host "Installing UWP Apps" -ForegroundColor Magenta
Write-Host "========================================`n" -ForegroundColor Magenta
# Load UWP apps JSON
$uwpAppsData = Get-Content $uwpAppsFile | ConvertFrom-Json
$uwpApps = $uwpAppsData.apps
$dependenciesTable = $uwpAppsData.dependencies
# Install dependencies and main apps per app
foreach ($app in $uwpApps) {
$appPackageFamilyName = $app.packageFamilyName
Write-Host "Installing $appPackageFamilyName..." -ForegroundColor Green
# Collect valid dependencies for this app
$validDependencies = @()
# Install dependencies per app (new format)
if ($app.dependencies) {
foreach ($depName in $app.dependencies.PSObject.Properties.Name) {
$requiredVersion = $app.dependencies.$depName.version
Write-Host "Processing dependency: $depName (version $requiredVersion)..." -ForegroundColor Cyan
# Get the dependency file path for current architecture
$depPath = Get-DependencyFilePath -dependencyName $depName `
-version $requiredVersion `
-arch $systemArch `
-dependenciesTable $dependenciesTable
if (-not $depPath) {
Write-Host "Could not resolve dependency path for $depName" -ForegroundColor Red
continue
}
if (-not (Test-Path $depPath)) {
Write-Host "Dependency file not found: $depPath" -ForegroundColor Red
continue
}
# Extract package name and version from file
$depFileName = Split-Path $depPath -Leaf
$packageName = Get-PackageNameFromFile $depFileName
$fileVersion = Get-VersionFromFile $depFileName
# Check if package is already installed
$installedPackage = Get-AppxPackage -Name $packageName -ErrorAction SilentlyContinue
if ($installedPackage) {
if (Test-NewerVersionInstalled -packageName $packageName -fileVersion $fileVersion) {
Write-Host "Newer version of $packageName already installed ($($installedPackage.Version) > $fileVersion), skipping..." -ForegroundColor Green
}
else {
Write-Host "Dependency $packageName is already installed, skipping..." -ForegroundColor Green
}
$validDependencies += $depPath
}
else {
Write-Host "Installing dependency $depFileName..." -ForegroundColor Cyan
try {
Add-AppxPackage -Path $depPath -ForceApplicationShutdown -ForceUpdateFromAnyVersion -ErrorAction Stop
Write-Host "Successfully installed dependency: $depFileName" -ForegroundColor Green
$validDependencies += $depPath
}
catch {
Write-Host "Failed to install dependency $depFileName : $($_.Exception.Message)" -ForegroundColor Red
# Continue with installation even if dependency fails
}
}
}
}
# Select main package file for this system architecture
$mainAppPath = $null
switch ($app.installSource) {
"Store" {
if ($app.store.productId) {
if (Install-FromStore $app.store.productId) {
Write-Host "Installed via Microsoft Store." -ForegroundColor Green
continue
}
else {
Write-Host "Store install failed." -ForegroundColor Red
continue
}
}
}
"Url" {
$selectedFile = $app.urlFiles | Where-Object { $_.arch -contains $systemArch } | Select-Object -First 1
if (-not $selectedFile) {
$selectedFile = $app.urlFiles | Where-Object { $_.arch -contains "neutral" } | Select-Object -First 1
}
if (-not $selectedFile) {
Write-Host "No compatible URL file found." -ForegroundColor Red
continue
}
$mainAppPath = Get-RemotePackage $selectedFile.url
}
default {
# Local (new format uses 'files' array)
$selectedFile = $app.files | Where-Object { $_.arch -contains $systemArch } | Select-Object -First 1
if (-not $selectedFile) {
$selectedFile = $app.files | Where-Object { $_.arch -contains "neutral" } | Select-Object -First 1
}
if (-not $selectedFile) {
Write-Host "No compatible local file found for $appPackageFamilyName (System Arch: $systemArch)" -ForegroundColor Red
continue
}
$mainAppPath = Join-Path $uwpFolder $selectedFile.path
}
}
# Check if main app file exists
if (-not (Test-Path $mainAppPath)) {
Write-Host "Main app file not found: $mainAppPath" -ForegroundColor Red
continue
}
# Check if a newer version of the main app is already installed
$mainPackageName = Get-PackageNameFromFile $selectedFile.path
$mainFileVersion = Get-VersionFromFile $selectedFile.path
$installedMainApp = Get-AppxPackage -Name $mainPackageName -ErrorAction SilentlyContinue
if ($installedMainApp) {
if (Test-NewerVersionInstalled -packageName $mainPackageName -fileVersion $mainFileVersion) {
Write-Host "Newer version of $mainPackageName already installed ($($installedMainApp.Version) > $mainFileVersion), skipping..." -ForegroundColor Green
continue
}
else {
Write-Host "App $mainPackageName is already installed, attempting update..." -ForegroundColor Yellow
}
}
$params = @{
Path = $mainAppPath
ForceApplicationShutdown = $true
ForceUpdateFromAnyVersion = $true
}
if ($app.licensePath) {
$licensePath = Join-Path $uwpFolder $app.licensePath
if (Test-Path $licensePath) {
$params.LicensePath = $licensePath
}
else {
Write-Host "License file not found: $licensePath" -ForegroundColor Yellow
}
}
if ($app.externalLocation) {
$externalLocation = Join-Path $uwpFolder $app.externalLocation
if (Test-Path $externalLocation) {
$params.ExternalLocation = $externalLocation
}
else {
Write-Host "External location not found: $externalLocation" -ForegroundColor Yellow
}
}
# Only add DependencyPath if we have valid dependencies that were successfully installed
if ($validDependencies.Count -gt 0) {
$params.DependencyPath = $validDependencies
}
# Apply optional flags (using camelCase from new JSON format)
foreach ($flag in @(
"requiredContentGroupOnly",
"forceTargetApplicationShutdown",
"installAllResources",
"preventRegistration",
"register",
"stageOnly",
"deferRegistrationWhenPackagesAreInUse"
)) {
if ($app.$flag -eq $true) {
# Convert camelCase to PascalCase for PowerShell parameter
$paramName = $flag.Substring(0,1).ToUpper() + $flag.Substring(1)
$params[$paramName] = $true
}
}
# Install the app with error handling
try {
Write-Host "Installing main app: $appPackageFamilyName..." -ForegroundColor Cyan
Add-AppxPackage @params -ErrorAction Stop
Write-Host "Successfully installed: $appPackageFamilyName" -ForegroundColor Green
}
catch {
Write-Host "Failed to install $appPackageFamilyName : $($_.Exception.Message)" -ForegroundColor Red
# Try installing without dependencies if that was the issue
if ($_.Exception.Message -like "*was provided but not used*") {
Write-Host "Retrying without dependency path..." -ForegroundColor Yellow
try {
$params.Remove('DependencyPath')
Add-AppxPackage @params -ErrorAction Stop
Write-Host "Successfully installed $appPackageFamilyName without dependencies" -ForegroundColor Green
}
catch {
Write-Host "Failed to install even without dependencies: $($_.Exception.Message)" -ForegroundColor Red
}
}
}
}
Write-Host "UWP app installation completed!" -ForegroundColor Green
}
#endregion
#region Winget App Installation
if (-not $wingetAvailable) {
Write-Host "`nwinget is not available. Skipping winget app installation." -ForegroundColor Yellow
}
elseif (-not (Test-Path $wingetAppsFile)) {
Write-Host "`nNo winget apps list file found at $wingetAppsFile. Skipping winget app installation." -ForegroundColor Yellow
}
else {
Write-Host "`n========================================" -ForegroundColor Magenta
Write-Host "Installing Winget Apps" -ForegroundColor Magenta
Write-Host "========================================`n" -ForegroundColor Magenta
$wingetApps = Get-Content $wingetAppsFile | ConvertFrom-Json
foreach ($app in $wingetApps) {
$appId = $app.Id
Write-Host "Processing $appId..." -ForegroundColor Cyan
# Select the correct variant by architecture (fallback to neutral)
$variant = $app.Variants | Where-Object { $_.Arch -contains $systemArch } | Select-Object -First 1
if (-not $variant) {
$variant = $app.Variants | Where-Object { $_.Arch -contains "neutral" } | Select-Object -First 1
}
if (-not $variant) {
Write-Host "No matching architecture variant found for $appId (System Arch: $systemArch)" -ForegroundColor Red
continue
}
Write-Host "Installing $appId for architecture $systemArch..." -ForegroundColor Green
# Base command
$wingetArgs = @(
"install",
"--id", $appId,
"--accept-source-agreements",
"--accept-package-agreements",
"--silent"
)
# Architecture from chosen variant
if ($variant.Arch) {
$wingetArgs += "--architecture"
$wingetArgs += $systemArch
}
# Variant version if included
if ($variant.Version) {
$wingetArgs += "--version"
$wingetArgs += $variant.Version
}
# Global (non-arch) properties
if ($app.Source) { $wingetArgs += @("--source", $app.Source) }
if ($app.Scope) { $wingetArgs += @("--scope", $app.Scope) }
if ($app.Mode) { $wingetArgs += @("--mode", $app.Mode) }
if ($app.InstallationPath) { $wingetArgs += @("--location", $app.InstallationPath) }
if ($app.Log) { $wingetArgs += @("--log", $app.Log) }
if ($app.Override) { $wingetArgs += @("--override", $app.Override) }
# Boolean flags
foreach ($flag in @("Force","Interactive","DisableInteractivity","VerboseLogs","NoUpgrade","Help","Wait","OpenLogs")) {
if ($app.$flag -eq $true) {
$wingetArgs += "--" + ($flag -replace '([A-Z])', '-$1').ToLower()
}
}
# Execute installation
winget @wingetArgs
}
Write-Host "`nWinget app installation completed!" -ForegroundColor Green
}
#endregion
Write-Host "`n========================================" -ForegroundColor Magenta
Write-Host "All app installations completed!" -ForegroundColor Magenta
Write-Host "========================================" -ForegroundColor Magenta