489 lines
17 KiB
PowerShell
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 |