# Stop on errors to avoid cascading failures. $ErrorActionPreference = "Stop" #Set-StrictMode -Version Latest # === 1. Check dependencies === # Make sure the lib directory exists. if (-not (Test-Path "$PSScriptRoot\..\lib")) { Write-Error "Cannot find .\lib, some files missing." exit 1 } # === 2. Read configuration === $jsonPath = "$PSScriptRoot\apps.json" if (-not (Test-Path $jsonPath)) { Write-Error "Cannot find: $jsonPath" exit 1 } Write-Host "Reading configurations..." -ForegroundColor Cyan # Read as UTF-8. $apps = Get-Content $jsonPath -Encoding UTF8 | ConvertFrom-Json $forcePostInstall = $false function Unblock-WinGetCache { param( [Parameter(Mandatory=$true)] [string]$PackageId ) $wingetCacheRoot = Join-Path $env:TEMP "WinGet" if (-not (Test-Path $wingetCacheRoot)) { return } $cacheItems = Get-ChildItem -Path $wingetCacheRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*\$PackageId.*" } foreach ($item in $cacheItems) { try { Unblock-File -LiteralPath $item.FullName -ErrorAction Stop Write-Host "-> Unblocked cached installer: $($item.FullName)" -ForegroundColor DarkGray } catch { Write-Warning "Failed to unblock cached installer: $($item.FullName), $_" } } } # === 2.1 Load selection.ini overrides === # selection.ini is located in the project root, one level above bin. $selectionPath = Join-Path (Split-Path $PSScriptRoot -Parent) "selection.ini" if (Test-Path $selectionPath) { Write-Host "Loading selection overrides from selection.ini..." -ForegroundColor Cyan try { # Store app switches and global options separately. $selectionConfig = @{} $optionsConfig = @{} $currentSection = "" foreach ($line in (Get-Content $selectionPath -Encoding UTF8)) { $line = $line.Trim() if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(";") -or $line.StartsWith("#")) { continue } if ($line -match '^\[(.+)\]$') { $currentSection = $matches[1].Trim() continue } if ($line -notmatch "=") { continue } # Split on the first equals sign only. $parts = $line -split '=', 2 if ($parts.Count -eq 2) { $key = $parts[0].Trim() $value = $parts[1].Trim() if ($currentSection -eq "Options") { $optionsConfig[$key] = $value } else { $selectionConfig[$key] = $value } } } if ($optionsConfig.ContainsKey("ForcePostInstall")) { $forcePostInstall = $optionsConfig["ForcePostInstall"] -eq "1" } # Apply app enabled overrides. foreach ($app in $apps) { if ($selectionConfig.ContainsKey($app.Name)) { # Convert to integer 1 or 0. $app.Enabled = [int]$selectionConfig[$app.Name] } } } catch { Write-Warning "Error reading selection.ini: $_" } } # === 2.2 Enable local manifest installation === # winget install --manifest requires LocalManifestFiles. Re-running this is harmless. Write-Host "Enabling winget local manifest support..." -ForegroundColor Cyan winget settings --enable LocalManifestFiles # === 3. Main loop === foreach ($app in $apps) { if ($app.Enabled -ne 1) { Write-Host "[Skipping] $($app.Name)" -ForegroundColor DarkGray continue # Skip this app. } Write-Host "`n==========================================" -ForegroundColor Cyan Write-Host "Installing: $($app.Name)" -ForegroundColor Yellow Write-Host "==========================================" if ($app.Id -eq "System.Config") { # This is a configuration-only item. Write-Host "[System Config] Skipping install..." -ForegroundColor Magenta } else { # --- Step A: Winget install --- $manifestPath = $null $manifestTempDir = $null if (-not [string]::IsNullOrWhiteSpace($app.Manifest)) { Write-Host "-> Manifest: $($app.Manifest)" if ($app.Manifest -match '^https?://') { $manifestTempDir = Join-Path $env:TEMP ("winit-helper-manifest-" + [guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $manifestTempDir -Force | Out-Null $manifestFileName = [System.IO.Path]::GetFileName(([uri]$app.Manifest).AbsolutePath) if ([string]::IsNullOrWhiteSpace($manifestFileName)) { $manifestFileName = "$($app.Id).yaml" } $manifestPath = Join-Path $manifestTempDir $manifestFileName Write-Host "-> Downloading custom manifest..." Invoke-WebRequest -Uri $app.Manifest -OutFile $manifestPath } else { $manifestPath = $app.Manifest if (-not [System.IO.Path]::IsPathRooted($manifestPath)) { $manifestPath = Join-Path $PSScriptRoot $manifestPath } if (-not (Test-Path $manifestPath)) { Write-Error "Cannot find manifest: $manifestPath" continue } } $wingetArgs = @( "install", "--manifest", $manifestPath, "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity" # "--scope", "machine" ) } else { $wingetArgs = @( "install", "--id", $app.Id, "-e", "--silent", "--accept-package-agreements", "--accept-source-agreements", "--disable-interactivity" # "--scope", "machine" ) # Add Version only when configured. if (-not [string]::IsNullOrWhiteSpace($app.Version)) { Write-Host "-> Version: $($app.Version)" $wingetArgs += "-v" $wingetArgs += $app.Version } else { Write-Host "-> Version: latest" } } Write-Host "-> Installing via winget..." # Run install command. try { & winget @wingetArgs $exitCode = $LASTEXITCODE if ($exitCode -eq -1978335231 -and -not [string]::IsNullOrWhiteSpace($app.Manifest)) { Write-Warning "winget failed after download. Unblocking cached installer and retrying once..." Unblock-WinGetCache -PackageId $app.Id & winget @wingetArgs $exitCode = $LASTEXITCODE } } finally { if ($manifestTempDir -and (Test-Path $manifestTempDir)) { Remove-Item -LiteralPath $manifestTempDir -Recurse -Force } } # Check install result (0=success, -1978335189=already installed). if ($exitCode -eq 0) { Write-Host "[Success]" -ForegroundColor Green } elseif ($exitCode -eq -1978335189) { if ($forcePostInstall) { Write-Host "[Skip] Already installed, running PostInstall..." -ForegroundColor Yellow } else { Write-Host "[Skip] Already installed" -ForegroundColor Yellow continue # Skip PostInstall when already installed. } } else { Write-Error "[Fail] Error code: $exitCode" continue # Skip PostInstall on install failure. } } # --- Step B: PostInstall configuration --- if ($app.PostInstall -and $app.PostInstall.Count -gt 0) { Write-Host "`n-> Configuring..." -ForegroundColor Cyan $stepIndex = 0 foreach ($action in $app.PostInstall) { # Wait between PostInstall steps. if ($stepIndex -gt 0) { Write-Host "(Waiting 2 seconds...)" -ForegroundColor DarkGray Start-Sleep -Seconds 2 } try { switch ($action.Type) { # 1. Copy file. "FileCopy" { & "$PSScriptRoot\..\lib\invoke-filecopy.ps1" ` -Source $action.Source ` -Destination $action.Destination } # 2. Import registry file. "RegImport" { & "$PSScriptRoot\..\lib\invoke-regimport.ps1" ` -Path $action.Path } # 3. Run command. "Command" { & "$PSScriptRoot\..\lib\invoke-cmdexec.ps1" ` -Command $action.Command ` -WorkDir $action.WorkDir } Default { Write-Warning "`nUnknown action: $($action.Type)" } } } catch { Write-Error "`nFailed to operate step $($stepIndex + 1): $_" } $stepIndex++ } } else { Write-Host "-> No configurations." -ForegroundColor DarkGray } } Write-Host "`n=== Done. Need restart ===" -ForegroundColor Green Pause