mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf27da5c66 | |||
| 1010b9fce7 | |||
| 3e34bd6bff | |||
| 3f892493c0 | |||
| 7d4567efbe | |||
| 9aed707a77 | |||
| 0c373e6b2c | |||
| a501b32a03 | |||
| 8ab6603999 | |||
| 85383f989a | |||
| 0423ac31d9 | |||
| 35f37f3a36 | |||
| 78d7bb9262 | |||
| 3c545be5c5 | |||
| c1983f75e6 | |||
| 7c3de6d77f | |||
| 17dc80f11b | |||
| 846d449aac | |||
| db9b7335f2 | |||
| 6f98473009 | |||
| 357261ec73 | |||
| 5bef901295 | |||
| 59e247c012 | |||
| a87c4796b5 | |||
| 4d289ee14a | |||
| 08feb7c9dd | |||
| 9cb06cb71e | |||
| 5ec607d94a | |||
| ac7ef119e0 | |||
| 03c8127bd3 | |||
| eb001e59b3 | |||
| 3e46d4b280 | |||
| eae07fcad0 | |||
| 41b65a76c1 | |||
| 67c992806f |
@@ -7,47 +7,81 @@ function Invoke-Process {
|
|||||||
[string]$FilePath,
|
[string]$FilePath,
|
||||||
|
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[ValidateNotNullOrEmpty()]
|
|
||||||
[string[]]$ArgumentList,
|
[string[]]$ArgumentList,
|
||||||
|
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[ValidateNotNullOrEmpty()]
|
[ValidateNotNullOrEmpty()]
|
||||||
[bool]$Wait = $true
|
[bool]$Wait = $true,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string[]]$AdditionalSuccessCodes,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$IgnoreNonZeroExitCodes = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)"
|
|
||||||
$stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)"
|
|
||||||
|
|
||||||
$startProcessParams = @{
|
|
||||||
FilePath = $FilePath
|
|
||||||
ArgumentList = $ArgumentList
|
|
||||||
RedirectStandardError = $stdErrTempFile
|
|
||||||
RedirectStandardOutput = $stdOutTempFile
|
|
||||||
Wait = $($Wait);
|
|
||||||
PassThru = $true;
|
|
||||||
NoNewWindow = $true;
|
|
||||||
}
|
|
||||||
if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
|
if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) {
|
||||||
$cmd = Start-Process @startProcessParams
|
# Use .NET Process class for proper stream handling
|
||||||
$cmdOutput = Get-Content -Path $stdOutTempFile -Raw
|
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$cmdError = Get-Content -Path $stdErrTempFile -Raw
|
$pinfo.FileName = $FilePath
|
||||||
if ($cmd.ExitCode -ne 0 -and $wait -eq $true) {
|
if ($ArgumentList) {
|
||||||
if ($cmdError) {
|
$pinfo.Arguments = $ArgumentList -join ' '
|
||||||
throw $cmdError.Trim()
|
}
|
||||||
|
$pinfo.RedirectStandardOutput = $true
|
||||||
|
$pinfo.RedirectStandardError = $true
|
||||||
|
$pinfo.UseShellExecute = $false
|
||||||
|
$pinfo.CreateNoWindow = $true
|
||||||
|
|
||||||
|
$p = New-Object System.Diagnostics.Process
|
||||||
|
$p.StartInfo = $pinfo
|
||||||
|
|
||||||
|
# Start the process
|
||||||
|
$p.Start() | Out-Null
|
||||||
|
|
||||||
|
# Read output and error streams
|
||||||
|
$cmdOutput = $p.StandardOutput.ReadToEnd()
|
||||||
|
$cmdError = $p.StandardError.ReadToEnd()
|
||||||
|
|
||||||
|
if ($Wait) {
|
||||||
|
$p.WaitForExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
$exitCode = $p.ExitCode
|
||||||
|
# An exit code of 0 is always a success
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
# If IgnoreNonZeroExitCodes is true, treat any non-zero exit code as a success
|
||||||
|
if ($IgnoreNonZeroExitCodes) {
|
||||||
|
Write-Host "Ignoring non-zero exit code $exitCode because IgnoreNonZeroExitCodes is set to true."
|
||||||
}
|
}
|
||||||
if ($cmdOutput) {
|
# Check if the non-zero exit code is in the list of additional success codes
|
||||||
throw $cmdOutput.Trim()
|
elseif ($null -eq $AdditionalSuccessCodes -or $exitCode -notin $AdditionalSuccessCodes) {
|
||||||
|
if ($cmdError) {
|
||||||
|
throw $cmdError.Trim()
|
||||||
|
}
|
||||||
|
if ($cmdOutput) {
|
||||||
|
throw $cmdOutput.Trim()
|
||||||
|
}
|
||||||
|
# If there's no output, throw a generic error with the exit code
|
||||||
|
if (-not $cmdError -and -not $cmdOutput) {
|
||||||
|
throw "Process exited with non-zero code: $exitCode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) {
|
||||||
# WriteLog $cmdOutput
|
# WriteLog $cmdOutput
|
||||||
Write-Host $cmdOutput
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Create a simple object with exit code for compatibility
|
||||||
|
$result = [PSCustomObject]@{
|
||||||
|
ExitCode = $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -55,12 +89,7 @@ function Invoke-Process {
|
|||||||
# WriteLog $_
|
# WriteLog $_
|
||||||
# Write-Host "Script failed - $Logfile for more info"
|
# Write-Host "Script failed - $Logfile for more info"
|
||||||
throw $_
|
throw $_
|
||||||
|
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore
|
|
||||||
}
|
|
||||||
return $cmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Install-Applications {
|
function Install-Applications {
|
||||||
@@ -110,15 +139,58 @@ function Install-Applications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
# Check for 'PAUSE' command
|
||||||
# Construct the argument list properly, handling potential array vs string
|
if ($app.CommandLine -eq 'PAUSE') {
|
||||||
$argumentsToPass = if ($app.Arguments -is [array]) { $app.Arguments } else { @($app.Arguments) }
|
Write-Host "Pausing script as requested by '$($app.Name)'. Press Enter to continue..."
|
||||||
|
$null = Read-Host
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Running command: $($app.CommandLine) $($argumentsToPass -join ' ')"
|
try {
|
||||||
$result = Invoke-Process -FilePath $($app.CommandLine) -ArgumentList $argumentsToPass
|
# Normalize arguments: treat null/empty/whitespace as no arguments
|
||||||
|
$argumentsToPass = $null
|
||||||
|
if ($null -ne $app.Arguments) {
|
||||||
|
if ($app.Arguments -is [array]) {
|
||||||
|
$trimmed = $app.Arguments | ForEach-Object { ($_ | ForEach-Object { if ($_ -ne $null) { $_.ToString().Trim() } else { $_ } }) } | Where-Object { $_ -and (-not [string]::IsNullOrWhiteSpace($_)) }
|
||||||
|
if ($trimmed.Count -gt 0) {
|
||||||
|
$argumentsToPass = $trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$single = $app.Arguments.ToString().Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($single)) {
|
||||||
|
$argumentsToPass = @($single)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for and parse AdditionalExitCodes
|
||||||
|
$additionalSuccessCodes = @()
|
||||||
|
if ($app.PSObject.Properties['AdditionalExitCodes'] -and -not [string]::IsNullOrWhiteSpace($app.AdditionalExitCodes)) {
|
||||||
|
$additionalSuccessCodes = $app.AdditionalExitCodes -split ',' | ForEach-Object { $_.Trim() }
|
||||||
|
Write-Host "Additional success exit codes for $($app.Name): $($additionalSuccessCodes -join ', ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for IgnoreNonZeroExitCodes
|
||||||
|
$ignoreNonZeroExitCodes = $false
|
||||||
|
if ($app.PSObject.Properties['IgnoreNonZeroExitCodes'] -and $app.IgnoreNonZeroExitCodes -is [bool]) {
|
||||||
|
$ignoreNonZeroExitCodes = $app.IgnoreNonZeroExitCodes
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $argumentsToPass -or $argumentsToPass.Count -eq 0) {
|
||||||
|
Write-Host "Running command: $($app.CommandLine) (no arguments)"
|
||||||
|
$result = Invoke-Process -FilePath $app.CommandLine -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Running command: $($app.CommandLine) $($argumentsToPass -join ' ')"
|
||||||
|
$result = Invoke-Process -FilePath $app.CommandLine -ArgumentList $argumentsToPass -AdditionalSuccessCodes $additionalSuccessCodes -IgnoreNonZeroExitCodes $ignoreNonZeroExitCodes
|
||||||
|
}
|
||||||
Write-Host "$($app.Name) exited with exit code: $($result.ExitCode)`r`n"
|
Write-Host "$($app.Name) exited with exit code: $($result.ExitCode)`r`n"
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
Write-Error "Error occurred while installing $($app.Name): $_"
|
Write-Error "Error occurred while installing $($app.Name): $_"
|
||||||
|
Read-Host "An error occurred, and the script cannot continue. Press Enter to exit."
|
||||||
|
throw $_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,16 +212,20 @@ if (Test-Path -Path $wingetAppsJsonFile) {
|
|||||||
if ($wingetContent -is [array]) {
|
if ($wingetContent -is [array]) {
|
||||||
$wingetApps = $wingetContent
|
$wingetApps = $wingetContent
|
||||||
Write-Host "Found $(($wingetApps | Measure-Object).Count) WinGet Win32 apps."
|
Write-Host "Found $(($wingetApps | Measure-Object).Count) WinGet Win32 apps."
|
||||||
} elseif ($wingetContent) {
|
}
|
||||||
|
elseif ($wingetContent) {
|
||||||
$wingetApps = @($wingetContent) # Ensure it's an array
|
$wingetApps = @($wingetContent) # Ensure it's an array
|
||||||
Write-Host "Found 1 WinGet Win32 app."
|
Write-Host "Found 1 WinGet Win32 app."
|
||||||
} else {
|
|
||||||
Write-Host "WinGetWin32Apps.json is empty or invalid."
|
|
||||||
}
|
}
|
||||||
} catch {
|
else {
|
||||||
|
Write-Host "WinGetWin32Apps.json is empty or invalid."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
Write-Error "Failed to read or parse WinGetWin32Apps.json file: $_"
|
Write-Error "Failed to read or parse WinGetWin32Apps.json file: $_"
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host "WinGetWin32Apps.json file not found. Skipping."
|
Write-Host "WinGetWin32Apps.json file not found. Skipping."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,16 +242,20 @@ if (Test-Path -Path $userAppsJsonFile) {
|
|||||||
if ($userContent -is [array]) {
|
if ($userContent -is [array]) {
|
||||||
$userApps = $userContent
|
$userApps = $userContent
|
||||||
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
|
Write-Host "Found $(($userApps | Measure-Object).Count) user-defined apps."
|
||||||
} elseif ($userContent) {
|
}
|
||||||
|
elseif ($userContent) {
|
||||||
$userApps = @($userContent) # Ensure it's an array
|
$userApps = @($userContent) # Ensure it's an array
|
||||||
Write-Host "Found 1 user-defined app."
|
Write-Host "Found 1 user-defined app."
|
||||||
} else {
|
|
||||||
Write-Host "UserAppList.json is empty or invalid."
|
|
||||||
}
|
}
|
||||||
} catch {
|
else {
|
||||||
|
Write-Host "UserAppList.json is empty or invalid."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
Write-Error "Failed to read or parse UserAppList.json file: $_"
|
Write-Error "Failed to read or parse UserAppList.json file: $_"
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Write-Host "UserAppList.json file not found. Skipping."
|
Write-Host "UserAppList.json file not found. Skipping."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ else {
|
|||||||
|
|
||||||
# Example of how to use the AppsScriptVariables hashtable to control script execution
|
# Example of how to use the AppsScriptVariables hashtable to control script execution
|
||||||
|
|
||||||
|
# Note: The UI saves the values as strings, so if you type true for a value, it'll save to the config file as a string, not boolean
|
||||||
|
|
||||||
# Example: Check if a variable named 'foo' is set to string 'bar' and run a script accordingly
|
# Example: Check if a variable named 'foo' is set to string 'bar' and run a script accordingly
|
||||||
# if ($AppsScriptVariables['foo'] -eq 'bar') {
|
# if ($AppsScriptVariables['foo'] -eq 'bar') {
|
||||||
# Write-Host "Foo would have installed"
|
# Write-Host "Foo would have installed"
|
||||||
@@ -46,8 +48,8 @@ else {
|
|||||||
# Write-Host "Foo would not have installed"
|
# Write-Host "Foo would not have installed"
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Example: Check if a variable named 'foo' is set to boolean $true and run a script accordingly
|
# Example: Check if a variable named 'Teams' is set to string 'true' and run a script accordingly
|
||||||
# if ($AppsScriptVariables[Teams] -eq $true) {
|
# if ($AppsScriptVariables['Teams'] -eq 'true') {
|
||||||
# Write-Host "Teams would have been installed"
|
# Write-Host "Teams would have been installed"
|
||||||
# }
|
# }
|
||||||
# else {
|
# else {
|
||||||
@@ -55,5 +57,4 @@ else {
|
|||||||
# }
|
# }
|
||||||
|
|
||||||
# Your code below here
|
# Your code below here
|
||||||
|
|
||||||
Write-Host 'Invoke-AppsScript.ps1 finished'
|
Write-Host 'Invoke-AppsScript.ps1 finished'
|
||||||
@@ -1,14 +1,90 @@
|
|||||||
#The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time.
|
#The below lines will remove the unattend.xml that gets the machine into audit mode. If not removed, the OS will get stuck booting to audit mode each time.
|
||||||
#Also kills the sysprep process in order to automate sysprep generalize
|
#Also kills the sysprep process in order to automate sysprep generalize
|
||||||
# Convert these commands to native powershell
|
Write-Host "Removing existing unattend.xml files and stopping sysprep process if running..."
|
||||||
# del c:\windows\panther\unattend\unattend.xml /F /Q
|
|
||||||
# del c:\windows\panther\unattend.xml /F /Q
|
|
||||||
# taskkill /IM sysprep.exe
|
|
||||||
# timeout /t 10
|
|
||||||
# & c:\windows\system32\sysprep\sysprep.exe /quiet /generalize /oobe
|
|
||||||
|
|
||||||
Remove-Item -Path "C:\windows\panther\unattend\unattend.xml" -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path "C:\windows\panther\unattend\unattend.xml" -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -Path "C:\windows\panther\unattend.xml" -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path "C:\windows\panther\unattend.xml" -Force -ErrorAction SilentlyContinue
|
||||||
Stop-Process -Name "sysprep" -Force -ErrorAction SilentlyContinue
|
Stop-Process -Name "sysprep" -Force -ErrorAction SilentlyContinue
|
||||||
Start-Sleep -Seconds 10
|
Start-Sleep -Seconds 10
|
||||||
& "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe
|
|
||||||
|
# Detect and remediate per-user, non-provisioned Appx packages that would block Sysprep.
|
||||||
|
Write-Host "Checking for per-user Appx packages not provisioned for all users (potential Sysprep blockers)..."
|
||||||
|
|
||||||
|
# Build hash set of provisioned package families (DisplayName_PublisherId).
|
||||||
|
$provFamilies = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
||||||
|
Get-AppxProvisionedPackage -Online | ForEach-Object {
|
||||||
|
$family = '{0}_{1}' -f $_.DisplayName, $_.PublisherId
|
||||||
|
[void]$provFamilies.Add($family)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect current user Appx packages excluding frameworks, resource packs, and non-removable packages.
|
||||||
|
$userApps = Get-AppxPackage -User $env:USERNAME | Where-Object {
|
||||||
|
$_.Status -eq 'Ok' -and
|
||||||
|
-not $_.IsFramework -and
|
||||||
|
-not $_.IsResourcePackage -and
|
||||||
|
-not $_.NonRemovable
|
||||||
|
}
|
||||||
|
|
||||||
|
# Identify packages not provisioned (per-user only).
|
||||||
|
$notProvisioned = foreach ($pkg in $userApps) {
|
||||||
|
if (-not $provFamilies.Contains($pkg.PackageFamilyName)) {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Name = $pkg.Name
|
||||||
|
PackageFamilyName = $pkg.PackageFamilyName
|
||||||
|
Version = $pkg.Version
|
||||||
|
SignatureKind = $pkg.SignatureKind
|
||||||
|
PackageFullName = $pkg.PackageFullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($notProvisioned) {
|
||||||
|
Write-Host "Found $($notProvisioned.Count) per-user Appx package(s) not provisioned for all users:"
|
||||||
|
$notProvisioned | Sort-Object PackageFamilyName | Format-Table -AutoSize -Property Name,PackageFamilyName,Version
|
||||||
|
Write-Host "Attempting removal of per-user, non-provisioned Appx packages..."
|
||||||
|
foreach ($pkg in $notProvisioned) {
|
||||||
|
try {
|
||||||
|
Write-Host "Removing $($pkg.PackageFullName)..."
|
||||||
|
Remove-AppxPackage -Package $pkg.PackageFullName -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to remove $($pkg.PackageFullName): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-check after attempted removals.
|
||||||
|
$remaining = @()
|
||||||
|
$currentUserApps = Get-AppxPackage -User $env:USERNAME | Where-Object {
|
||||||
|
$_.Status -eq 'Ok' -and
|
||||||
|
-not $_.IsFramework -and
|
||||||
|
-not $_.IsResourcePackage -and
|
||||||
|
-not $_.NonRemovable
|
||||||
|
}
|
||||||
|
foreach ($pkg in $currentUserApps) {
|
||||||
|
if (-not $provFamilies.Contains($pkg.PackageFamilyName)) {
|
||||||
|
$remaining += $pkg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remaining.Count -gt 0) {
|
||||||
|
Write-Error "Unable to remove all per-user, non-provisioned Appx packages. Sysprep cannot continue."
|
||||||
|
$remaining | Sort-Object PackageFamilyName | Format-Table -AutoSize -Property Name,PackageFamilyName,Version
|
||||||
|
throw "Sysprep aborted due to unresolved per-user Appx packages. Resolve manually and re-run."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "All per-user, non-provisioned Appx packages were successfully removed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "No per-user, non-provisioned Appx packages detected."
|
||||||
|
}
|
||||||
|
|
||||||
|
# If an Unattend.xml has been provided on the mounted Apps ISO (D:\Unattend\Unattend.xml),
|
||||||
|
# pass it to sysprep; otherwise, run without /unattend.
|
||||||
|
$unattendOnAppsIso = "D:\Unattend\Unattend.xml"
|
||||||
|
if (Test-Path -Path $unattendOnAppsIso) {
|
||||||
|
Write-Host "Using $unattendOnAppsIso from Apps ISO..."
|
||||||
|
& "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe /unattend:$unattendOnAppsIso
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
& "C:\windows\system32\sysprep\sysprep.exe" /quiet /generalize /oobe
|
||||||
|
}
|
||||||
|
|||||||
+772
-57
File diff suppressed because it is too large
Load Diff
@@ -43,14 +43,17 @@ $script:uiState = [PSCustomObject]@{
|
|||||||
vmSwitchMap = @{};
|
vmSwitchMap = @{};
|
||||||
logData = $null;
|
logData = $null;
|
||||||
logStreamReader = $null;
|
logStreamReader = $null;
|
||||||
pollTimer = $null
|
pollTimer = $null;
|
||||||
|
lastConfigFilePath = $null
|
||||||
};
|
};
|
||||||
Flags = @{
|
Flags = @{
|
||||||
installAppsForcedByUpdates = $false;
|
installAppsForcedByUpdates = $false;
|
||||||
prevInstallAppsStateBeforeUpdates = $null;
|
prevInstallAppsStateBeforeUpdates = $null;
|
||||||
installAppsCheckedByOffice = $false;
|
installAppsCheckedByOffice = $false;
|
||||||
lastSortProperty = $null;
|
lastSortProperty = $null;
|
||||||
lastSortAscending = $true
|
lastSortAscending = $true;
|
||||||
|
isBuilding = $false;
|
||||||
|
isCleanupRunning = $false
|
||||||
};
|
};
|
||||||
Defaults = @{};
|
Defaults = @{};
|
||||||
LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log"
|
LogFilePath = "$FFUDevelopmentPath\FFUDevelopment_UI.log"
|
||||||
@@ -132,7 +135,245 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
# Get a local reference to the button for convenience in this handler
|
# Get a local reference to the button for convenience in this handler
|
||||||
$btnRun = $script:uiState.Controls.btnRun
|
$btnRun = $script:uiState.Controls.btnRun
|
||||||
try {
|
try {
|
||||||
# Disable button to prevent multiple clicks
|
# If a build is running and cleanup is not already running, treat this click as Cancel
|
||||||
|
if ($script:uiState.Flags.isBuilding -and -not $script:uiState.Flags.isCleanupRunning) {
|
||||||
|
$btnRun.IsEnabled = $false
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Cancel requested. Stopping build..."
|
||||||
|
WriteLog "Cancel requested by user. Stopping background build job."
|
||||||
|
|
||||||
|
# Stop the timer
|
||||||
|
if ($null -ne $script:uiState.Data.pollTimer) {
|
||||||
|
$script:uiState.Data.pollTimer.Stop()
|
||||||
|
$script:uiState.Data.pollTimer = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Close the log stream
|
||||||
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
||||||
|
$script:uiState.Data.logStreamReader.Close()
|
||||||
|
$script:uiState.Data.logStreamReader.Dispose()
|
||||||
|
$script:uiState.Data.logStreamReader = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop and remove the running build job
|
||||||
|
$jobToStop = $script:uiState.Data.currentBuildJob
|
||||||
|
$script:uiState.Data.currentBuildJob = $null
|
||||||
|
if ($null -ne $jobToStop) {
|
||||||
|
try {
|
||||||
|
# Attempt graceful stop first
|
||||||
|
Stop-Job -Job $jobToStop -ErrorAction SilentlyContinue
|
||||||
|
Wait-Job -Job $jobToStop -Timeout 5 -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Stop-Job threw: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# If the job's hosting process is still alive, kill its process tree to stop child tools like DISM
|
||||||
|
try {
|
||||||
|
$jobProcId = $null
|
||||||
|
if ($null -ne $jobToStop.ChildJobs -and $jobToStop.ChildJobs.Count -gt 0) {
|
||||||
|
$jobProcId = $jobToStop.ChildJobs[0].ProcessId
|
||||||
|
}
|
||||||
|
if ($jobProcId) {
|
||||||
|
# Recursively terminate the job process and any children
|
||||||
|
function Stop-ProcessTree {
|
||||||
|
param([int]$parentPid)
|
||||||
|
$children = Get-CimInstance Win32_Process -Filter "ParentProcessId=$parentPid" -ErrorAction SilentlyContinue
|
||||||
|
foreach ($child in $children) {
|
||||||
|
Stop-ProcessTree -parentPid $child.ProcessId
|
||||||
|
}
|
||||||
|
try { Stop-Process -Id $parentPid -Force -ErrorAction SilentlyContinue } catch {}
|
||||||
|
}
|
||||||
|
Stop-ProcessTree -parentPid $jobProcId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Error terminating job process tree: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Safety net: kill any active DISM capture still running
|
||||||
|
try {
|
||||||
|
$dismCaptures = Get-CimInstance Win32_Process -Filter "Name='DISM.EXE'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match '/Capture-FFU' }
|
||||||
|
foreach ($p in $dismCaptures) {
|
||||||
|
try { Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Error stopping DISM capture processes: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also stop Office ODT setup.exe if running (to avoid recreating files after cleanup)
|
||||||
|
try {
|
||||||
|
$officePathForKill = $null
|
||||||
|
|
||||||
|
# Prefer explicit UI path
|
||||||
|
$uiOfficePath = $script:uiState.Controls.txtOfficePath.Text
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($uiOfficePath)) {
|
||||||
|
$officePathForKill = $uiOfficePath
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Fall back to the last config path only if known
|
||||||
|
$lastConfigPathLocal = $script:uiState.Data.lastConfigFilePath
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($lastConfigPathLocal)) {
|
||||||
|
$ffuDevRoot = Split-Path (Split-Path $lastConfigPathLocal -Parent) -Parent
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ffuDevRoot)) {
|
||||||
|
$officePathForKill = Join-Path $ffuDevRoot 'Apps\Office'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only proceed when a valid Office folder exists
|
||||||
|
if ($officePathForKill -and (Test-Path -LiteralPath $officePathForKill -PathType Container)) {
|
||||||
|
$setupProcs = Get-CimInstance Win32_Process -Filter "Name='setup.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.ExecutablePath -like "$officePathForKill*" }
|
||||||
|
foreach ($p in $setupProcs) {
|
||||||
|
try { Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Error stopping Office setup.exe processes: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Remove-Job -Job $jobToStop -Force -ErrorAction SilentlyContinue
|
||||||
|
WriteLog "Background build job stopped and removed."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Error removing background build job: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start cleanup using the same BuildFFUVM.ps1 via -Cleanup short-circuit
|
||||||
|
$lastConfigPath = $script:uiState.Data.lastConfigFilePath
|
||||||
|
if ([string]::IsNullOrWhiteSpace($lastConfigPath)) {
|
||||||
|
WriteLog "No stored config file path found. Cleanup cannot proceed."
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled. No config found for cleanup."
|
||||||
|
$script:uiState.Flags.isBuilding = $false
|
||||||
|
$script:uiState.Flags.isCleanupRunning = $false
|
||||||
|
$btnRun.Content = "Build FFU"
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$ffuDevPath = Split-Path (Split-Path $lastConfigPath -Parent) -Parent
|
||||||
|
$mainLogPath = Join-Path $ffuDevPath "FFUDevelopment.log"
|
||||||
|
|
||||||
|
WriteLog "Starting cleanup without deleting FFUDevelopment.log (will append new entries)."
|
||||||
|
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Cancel in progress... Cleaning environment..."
|
||||||
|
WriteLog "Starting cleanup job (BuildFFUVM.ps1 -Cleanup)."
|
||||||
|
|
||||||
|
# Prepare parameters for cleanup
|
||||||
|
# Inform user: in-progress items will be removed; ask whether to also remove other items downloaded during this run
|
||||||
|
$removeCurrentRunToo = $false
|
||||||
|
$promptText = "Cancel requested.`n`nWe'll remove the download currently in progress to avoid partial/corrupt content.`n`nDo you also want to remove other items downloaded during this run? Previously downloaded items will be kept."
|
||||||
|
$result = [System.Windows.MessageBox]::Show($promptText, "Cancel cleanup options", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
||||||
|
if ($result -eq [System.Windows.MessageBoxResult]::Yes) { $removeCurrentRunToo = $true }
|
||||||
|
|
||||||
|
$cleanupParams = @{
|
||||||
|
ConfigFile = $lastConfigPath
|
||||||
|
Cleanup = $true
|
||||||
|
# Avoid wiping all user content on cancel
|
||||||
|
RemoveApps = $false
|
||||||
|
RemoveUpdates = $false
|
||||||
|
CleanupDrivers = $false
|
||||||
|
# Scoped removal to current run only (optional per user choice)
|
||||||
|
CleanupCurrentRunDownloads = $removeCurrentRunToo
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanupScriptBlock = {
|
||||||
|
param($buildParams, $PSScriptRoot)
|
||||||
|
& "$PSScriptRoot\BuildFFUVM.ps1" @buildParams
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start cleanup job
|
||||||
|
$script:uiState.Data.currentBuildJob = Start-Job -ScriptBlock $cleanupScriptBlock -ArgumentList @($cleanupParams, $PSScriptRoot)
|
||||||
|
|
||||||
|
# Wait for log file to appear (or open immediately if it exists)
|
||||||
|
$logWaitTimeout = 60
|
||||||
|
$watch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
while (-not (Test-Path $mainLogPath) -and $watch.Elapsed.TotalSeconds -lt $logWaitTimeout) {
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
}
|
||||||
|
$watch.Stop()
|
||||||
|
|
||||||
|
# Open log stream for cleanup (tail to end to avoid re-reading the whole file)
|
||||||
|
if (Test-Path $mainLogPath) {
|
||||||
|
$fileStream = [System.IO.File]::Open($mainLogPath, 'Open', 'Read', 'ReadWrite')
|
||||||
|
[void]$fileStream.Seek(0, [System.IO.SeekOrigin]::End)
|
||||||
|
$script:uiState.Data.logStreamReader = [System.IO.StreamReader]::new($fileStream)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Warning: Main log file not found at $mainLogPath after waiting. Monitor tab will not update during cleanup."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a timer to poll the cleanup job
|
||||||
|
$script:uiState.Data.pollTimer = New-Object System.Windows.Threading.DispatcherTimer
|
||||||
|
$script:uiState.Data.pollTimer.Interval = [TimeSpan]::FromSeconds(1)
|
||||||
|
$script:uiState.Flags.isCleanupRunning = $true
|
||||||
|
|
||||||
|
$script:uiState.Data.pollTimer.Add_Tick({
|
||||||
|
param($sender, $e)
|
||||||
|
$currentJob = $script:uiState.Data.currentBuildJob
|
||||||
|
|
||||||
|
# Read new lines from log
|
||||||
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
||||||
|
while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) {
|
||||||
|
$script:uiState.Data.logData.Add($line)
|
||||||
|
if ($script:uiState.Flags.autoScrollLog) {
|
||||||
|
$script:uiState.Controls.lstLogOutput.ScrollIntoView($line)
|
||||||
|
$script:uiState.Controls.lstLogOutput.SelectedIndex = $script:uiState.Controls.lstLogOutput.Items.Count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $currentJob -or $null -eq $script:uiState.Data.pollTimer) {
|
||||||
|
if ($null -ne $sender) { $sender.Stop() }
|
||||||
|
$script:uiState.Data.pollTimer = $null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentJob.State -in 'Completed', 'Failed', 'Stopped') {
|
||||||
|
if ($null -ne $sender) { $sender.Stop() }
|
||||||
|
$script:uiState.Data.pollTimer = $null
|
||||||
|
|
||||||
|
if ($null -ne $script:uiState.Data.logStreamReader) {
|
||||||
|
$lastLine = $null
|
||||||
|
while ($null -ne ($line = $script:uiState.Data.logStreamReader.ReadLine())) {
|
||||||
|
$script:uiState.Data.logData.Add($line)
|
||||||
|
$lastLine = $line
|
||||||
|
}
|
||||||
|
if ($script:uiState.Flags.autoScrollLog -and $null -ne $lastLine) {
|
||||||
|
$script:uiState.Controls.lstLogOutput.ScrollIntoView($lastLine)
|
||||||
|
$script:uiState.Controls.lstLogOutput.SelectedIndex = $script:uiState.Controls.lstLogOutput.Items.Count - 1
|
||||||
|
}
|
||||||
|
$script:uiState.Data.logStreamReader.Close()
|
||||||
|
$script:uiState.Data.logStreamReader.Dispose()
|
||||||
|
$script:uiState.Data.logStreamReader = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled. Environment cleaned."
|
||||||
|
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
||||||
|
$script:uiState.Controls.pbOverallProgress.Value = 0
|
||||||
|
|
||||||
|
# Receive and remove cleanup job
|
||||||
|
$currentJob | Receive-Job -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
Remove-Job -Job $currentJob -Force
|
||||||
|
$script:uiState.Data.currentBuildJob = $null
|
||||||
|
|
||||||
|
# Reset flags and button
|
||||||
|
$script:uiState.Flags.isCleanupRunning = $false
|
||||||
|
$script:uiState.Flags.isBuilding = $false
|
||||||
|
$btn = $script:uiState.Controls.btnRun
|
||||||
|
$btn.Content = "Build FFU"
|
||||||
|
$btn.IsEnabled = $true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$script:uiState.Data.pollTimer.Start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not currently building: start a new build
|
||||||
$btnRun.IsEnabled = $false
|
$btnRun.IsEnabled = $false
|
||||||
|
|
||||||
# Switch to Monitor Tab
|
# Switch to Monitor Tab
|
||||||
@@ -153,6 +394,7 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
$config = Get-UIConfig -State $script:uiState
|
$config = Get-UIConfig -State $script:uiState
|
||||||
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
|
$configFilePath = Join-Path $config.FFUDevelopmentPath "\config\FFUConfig.json"
|
||||||
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8
|
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configFilePath -Encoding UTF8
|
||||||
|
$script:uiState.Data.lastConfigFilePath = $configFilePath
|
||||||
|
|
||||||
if ($config.InstallOffice -and $config.OfficeConfigXMLFile) {
|
if ($config.InstallOffice -and $config.OfficeConfigXMLFile) {
|
||||||
Copy-Item -Path $config.OfficeConfigXMLFile -Destination $config.OfficePath -Force
|
Copy-Item -Path $config.OfficeConfigXMLFile -Destination $config.OfficePath -Force
|
||||||
@@ -283,25 +525,21 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
$script:uiState.Data.logStreamReader = $null
|
$script:uiState.Data.logStreamReader = $null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Determine final status based on job result and whether cleanup was running (should be false here)
|
||||||
$finalStatusText = "FFU build completed successfully."
|
$finalStatusText = "FFU build completed successfully."
|
||||||
if ($currentJob.State -eq 'Failed') {
|
if ($currentJob.State -eq 'Failed') {
|
||||||
$reason = $null
|
$reason = $null
|
||||||
|
|
||||||
# Use Receive-Job with -ErrorVariable to reliably capture the error stream from the job,
|
|
||||||
# as suggested by the research on handling job errors.
|
|
||||||
Receive-Job -Job $currentJob -Keep -ErrorVariable jobErrors -ErrorAction SilentlyContinue | Out-Null
|
Receive-Job -Job $currentJob -Keep -ErrorVariable jobErrors -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
|
||||||
if ($null -ne $jobErrors -and $jobErrors.Count -gt 0) {
|
if ($null -ne $jobErrors -and $jobErrors.Count -gt 0) {
|
||||||
# The terminating error is typically the last one in the stream.
|
|
||||||
$reason = ($jobErrors | Select-Object -Last 1).ToString()
|
$reason = ($jobErrors | Select-Object -Last 1).ToString()
|
||||||
}
|
}
|
||||||
|
|
||||||
# If Receive-Job didn't surface an error, fall back to the JobStateInfo.Reason property.
|
|
||||||
if ([string]::IsNullOrWhiteSpace($reason) -and $currentJob.JobStateInfo.Reason) {
|
if ([string]::IsNullOrWhiteSpace($reason) -and $currentJob.JobStateInfo.Reason) {
|
||||||
$reason = $currentJob.JobStateInfo.Reason.Message
|
$reason = $currentJob.JobStateInfo.Reason.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
# Final fallback if no specific reason can be found.
|
|
||||||
if ([string]::IsNullOrWhiteSpace($reason)) {
|
if ([string]::IsNullOrWhiteSpace($reason)) {
|
||||||
$reason = "An unknown error occurred. The job failed without a specific reason."
|
$reason = "An unknown error occurred. The job failed without a specific reason."
|
||||||
}
|
}
|
||||||
@@ -318,19 +556,27 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
|
|
||||||
# Update UI elements
|
# Update UI elements
|
||||||
$script:uiState.Controls.txtStatus.Text = $finalStatusText
|
$script:uiState.Controls.txtStatus.Text = $finalStatusText
|
||||||
$script:uiState.Controls.btnRun.IsEnabled = $true
|
|
||||||
|
|
||||||
# Clean up the job object
|
# Receive & remove job and clear state
|
||||||
$currentJob | Receive-Job -ErrorAction SilentlyContinue | Out-Null
|
$currentJob | Receive-Job -ErrorAction SilentlyContinue | Out-Null
|
||||||
Remove-Job -Job $currentJob -Force
|
Remove-Job -Job $currentJob -Force
|
||||||
|
|
||||||
# Clear the job from the state
|
|
||||||
$script:uiState.Data.currentBuildJob = $null
|
$script:uiState.Data.currentBuildJob = $null
|
||||||
|
|
||||||
|
# Reset button and flags for next run
|
||||||
|
$script:uiState.Flags.isBuilding = $false
|
||||||
|
$script:uiState.Flags.isCleanupRunning = $false
|
||||||
|
$script:uiState.Controls.btnRun.Content = "Build FFU"
|
||||||
|
$script:uiState.Controls.btnRun.IsEnabled = $true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Start the timer
|
# Start the timer
|
||||||
$script:uiState.Data.pollTimer.Start()
|
$script:uiState.Data.pollTimer.Start()
|
||||||
|
|
||||||
|
# Mark building and toggle button to Cancel
|
||||||
|
$script:uiState.Flags.isBuilding = $true
|
||||||
|
$btnRun.Content = "Cancel"
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# This catch block handles errors during the setup of the job (e.g., Get-UIConfig fails)
|
# This catch block handles errors during the setup of the job (e.g., Get-UIConfig fails)
|
||||||
@@ -350,6 +596,9 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
$script:uiState.Controls.pbOverallProgress.Visibility = 'Collapsed'
|
||||||
if ($null -ne $script:uiState.Controls.btnRun) {
|
if ($null -ne $script:uiState.Controls.btnRun) {
|
||||||
$script:uiState.Controls.btnRun.IsEnabled = $true
|
$script:uiState.Controls.btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.btnRun.Content = "Build FFU"
|
||||||
|
$script:uiState.Flags.isBuilding = $false
|
||||||
|
$script:uiState.Flags.isCleanupRunning = $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -221,7 +221,6 @@
|
|||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Margin="0,5,0,0">
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Margin="0,5,0,0">
|
||||||
<StackPanel x:Name="stackFeaturesContainer" Margin="15,5">
|
<StackPanel x:Name="stackFeaturesContainer" Margin="15,5">
|
||||||
<TextBlock Text="Selected features (semicolon):" Margin="0,10,0,5" FontStyle="Italic"/>
|
<TextBlock Text="Selected features (semicolon):" Margin="0,10,0,5" FontStyle="Italic"/>
|
||||||
<TextBox x:Name="txtOptionalFeatures" IsReadOnly="True" Width="350" Margin="0,0,0,10" ToolTip="Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP)."/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Expander>
|
</Expander>
|
||||||
@@ -373,6 +372,13 @@
|
|||||||
<Button x:Name="btnBrowseAppSource" Grid.Column="1" Content="Browse..." Width="80" Margin="5,0,0,0" VerticalAlignment="Center"/>
|
<Button x:Name="btnBrowseAppSource" Grid.Column="1" Content="Browse..." Width="80" Margin="5,0,0,0" VerticalAlignment="Center"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Additional Exit Codes -->
|
||||||
|
<TextBlock Text="Additional Exit Codes:" Margin="0,0,0,5"/>
|
||||||
|
<TextBox x:Name="txtAppAdditionalExitCodes" Margin="0,0,0,10" ToolTip="Enter a comma-separated list of additional success exit codes."/>
|
||||||
|
|
||||||
|
<!-- Ignore Non-Zero Exit Codes Checkbox -->
|
||||||
|
<CheckBox x:Name="chkIgnoreExitCodes" Content="Ignore all non-zero exit codes" Margin="0,0,0,10" ToolTip="If checked, any non-zero exit code will be considered a success."/>
|
||||||
|
|
||||||
<!-- Add Application Button -->
|
<!-- Add Application Button -->
|
||||||
<Button x:Name="btnAddApplication" Content="Add Application" Width="120" HorizontalAlignment="Left" Margin="0,10,0,10" Padding="10,5" ToolTip="Add the application to the list"/>
|
<Button x:Name="btnAddApplication" Content="Add Application" Width="120" HorizontalAlignment="Left" Margin="0,10,0,10" Padding="10,5" ToolTip="Add the application to the list"/>
|
||||||
|
|
||||||
@@ -385,25 +391,6 @@
|
|||||||
|
|
||||||
<!-- Applications ListView -->
|
<!-- Applications ListView -->
|
||||||
<ListView x:Name="lstApplications" Grid.Column="0" Height="200">
|
<ListView x:Name="lstApplications" Grid.Column="0" Height="200">
|
||||||
<ListView.View>
|
|
||||||
<GridView>
|
|
||||||
<GridViewColumn Header="Priority" DisplayMemberBinding="{Binding Priority}" Width="60"/>
|
|
||||||
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="150"/>
|
|
||||||
<GridViewColumn Header="Command Line" DisplayMemberBinding="{Binding CommandLine}" Width="200"/>
|
|
||||||
<GridViewColumn Header="Arguments" DisplayMemberBinding="{Binding Arguments}" Width="200"/>
|
|
||||||
<GridViewColumn Header="Source" DisplayMemberBinding="{Binding Source}" Width="150"/>
|
|
||||||
<GridViewColumn Header="Copy Status" DisplayMemberBinding="{Binding CopyStatus}" Width="150"/>
|
|
||||||
<GridViewColumn Header="Action" Width="85">
|
|
||||||
<GridViewColumn.CellTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Grid HorizontalAlignment="Stretch">
|
|
||||||
<Button Content="Remove" Tag="{Binding Priority}" Width="70" HorizontalAlignment="Center" ToolTip="Remove this application from the list and reorder priorities"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</GridViewColumn.CellTemplate>
|
|
||||||
</GridViewColumn>
|
|
||||||
</GridView>
|
|
||||||
</ListView.View>
|
|
||||||
</ListView>
|
</ListView>
|
||||||
|
|
||||||
<!-- Reorder Buttons -->
|
<!-- Reorder Buttons -->
|
||||||
@@ -420,7 +407,9 @@
|
|||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,0,0,10">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,0,0,10">
|
||||||
<Button x:Name="btnSaveBYOApplications" Content="Save UserAppList.json" Margin="0,0,10,0" Padding="10,5" ToolTip="Save application list to JSON file"/>
|
<Button x:Name="btnSaveBYOApplications" Content="Save UserAppList.json" Margin="0,0,10,0" Padding="10,5" ToolTip="Save application list to JSON file"/>
|
||||||
<Button x:Name="btnLoadBYOApplications" Content="Import UserAppList.json" Margin="0,0,10,0" Padding="10,5" ToolTip="Import application list from JSON file"/>
|
<Button x:Name="btnLoadBYOApplications" Content="Import UserAppList.json" Margin="0,0,10,0" Padding="10,5" ToolTip="Import application list from JSON file"/>
|
||||||
|
<Button x:Name="btnEditApplication" Content="Edit Application" IsEnabled="False" Margin="0,0,10,0" Padding="10,5" ToolTip="Edit the selected application's details"/>
|
||||||
<Button x:Name="btnCopyBYOApps" Content="Copy Apps" IsEnabled="False" Margin="0,0,10,0" Padding="10,5" ToolTip="Copy applications with a specified source path to the AppsPath\Win32 folder"/>
|
<Button x:Name="btnCopyBYOApps" Content="Copy Apps" IsEnabled="False" Margin="0,0,10,0" Padding="10,5" ToolTip="Copy applications with a specified source path to the AppsPath\Win32 folder"/>
|
||||||
|
<Button x:Name="btnRemoveSelectedBYOApps" Content="Remove Selected" IsEnabled="False" Margin="0,0,10,0" Padding="10,5" ToolTip="Remove selected applications from the list"/>
|
||||||
<Button x:Name="btnClearBYOApplications" Content="Clear List" Padding="10,5" ToolTip="Clear all applications from the list"/>
|
<Button x:Name="btnClearBYOApplications" Content="Clear List" Padding="10,5" ToolTip="Clear all applications from the list"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -751,6 +740,7 @@
|
|||||||
<CheckBox x:Name="chkAllowVHDXCaching" Content="Allow VHDX Caching" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will cache the VHDX file to cache folder and create a config json file to track Windows build information."/>
|
<CheckBox x:Name="chkAllowVHDXCaching" Content="Allow VHDX Caching" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will cache the VHDX file to cache folder and create a config json file to track Windows build information."/>
|
||||||
<CheckBox x:Name="chkCreateCaptureMedia" Content="Create Capture Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE capture media for use when InstallApps is set to $true."/>
|
<CheckBox x:Name="chkCreateCaptureMedia" Content="Create Capture Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE capture media for use when InstallApps is set to $true."/>
|
||||||
<CheckBox x:Name="chkCreateDeploymentMedia" Content="Create Deployment Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE deployment media for use when deploying to a physical device."/>
|
<CheckBox x:Name="chkCreateDeploymentMedia" Content="Create Deployment Media" Margin="5" VerticalAlignment="Center" Tag="When set to $true, this will create WinPE deployment media for use when deploying to a physical device."/>
|
||||||
|
<CheckBox x:Name="chkInjectUnattend" Content="Inject Unattend.xml" Margin="5" VerticalAlignment="Center" Tag="When set to $true and Install Apps is enabled, copies unattend_[arch].xml from $FFUDevelopmentPath\unattend into Apps\Unattend\Unattend.xml to be used by sysprep."/>
|
||||||
<CheckBox x:Name="chkVerbose" Content="Verbose" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/>
|
<CheckBox x:Name="chkVerbose" Content="Verbose" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will enable write-verbose output to the console for the build script."/>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
|
|
||||||
@@ -766,6 +756,12 @@
|
|||||||
<CheckBox x:Name="chkCopyUnattend" Content="Copy Unattend.xml" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Unattend.xml file to the USB drive."/>
|
<CheckBox x:Name="chkCopyUnattend" Content="Copy Unattend.xml" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Unattend.xml file to the USB drive."/>
|
||||||
<CheckBox x:Name="chkCopyPPKG" Content="Copy Provisioning Package" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the provisioning package to the USB drive."/>
|
<CheckBox x:Name="chkCopyPPKG" Content="Copy Provisioning Package" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the provisioning package to the USB drive."/>
|
||||||
|
|
||||||
|
<!-- Max USB Drives -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="5">
|
||||||
|
<TextBlock Text="Max USB Drives" VerticalAlignment="Center" ToolTip="Maximum number of USB drives to build at once. Enter 0 to process all discovered (or all selected) drives."/>
|
||||||
|
<TextBox x:Name="txtMaxUSBDrives" Width="50" Margin="10,0,0,0" Text="5" VerticalAlignment="Center" ToolTip="Maximum number of USB drives to build at once. Enter 0 to process all discovered (or all selected) drives."/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- USB Drive Selection Section -->
|
<!-- USB Drive Selection Section -->
|
||||||
<Grid x:Name="usbDriveSelectionPanel" Margin="5,0,0,0">
|
<Grid x:Name="usbDriveSelectionPanel" Margin="5,0,0,0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "Start & Initialization"
|
||||||
|
A[Start] --> B{Load ConfigFile?};
|
||||||
|
B --> C[Process Parameters];
|
||||||
|
C --> D{Validate Parameters};
|
||||||
|
D --> E{"dirty.txt exists?"};
|
||||||
|
E -- Yes --> F[Run Cleanup Routine];
|
||||||
|
F --> G["Create new dirty.txt"];
|
||||||
|
E -- No --> G;
|
||||||
|
end
|
||||||
|
|
||||||
|
G --> H{"-InstallDrivers or -CopyDrivers?"};
|
||||||
|
|
||||||
|
subgraph "Pre-Build Preparations"
|
||||||
|
H -- Yes --> I{Driver Source?};
|
||||||
|
I -- "-DriversJsonPath" --> J[Download Drivers via JSON in Parallel];
|
||||||
|
I -- "-Make and -Model" --> K[Download Drivers for specific Make/Model];
|
||||||
|
I -- "Local Folder" --> L[Use Existing Drivers in Drivers Folder];
|
||||||
|
|
||||||
|
subgraph "ADK & WinPE"
|
||||||
|
M[Check for ADK & WinPE Add-on];
|
||||||
|
M --> N{Latest Version Installed?};
|
||||||
|
N -- No --> O[Uninstall Old & Install Latest ADK/WinPE];
|
||||||
|
N -- Yes --> P[Get ADK Path];
|
||||||
|
O --> P;
|
||||||
|
end
|
||||||
|
|
||||||
|
Q{"-InstallApps?"};
|
||||||
|
subgraph "Application & In-VM Content Preparation"
|
||||||
|
direction LR
|
||||||
|
R[Check for existing downloaded apps];
|
||||||
|
R --> S{Download missing WinGet apps};
|
||||||
|
S --> T{"-InstallOffice?"};
|
||||||
|
T -- Yes --> U[Download ODT & Office content];
|
||||||
|
T -- No --> V[Continue];
|
||||||
|
U --> V;
|
||||||
|
V --> W["Download in-VM updates: Defender, MSRT, etc."];
|
||||||
|
W --> X["Create Apps.iso"];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
J --> M;
|
||||||
|
K --> M;
|
||||||
|
L --> M;
|
||||||
|
H -- No --> M;
|
||||||
|
P --> Q;
|
||||||
|
Q -- Yes --> R;
|
||||||
|
X --> Y;
|
||||||
|
Q -- No --> Y{"-AllowVHDXCaching?"};
|
||||||
|
|
||||||
|
subgraph "VHDX Management"
|
||||||
|
Y -- Yes --> Z[Check for matching cached VHDX];
|
||||||
|
Z --> AA{Cache Hit?};
|
||||||
|
AA -- Yes --> AB[Use Cached VHDX];
|
||||||
|
AA -- No --> AC[Create New VHDX];
|
||||||
|
Y -- No --> AC;
|
||||||
|
|
||||||
|
subgraph "VHDX Creation Workflow"
|
||||||
|
AC --> AD{ISOPath provided?};
|
||||||
|
AD -- No --> AE[Download Windows ESD media];
|
||||||
|
AD -- Yes --> AF[Use provided ISO];
|
||||||
|
AE --> AG[Create & Partition VHDX];
|
||||||
|
AF --> AG;
|
||||||
|
AG --> AH[Apply Base Windows Image to VHDX];
|
||||||
|
AH --> AI{"Updates specified? (CU, dotNET, etc.)"};
|
||||||
|
AI -- Yes --> AJ[Apply Updates to Offline VHDX];
|
||||||
|
AJ --> AK[Run Component Cleanup];
|
||||||
|
AI -- No --> AK;
|
||||||
|
AK --> AL{"Optional Features specified?"};
|
||||||
|
AL -- Yes --> AM[Enable Optional Features];
|
||||||
|
AL -- No --> AN[Finalize VHDX Setup];
|
||||||
|
AM --> AN;
|
||||||
|
AN --> AO{"-AllowVHDXCaching?"};
|
||||||
|
AO -- Yes --> AP[Optimize and Copy VHDX to Cache];
|
||||||
|
AO -- No --> AQ[Continue];
|
||||||
|
AP --> AQ;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AB --> BA;
|
||||||
|
AQ --> BA{"-InstallApps?"};
|
||||||
|
|
||||||
|
subgraph "FFU Creation"
|
||||||
|
subgraph "VM-Based Capture (-InstallApps)"
|
||||||
|
direction LR
|
||||||
|
BB[Create Hyper-V VM from VHDX];
|
||||||
|
BB --> BC["Create WinPE Capture Media iso"];
|
||||||
|
BC --> BD[Configure network share for capture];
|
||||||
|
BD --> BE["Start VM: Boots to Audit Mode"];
|
||||||
|
BE --> BF[Orchestrator runs: Installs apps, syspreps, shuts down];
|
||||||
|
BF --> BG[VM reboots from Capture Media];
|
||||||
|
BG --> BH["CaptureFFU.ps1 runs, saves FFU to share, shuts down"];
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Direct VHDX Capture"
|
||||||
|
BI[Capture FFU directly from VHDX using DISM];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
BA -- Yes --> BB;
|
||||||
|
BA -- No --> BI;
|
||||||
|
|
||||||
|
subgraph "Post-Processing & Media Creation"
|
||||||
|
BK{"-InstallDrivers?"};
|
||||||
|
BK -- Yes --> BL[Mount FFU & Inject Drivers];
|
||||||
|
BK -- No --> BM[Continue];
|
||||||
|
BL --> BM;
|
||||||
|
BM --> BN{"-Optimize?"};
|
||||||
|
BN -- Yes --> BO[Optimize FFU using DISM];
|
||||||
|
BN -- No --> BP[Continue];
|
||||||
|
BO --> BP;
|
||||||
|
BP --> BQ{"-BuildUSBDrive?"};
|
||||||
|
BQ -- Yes --> BR[Create WinPE Deployment Media];
|
||||||
|
BR --> BS["Partition USB Drive(s)"];
|
||||||
|
BS --> BT[Copy FFU, Deploy scripts & other assets to USB];
|
||||||
|
BQ -- No --> BU[Continue];
|
||||||
|
BT --> BU;
|
||||||
|
end
|
||||||
|
|
||||||
|
BH --> BK;
|
||||||
|
BI --> BK;
|
||||||
|
|
||||||
|
subgraph "Final Cleanup"
|
||||||
|
BU --> BV[Cleanup VM, VHDX, temp files];
|
||||||
|
BV --> BW["Remove dirty.txt"];
|
||||||
|
BW --> BX[End];
|
||||||
|
end
|
||||||
@@ -163,6 +163,7 @@ function Invoke-ParallelProcessing {
|
|||||||
AppsPath = $localJobArgs['AppsPath']
|
AppsPath = $localJobArgs['AppsPath']
|
||||||
OrchestrationPath = $localJobArgs['OrchestrationPath']
|
OrchestrationPath = $localJobArgs['OrchestrationPath']
|
||||||
ProgressQueue = $localProgressQueue
|
ProgressQueue = $localProgressQueue
|
||||||
|
WindowsArch = $localJobArgs['WindowsArch']
|
||||||
}
|
}
|
||||||
$taskResult = Start-WingetAppDownloadTask @wingetTaskArgs
|
$taskResult = Start-WingetAppDownloadTask @wingetTaskArgs
|
||||||
if ($null -ne $taskResult) {
|
if ($null -ne $taskResult) {
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ function Get-Application {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$AppsPath,
|
[string]$AppsPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ApplicationArch,
|
||||||
[string]$WindowsArch,
|
[string]$WindowsArch,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$OrchestrationPath
|
[string]$OrchestrationPath,
|
||||||
|
[switch]$SkipWin32Json
|
||||||
)
|
)
|
||||||
|
|
||||||
# Block Company Portal from winget source
|
# Block Company Portal from winget source
|
||||||
@@ -48,6 +50,29 @@ function Get-Application {
|
|||||||
# Check if the folder is not empty.
|
# Check if the folder is not empty.
|
||||||
if (Get-ChildItem -Path $appBaseFolderPathForCheck -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1) {
|
if (Get-ChildItem -Path $appBaseFolderPathForCheck -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1) {
|
||||||
WriteLog "Application '$AppName' appears to be already downloaded as content exists in '$appBaseFolderPathForCheck'. Skipping download."
|
WriteLog "Application '$AppName' appears to be already downloaded as content exists in '$appBaseFolderPathForCheck'. Skipping download."
|
||||||
|
|
||||||
|
# Add silent install command(s) only if not skipping JSON generation (build-time scenario)
|
||||||
|
$appIsWin32Existing = ($Source -eq 'winget' -or ($Source -eq 'msstore' -and $AppId.StartsWith('XP')))
|
||||||
|
if ($appIsWin32Existing -and -not $SkipWin32Json) {
|
||||||
|
$win32BasePath = Join-Path -Path "$AppsPath\Win32" -ChildPath $AppName
|
||||||
|
if (Test-Path -Path $win32BasePath -PathType Container) {
|
||||||
|
$archFolders = Get-ChildItem -Path $win32BasePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @('x86', 'x64', 'arm64') }
|
||||||
|
if ($archFolders) {
|
||||||
|
foreach ($archFolder in $archFolders) {
|
||||||
|
WriteLog "Adding silent install command for pre-downloaded $AppName ($($archFolder.Name)) to $OrchestrationPath\WinGetWin32Apps.json"
|
||||||
|
Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $archFolder.FullName -OrchestrationPath $OrchestrationPath -SubFolder $archFolder.Name | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Adding silent install command for pre-downloaded $AppName to $OrchestrationPath\WinGetWin32Apps.json"
|
||||||
|
Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $win32BasePath -OrchestrationPath $OrchestrationPath | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($appIsWin32Existing -and $SkipWin32Json) {
|
||||||
|
WriteLog "Skipping WinGetWin32Apps.json regeneration for pre-downloaded $AppName (UI mode)."
|
||||||
|
}
|
||||||
|
|
||||||
return 0 # Success, already present
|
return 0 # Success, already present
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +89,8 @@ function Get-Application {
|
|||||||
return 1 # Return error code
|
return 1 # Return error code
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine architectures to download
|
# Determine architectures to download (ApplicationArch controls download set; WindowsArch (optional) used later for pruning store installers)
|
||||||
$architecturesToDownload = if ($WindowsArch -eq 'x86 x64') { @('x86', 'x64') } else { @($WindowsArch) }
|
$architecturesToDownload = if ($ApplicationArch -eq 'x86 x64') { @('x86', 'x64') } else { @($ApplicationArch) }
|
||||||
$overallResult = 0
|
$overallResult = 0
|
||||||
|
|
||||||
# For msstore, we don't specify architecture, so we only need to loop once.
|
# For msstore, we don't specify architecture, so we only need to loop once.
|
||||||
@@ -196,8 +221,14 @@ function Get-Application {
|
|||||||
}
|
}
|
||||||
# If app is in Win32 folder, add the silent install command to the WinGetWin32Apps.json file
|
# If app is in Win32 folder, add the silent install command to the WinGetWin32Apps.json file
|
||||||
elseif ($appFolderPath -match 'Win32') {
|
elseif ($appFolderPath -match 'Win32') {
|
||||||
WriteLog "$AppName is a Win32 app. Adding silent install command to $OrchestrationPath\WinGetWin32Apps.json"
|
if (-not $SkipWin32Json) {
|
||||||
$result = Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $appFolderPath -OrchestrationPath $OrchestrationPath -SubFolder $subFolderForCommand
|
WriteLog "$AppName is a Win32 app. Adding silent install command to $OrchestrationPath\WinGetWin32Apps.json"
|
||||||
|
$result = Add-Win32SilentInstallCommand -AppFolder $AppName -AppFolderPath $appFolderPath -OrchestrationPath $OrchestrationPath -SubFolder $subFolderForCommand
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "$AppName is a Win32 app. Skipping WinGetWin32Apps.json generation (UI mode)."
|
||||||
|
$result = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# For any other case, set result to 0 (success)
|
# For any other case, set result to 0 (success)
|
||||||
@@ -222,26 +253,84 @@ function Get-Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clean up multiple versions (keep only the latest)
|
# Clean up multiple versions honoring WindowsArch (pruning target; keep only one installer)
|
||||||
WriteLog "$AppName has completed downloading. Identifying the latest version of $AppName."
|
WriteLog "$AppName has completed downloading. Evaluating installer set for pruning."
|
||||||
$packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop
|
$packages = Get-ChildItem -Path "$appFolderPath\*" -Exclude "Dependencies\*", "*.xml", "*.yaml" -File -ErrorAction Stop
|
||||||
|
if ($packages.Count -gt 1 -and $WindowsArch) {
|
||||||
# Find latest version based on signature date
|
WriteLog "WindowsArch pruning target provided: $WindowsArch"
|
||||||
$latestPackage = $packages | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1
|
# Detect universal bundles (contain x86,x64,arm64 in name)
|
||||||
|
$universalCandidates = $packages | Where-Object {
|
||||||
# Remove older versions
|
$base = $_.BaseName
|
||||||
WriteLog "Latest version of $AppName has been identified as $latestPackage. Removing old versions of $AppName that may have downloaded."
|
# Split base name into tokens to avoid partial matches (e.g. arm inside arm64)
|
||||||
foreach ($package in $packages) {
|
$tokens = ($base -split '[\.\-_]') | ForEach-Object { $_.ToLower() }
|
||||||
if ($package.FullName -ne $latestPackage.FullName) {
|
# Architecture tokens we recognize
|
||||||
try {
|
$archTokens = @('x86', 'x64', 'arm', 'arm64')
|
||||||
WriteLog "Removing $($package.FullName)"
|
# Distinct matched architecture tokens
|
||||||
Remove-Item -Path $package.FullName -Force
|
$matched = $tokens | Where-Object { $_ -in $archTokens } | Select-Object -Unique
|
||||||
|
if ($matched.Count -ge 2) {
|
||||||
|
WriteLog "Multi-architecture bundle detected: $base (tokens: $($matched -join ', '))"
|
||||||
|
$true
|
||||||
}
|
}
|
||||||
catch {
|
else {
|
||||||
WriteLog "Failed to delete: $($package.FullName) - $_"
|
$false
|
||||||
throw $_
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($universalCandidates) {
|
||||||
|
WriteLog "Universal bundle candidate(s) detected: $($universalCandidates.Name -join ', ')"
|
||||||
|
$candidateSet = $universalCandidates
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$archToken = switch -Regex ($WindowsArch.ToLower()) {
|
||||||
|
'^x64$' { 'x64' ; break }
|
||||||
|
'^x86$' { 'x86' ; break }
|
||||||
|
'^arm64$' { 'arm64' ; break }
|
||||||
|
default { $WindowsArch.ToLower() }
|
||||||
|
}
|
||||||
|
$archMatches = $packages | Where-Object { $_.BaseName -match "(?i)$archToken" }
|
||||||
|
if ($archMatches) {
|
||||||
|
WriteLog "Architecture-specific candidates matching '$archToken': $($archMatches.Name -join ', ')"
|
||||||
|
$candidateSet = $archMatches
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "No installer filename matched '$archToken'. Falling back to all installers."
|
||||||
|
$candidateSet = $packages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# From candidate set, choose latest by signature date
|
||||||
|
$latestPackage = $candidateSet | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1
|
||||||
|
WriteLog "Retaining installer: $($latestPackage.Name)"
|
||||||
|
foreach ($package in $packages) {
|
||||||
|
if ($package.FullName -ne $latestPackage.FullName) {
|
||||||
|
try {
|
||||||
|
WriteLog "Removing $($package.FullName)"
|
||||||
|
Remove-Item -Path $package.FullName -Force
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Failed to delete: $($package.FullName) - $_"
|
||||||
|
throw $_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($packages.Count -gt 1) {
|
||||||
|
WriteLog "Multiple installers present but no WindowsArch pruning target supplied. Using original latest-version logic."
|
||||||
|
$latestPackage = $packages | Sort-Object { (Get-AuthenticodeSignature $_.FullName).SignerCertificate.NotBefore } -Descending | Select-Object -First 1
|
||||||
|
WriteLog "Retaining latest by signature date: $($latestPackage.Name)"
|
||||||
|
foreach ($package in $packages) {
|
||||||
|
if ($package.FullName -ne $latestPackage.FullName) {
|
||||||
|
try {
|
||||||
|
WriteLog "Removing $($package.FullName)"
|
||||||
|
Remove-Item -Path $package.FullName -Force
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Failed to delete: $($package.FullName) - $_"
|
||||||
|
throw $_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Single installer present; no pruning required."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} # End foreach ($arch in $architecturesToDownload)
|
} # End foreach ($arch in $architecturesToDownload)
|
||||||
@@ -300,7 +389,7 @@ function Get-Apps {
|
|||||||
foreach ($wingetApp in $wingetApps) {
|
foreach ($wingetApp in $wingetApps) {
|
||||||
try {
|
try {
|
||||||
$appArch = if ($wingetApp.PSObject.Properties['architecture']) { $wingetApp.architecture } else { $WindowsArch }
|
$appArch = if ($wingetApp.PSObject.Properties['architecture']) { $wingetApp.architecture } else { $WindowsArch }
|
||||||
Get-Application -AppName $wingetApp.Name -AppId $wingetApp.Id -Source 'winget' -AppsPath $AppsPath -WindowsArch $appArch -OrchestrationPath $OrchestrationPath
|
Get-Application -AppName $wingetApp.Name -AppId $wingetApp.Id -Source 'winget' -AppsPath $AppsPath -ApplicationArch $appArch -OrchestrationPath $OrchestrationPath
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "Error occurred while processing $($wingetApp.Name): $_"
|
WriteLog "Error occurred while processing $($wingetApp.Name): $_"
|
||||||
@@ -318,7 +407,7 @@ function Get-Apps {
|
|||||||
foreach ($storeApp in $StoreApps) {
|
foreach ($storeApp in $StoreApps) {
|
||||||
try {
|
try {
|
||||||
$appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch }
|
$appArch = if ($storeApp.PSObject.Properties['architecture']) { $storeApp.architecture } else { $WindowsArch }
|
||||||
Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -WindowsArch $appArch -OrchestrationPath $OrchestrationPath
|
Get-Application -AppName $storeApp.Name -AppId $storeApp.Id -Source 'msstore' -AppsPath $AppsPath -ApplicationArch $appArch -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "Error occurred while processing $($storeApp.Name): $_"
|
WriteLog "Error occurred while processing $($storeApp.Name): $_"
|
||||||
@@ -326,6 +415,62 @@ function Get-Apps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Post-processing: Override CommandLine / Arguments from AppList.json if provided
|
||||||
|
# Users may supply custom silent install commands or arguments. These optional
|
||||||
|
# properties (CommandLine, Arguments) in AppList.json replace the auto-generated
|
||||||
|
# values in WinGetWin32Apps.json. Keyed by Name.
|
||||||
|
try {
|
||||||
|
$overrideMap = @{}
|
||||||
|
foreach ($app in $apps.apps) {
|
||||||
|
if ($app.source -in @('winget', 'msstore')) {
|
||||||
|
$hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine))
|
||||||
|
$hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments))
|
||||||
|
if ($hasCmd -or $hasArgs) {
|
||||||
|
$overrideMap[$app.name] = @{
|
||||||
|
CommandLine = if ($hasCmd) { $app.CommandLine } else { $null }
|
||||||
|
Arguments = if ($hasArgs) { $app.Arguments } else { $null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($overrideMap.Count -gt 0) {
|
||||||
|
$winGetWin32Path = Join-Path -Path $OrchestrationPath -ChildPath 'WinGetWin32Apps.json'
|
||||||
|
if (Test-Path -Path $winGetWin32Path) {
|
||||||
|
[array]$appsDataUpdated = Get-Content -Path $winGetWin32Path -Raw | ConvertFrom-Json
|
||||||
|
$changed = $false
|
||||||
|
foreach ($entry in $appsDataUpdated) {
|
||||||
|
if ($overrideMap.ContainsKey($entry.Name)) {
|
||||||
|
$ov = $overrideMap[$entry.Name]
|
||||||
|
if ($ov.CommandLine) {
|
||||||
|
WriteLog "Override (AppList.json) CommandLine for $($entry.Name)"
|
||||||
|
$entry.CommandLine = $ov.CommandLine
|
||||||
|
$changed = $true
|
||||||
|
}
|
||||||
|
if ($ov.Arguments) {
|
||||||
|
WriteLog "Override (AppList.json) Arguments for $($entry.Name)"
|
||||||
|
$entry.Arguments = $ov.Arguments
|
||||||
|
$changed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($changed) {
|
||||||
|
$appsDataUpdated | ConvertTo-Json -Depth 10 | Set-Content -Path $winGetWin32Path
|
||||||
|
WriteLog "Applied AppList.json command overrides to WinGetWin32Apps.json"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "No matching apps required command overrides."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "WinGetWin32Apps.json not found; no overrides applied."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Failed to apply AppList.json command overrides: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function Install-WinGet {
|
function Install-WinGet {
|
||||||
param (
|
param (
|
||||||
@@ -409,33 +554,144 @@ function Add-Win32SilentInstallCommand {
|
|||||||
[string]$SubFolder
|
[string]$SubFolder
|
||||||
)
|
)
|
||||||
$appName = $AppFolder
|
$appName = $AppFolder
|
||||||
$installerPath = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction Stop
|
|
||||||
if (-not $installerPath) {
|
# Discover installer candidates (top-level files as before)
|
||||||
|
$installerCandidates = Get-ChildItem -Path "$appFolderPath\*" -Include "*.exe", "*.msi" -File -ErrorAction SilentlyContinue
|
||||||
|
if (-not $installerCandidates) {
|
||||||
WriteLog "No win32 app installers were found. Skipping the inclusion of $AppFolder"
|
WriteLog "No win32 app installers were found. Skipping the inclusion of $AppFolder"
|
||||||
Remove-Item -Path $AppFolderPath -Recurse -Force
|
Remove-Item -Path $AppFolderPath -Recurse -Force
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Read the exported WinGet YAML
|
||||||
$yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop
|
$yamlFile = Get-ChildItem -Path "$appFolderPath\*" -Include "*.yaml" -File -ErrorAction Stop
|
||||||
$yamlContent = Get-Content -Path $yamlFile -Raw
|
$yamlText = Get-Content -Path $yamlFile -Raw
|
||||||
$silentInstallSwitch = [regex]::Match($yamlContent, 'Silent:\s*(.+)').Groups[1].Value.Replace("'", "").Trim()
|
|
||||||
|
# Attempt to resolve the correct installer from YAML NestedInstallerFiles within the matching Architecture block
|
||||||
|
$desiredArch = if (-not [string]::IsNullOrEmpty($SubFolder)) { $SubFolder } else { $null }
|
||||||
|
$relativeFromYaml = $null
|
||||||
|
$blockSilent = $null
|
||||||
|
|
||||||
|
$regexOptions = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
|
||||||
|
$pattern = '-\s+Architecture:\s*(?<arch>\S+)[\s\S]*?NestedInstallerFiles:\s*-\s*RelativeFilePath:\s*(?<path>.+?)\r?\n'
|
||||||
|
$yamlMatches = [regex]::Matches($yamlText, $pattern, $regexOptions)
|
||||||
|
|
||||||
|
$selectedMatch = $null
|
||||||
|
if ($yamlMatches.Count -gt 0) {
|
||||||
|
if ($desiredArch) {
|
||||||
|
foreach ($m in $yamlMatches) {
|
||||||
|
if ($m.Groups['arch'].Value -ieq $desiredArch) {
|
||||||
|
$selectedMatch = $m
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $selectedMatch) {
|
||||||
|
$selectedMatch = $yamlMatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
$pathValue = $selectedMatch.Groups['path'].Value.Trim()
|
||||||
|
$pathValue = $pathValue.Trim("'").Trim('"')
|
||||||
|
$relativeFromYaml = $pathValue
|
||||||
|
|
||||||
|
# Extract a Silent switch from within the same installer block if present
|
||||||
|
$startIndex = $selectedMatch.Index
|
||||||
|
$nextIndex = -1
|
||||||
|
for ($i = 0; $i -lt $yamlMatches.Count; $i++) {
|
||||||
|
if ($yamlMatches[$i].Index -gt $startIndex) {
|
||||||
|
$nextIndex = $yamlMatches[$i].Index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($nextIndex -gt -1) {
|
||||||
|
$blockText = $yamlText.Substring($startIndex, $nextIndex - $startIndex)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$blockText = $yamlText.Substring($startIndex)
|
||||||
|
}
|
||||||
|
$blockSilentMatch = [regex]::Match($blockText, 'InstallerSwitches:[\s\S]*?Silent:\s*(.+?)\r?\n', $regexOptions)
|
||||||
|
if ($blockSilentMatch.Success) {
|
||||||
|
$blockSilent = $blockSilentMatch.Groups[1].Value.Trim().Trim("'").Trim('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve Silent switch (prefer block-level, fallback to first Silent in file)
|
||||||
|
$silentInstallSwitch = $blockSilent
|
||||||
|
if ([string]::IsNullOrEmpty($silentInstallSwitch)) {
|
||||||
|
$globalSilentMatch = [regex]::Match($yamlText, 'Silent:\s*(.+)', $regexOptions)
|
||||||
|
$silentInstallSwitch = $globalSilentMatch.Groups[1].Value.Trim().Trim("'").Trim('"')
|
||||||
|
}
|
||||||
if (-not $silentInstallSwitch) {
|
if (-not $silentInstallSwitch) {
|
||||||
WriteLog "Silent install switch for $appName could not be found. Skipping the inclusion of $appName."
|
WriteLog "Silent install switch for $appName could not be found. Skipping the inclusion of $appName."
|
||||||
Remove-Item -Path $appFolderPath -Recurse -Force
|
Remove-Item -Path $appFolderPath -Recurse -Force
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
$installer = Split-Path -Path $installerPath -Leaf
|
|
||||||
|
# Choose final installer path and extension
|
||||||
|
$resolvedRelativePath = $null
|
||||||
|
$installerExt = $null
|
||||||
|
|
||||||
|
if ($installerCandidates.Count -eq 1 -and -not $relativeFromYaml) {
|
||||||
|
# Single installer – keep current behavior
|
||||||
|
$resolvedRelativePath = $installerCandidates[0].Name
|
||||||
|
$installerExt = $installerCandidates[0].Extension
|
||||||
|
WriteLog "Single installer detected ($resolvedRelativePath). Using current behavior."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($relativeFromYaml) {
|
||||||
|
$normalizedPath = ($relativeFromYaml -replace '/', '\')
|
||||||
|
$resolvedRelativePath = $normalizedPath
|
||||||
|
$installerExt = [System.IO.Path]::GetExtension($normalizedPath)
|
||||||
|
if ([string]::IsNullOrEmpty($installerExt)) {
|
||||||
|
$leafName = [System.IO.Path]::GetFileName($normalizedPath)
|
||||||
|
$matchedCandidate = $installerCandidates | Where-Object { $_.Name -ieq $leafName } | Select-Object -First 1
|
||||||
|
if ($matchedCandidate) {
|
||||||
|
$installerExt = $matchedCandidate.Extension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteLog "Multiple installers found. Selected by YAML NestedInstallerFiles: $resolvedRelativePath"
|
||||||
|
}
|
||||||
|
if (-not $resolvedRelativePath) {
|
||||||
|
# Fallbacks when YAML lacks NestedInstallerFiles or couldn't be matched
|
||||||
|
$msis = $installerCandidates | Where-Object { $_.Extension -ieq ".msi" }
|
||||||
|
if ($msis.Count -eq 1) {
|
||||||
|
$resolvedRelativePath = $msis[0].Name
|
||||||
|
$installerExt = ".msi"
|
||||||
|
WriteLog "Multiple installers found. YAML not used. Falling back to single MSI: $resolvedRelativePath"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$exes = $installerCandidates | Where-Object { $_.Extension -ieq ".exe" }
|
||||||
|
if ($exes.Count -eq 1) {
|
||||||
|
$resolvedRelativePath = $exes[0].Name
|
||||||
|
$installerExt = ".exe"
|
||||||
|
WriteLog "Multiple installers found. YAML not used. Falling back to single EXE: $resolvedRelativePath"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$first = $installerCandidates | Select-Object -First 1
|
||||||
|
$resolvedRelativePath = $first.Name
|
||||||
|
$installerExt = $first.Extension
|
||||||
|
WriteLog "Multiple installers found and ambiguous. Selecting the first candidate: $resolvedRelativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$basePath = "D:\win32\$AppFolder"
|
$basePath = "D:\win32\$AppFolder"
|
||||||
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
if (-not [string]::IsNullOrEmpty($SubFolder)) {
|
||||||
$basePath = "$basePath\$SubFolder"
|
$basePath = "$basePath\$SubFolder"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($installerPath.Extension -eq ".exe") {
|
# Build final command/arguments
|
||||||
$silentInstallCommand = "$basePath\$installer"
|
if ($installerExt -ieq ".exe") {
|
||||||
|
$silentInstallCommand = "$basePath\$resolvedRelativePath"
|
||||||
}
|
}
|
||||||
elseif ($installerPath.Extension -eq ".msi") {
|
elseif ($installerExt -ieq ".msi") {
|
||||||
$silentInstallCommand = "msiexec"
|
$silentInstallCommand = "msiexec"
|
||||||
$silentInstallSwitch = "/i `"$basePath\$installer`" $silentInstallSwitch"
|
$silentInstallSwitch = "/i `"$basePath\$resolvedRelativePath`" $silentInstallSwitch"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Default path usage if extension could not be inferred
|
||||||
|
$silentInstallCommand = "$basePath\$resolvedRelativePath"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Path to the JSON file
|
# Path to the JSON file
|
||||||
|
|||||||
@@ -5,6 +5,85 @@
|
|||||||
This module contains all the functions that power the "Applications" tab in the BuildFFUVM_UI. It handles user interactions for managing custom application lists (BYO Apps), such as adding, removing, reordering, and saving/loading the list from a JSON file (UserAppList.json). It also includes the logic for copying the application source files to the designated staging directory in parallel. Additionally, it manages the UI for creating and removing key-value pairs for the AppsScriptVariables.json file, which allows for custom parameterization of user-provided scripts.
|
This module contains all the functions that power the "Applications" tab in the BuildFFUVM_UI. It handles user interactions for managing custom application lists (BYO Apps), such as adding, removing, reordering, and saving/loading the list from a JSON file (UserAppList.json). It also includes the logic for copying the application source files to the designated staging directory in parallel. Additionally, it manages the UI for creating and removing key-value pairs for the AppsScriptVariables.json file, which allows for custom parameterization of user-provided scripts.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
# Function to update the enabled state of BYO Apps action buttons based on selection
|
||||||
|
function Update-BYOAppsActionButtonsState {
|
||||||
|
param(
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
$listView = $State.Controls.lstApplications
|
||||||
|
$removeButton = $State.Controls.btnRemoveSelectedBYOApps
|
||||||
|
$editButton = $State.Controls.btnEditApplication
|
||||||
|
|
||||||
|
if ($listView -and $removeButton -and $editButton) {
|
||||||
|
# Count selected items
|
||||||
|
$selectedItems = @($listView.Items | Where-Object { $_.IsSelected })
|
||||||
|
$selectedCount = $selectedItems.Count
|
||||||
|
|
||||||
|
# Enable the remove button if any item is selected
|
||||||
|
$removeButton.IsEnabled = ($selectedCount -gt 0)
|
||||||
|
|
||||||
|
# Enable the edit button only if exactly one item is selected
|
||||||
|
$editButton.IsEnabled = ($selectedCount -eq 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to remove all selected BYO applications
|
||||||
|
function Remove-SelectedBYOApplications {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
|
||||||
|
$listView = $State.Controls.lstApplications
|
||||||
|
$itemsToRemove = @($listView.Items | Where-Object { $_.IsSelected })
|
||||||
|
|
||||||
|
if ($itemsToRemove.Count -eq 0) {
|
||||||
|
# This should not happen if the button is correctly disabled, but as a safeguard:
|
||||||
|
[System.Windows.MessageBox]::Show("No applications are selected for removal.", "Remove Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the item being edited is among those being removed
|
||||||
|
if ($null -ne $State.Data.editingBYOApplication -and $itemsToRemove.Contains($State.Data.editingBYOApplication)) {
|
||||||
|
# Reset the edit state
|
||||||
|
$State.Data.editingBYOApplication = $null
|
||||||
|
$State.Controls.btnAddApplication.Content = "Add Application"
|
||||||
|
# Clear the form fields
|
||||||
|
$State.Controls.txtAppName.Clear()
|
||||||
|
$State.Controls.txtAppCommandLine.Clear()
|
||||||
|
$State.Controls.txtAppArguments.Clear()
|
||||||
|
$State.Controls.txtAppSource.Clear()
|
||||||
|
$State.Controls.txtAppAdditionalExitCodes.Clear()
|
||||||
|
$State.Controls.chkIgnoreExitCodes.IsChecked = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $itemsToRemove) {
|
||||||
|
$listView.Items.Remove($item)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-calculate priorities for the remaining items
|
||||||
|
Update-ListViewPriorities -ListView $listView
|
||||||
|
|
||||||
|
# Update button states (Copy and Remove)
|
||||||
|
Update-CopyButtonState -State $State
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
|
|
||||||
|
# Update the header checkbox state
|
||||||
|
$headerChk = $State.Controls.chkSelectAllBYOApps
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ask user if they want to save the changes
|
||||||
|
$result = [System.Windows.MessageBox]::Show("The selected applications have been removed from the list. Do you want to save these changes to UserAppList.json now?", "Save Changes", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
||||||
|
|
||||||
|
if ($result -eq 'Yes') {
|
||||||
|
$userAppListPath = Join-Path -Path $State.Controls.txtApplicationPath.Text -ChildPath 'UserAppList.json'
|
||||||
|
Save-BYOApplicationList -Path $userAppListPath -State $State
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Function to update the enabled state of the Copy Apps button
|
# Function to update the enabled state of the Copy Apps button
|
||||||
function Update-CopyButtonState {
|
function Update-CopyButtonState {
|
||||||
param(
|
param(
|
||||||
@@ -40,6 +119,7 @@ function Remove-Application {
|
|||||||
Update-ListViewPriorities -ListView $listView
|
Update-ListViewPriorities -ListView $listView
|
||||||
# Update the Copy Apps button state
|
# Update the Copy Apps button state
|
||||||
Update-CopyButtonState -State $State
|
Update-CopyButtonState -State $State
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,29 +135,109 @@ function Add-BYOApplication {
|
|||||||
$commandLine = $State.Controls.txtAppCommandLine.Text
|
$commandLine = $State.Controls.txtAppCommandLine.Text
|
||||||
$arguments = $State.Controls.txtAppArguments.Text
|
$arguments = $State.Controls.txtAppArguments.Text
|
||||||
$source = $State.Controls.txtAppSource.Text
|
$source = $State.Controls.txtAppSource.Text
|
||||||
|
$additionalExitCodes = $State.Controls.txtAppAdditionalExitCodes.Text
|
||||||
|
$ignoreNonZeroExitCodes = $State.Controls.chkIgnoreExitCodes.IsChecked
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($commandLine)) {
|
if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($commandLine)) {
|
||||||
[System.Windows.MessageBox]::Show("Please fill in all fields (Name and Command Line)", "Missing Information", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
[System.Windows.MessageBox]::Show("Please fill in all fields (Name and Command Line)", "Missing Information", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$listView = $State.Controls.lstApplications
|
$listView = $State.Controls.lstApplications
|
||||||
# Check for duplicate names
|
|
||||||
$existingApp = $listView.Items | Where-Object { $_.Name -eq $name }
|
# Check if we are in edit mode
|
||||||
if ($existingApp) {
|
if ($null -ne $State.Data.editingBYOApplication) {
|
||||||
[System.Windows.MessageBox]::Show("An application with the name '$name' already exists.", "Duplicate Name", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
$itemToUpdate = $State.Data.editingBYOApplication
|
||||||
return
|
|
||||||
|
# Check for duplicate names, excluding the item being edited
|
||||||
|
$existingApp = $listView.Items | Where-Object { $_.Name -eq $name -and $_ -ne $itemToUpdate }
|
||||||
|
if ($existingApp) {
|
||||||
|
[System.Windows.MessageBox]::Show("An application with the name '$name' already exists.", "Duplicate Name", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the properties of the existing object
|
||||||
|
$itemToUpdate.Name = $name
|
||||||
|
$itemToUpdate.CommandLine = $commandLine
|
||||||
|
$itemToUpdate.Arguments = $arguments
|
||||||
|
$itemToUpdate.Source = $source
|
||||||
|
$itemToUpdate.AdditionalExitCodes = $additionalExitCodes
|
||||||
|
$itemToUpdate.IgnoreNonZeroExitCodes = $ignoreNonZeroExitCodes
|
||||||
|
$itemToUpdate.IgnoreExitCodes = if ($ignoreNonZeroExitCodes) { "Yes" } else { "No" }
|
||||||
|
|
||||||
|
# Refresh the ListView to show the changes
|
||||||
|
$listView.Items.Refresh()
|
||||||
|
|
||||||
|
# Reset state
|
||||||
|
$State.Data.editingBYOApplication = $null
|
||||||
|
$State.Controls.btnAddApplication.Content = "Add Application"
|
||||||
}
|
}
|
||||||
$priority = 1
|
else {
|
||||||
if ($listView.Items.Count -gt 0) {
|
# This is a new application
|
||||||
$priority = ($listView.Items | Measure-Object -Property Priority -Maximum).Maximum + 1
|
# Check for duplicate names
|
||||||
|
$existingApp = $listView.Items | Where-Object { $_.Name -eq $name }
|
||||||
|
if ($existingApp) {
|
||||||
|
[System.Windows.MessageBox]::Show("An application with the name '$name' already exists.", "Duplicate Name", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$priority = 1
|
||||||
|
if ($listView.Items.Count -gt 0) {
|
||||||
|
$priority = ($listView.Items | Measure-Object -Property Priority -Maximum).Maximum + 1
|
||||||
|
}
|
||||||
|
$application = [PSCustomObject]@{
|
||||||
|
IsSelected = $false
|
||||||
|
Priority = $priority
|
||||||
|
Name = $name
|
||||||
|
CommandLine = $commandLine
|
||||||
|
Arguments = $arguments
|
||||||
|
Source = $source
|
||||||
|
AdditionalExitCodes = $additionalExitCodes
|
||||||
|
IgnoreNonZeroExitCodes = $ignoreNonZeroExitCodes
|
||||||
|
IgnoreExitCodes = if ($ignoreNonZeroExitCodes) { "Yes" } else { "No" }
|
||||||
|
CopyStatus = ""
|
||||||
|
}
|
||||||
|
$listView.Items.Add($application)
|
||||||
}
|
}
|
||||||
$application = [PSCustomObject]@{ Priority = $priority; Name = $name; CommandLine = $commandLine; Arguments = $arguments; Source = $source; CopyStatus = "" }
|
|
||||||
$listView.Items.Add($application)
|
# Clear form and update button states for both add and update operations
|
||||||
$State.Controls.txtAppName.Text = ""
|
$State.Controls.txtAppName.Text = ""
|
||||||
$State.Controls.txtAppCommandLine.Text = ""
|
$State.Controls.txtAppCommandLine.Text = ""
|
||||||
$State.Controls.txtAppArguments.Text = ""
|
$State.Controls.txtAppArguments.Text = ""
|
||||||
$State.Controls.txtAppSource.Text = ""
|
$State.Controls.txtAppSource.Text = ""
|
||||||
|
$State.Controls.txtAppAdditionalExitCodes.Text = ""
|
||||||
|
$State.Controls.chkIgnoreExitCodes.IsChecked = $false
|
||||||
Update-CopyButtonState -State $State
|
Update-CopyButtonState -State $State
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to populate the form for editing a BYO application
|
||||||
|
function Start-EditBYOApplication {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
|
||||||
|
$listView = $State.Controls.lstApplications
|
||||||
|
$itemToEdit = @($listView.Items | Where-Object { $_.IsSelected }) | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($null -eq $itemToEdit) {
|
||||||
|
[System.Windows.MessageBox]::Show("No application selected or multiple applications selected.", "Edit Error", "OK", "Warning")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store the item being edited in the state
|
||||||
|
$State.Data.editingBYOApplication = $itemToEdit
|
||||||
|
|
||||||
|
# Populate the form fields
|
||||||
|
$State.Controls.txtAppName.Text = $itemToEdit.Name
|
||||||
|
$State.Controls.txtAppCommandLine.Text = $itemToEdit.CommandLine
|
||||||
|
$State.Controls.txtAppArguments.Text = $itemToEdit.Arguments
|
||||||
|
$State.Controls.txtAppSource.Text = $itemToEdit.Source
|
||||||
|
$State.Controls.txtAppAdditionalExitCodes.Text = $itemToEdit.AdditionalExitCodes
|
||||||
|
$State.Controls.chkIgnoreExitCodes.IsChecked = $itemToEdit.IgnoreNonZeroExitCodes
|
||||||
|
|
||||||
|
# Change the Add button to Update
|
||||||
|
$State.Controls.btnAddApplication.Content = "Update App"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to add a new Apps Script Variable from the UI
|
# Function to add a new Apps Script Variable from the UI
|
||||||
@@ -160,8 +320,10 @@ function Save-BYOApplicationList {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
# Ensure items are sorted by current priority before saving
|
# Ensure items are sorted by current priority before saving
|
||||||
# Exclude CopyStatus when saving and ensure Priority is an integer
|
# Exclude UI-only properties (CopyStatus, IgnoreExitCodes) and ensure Priority is an integer
|
||||||
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source
|
$propertiesToSave = 'Priority', 'Name', 'CommandLine', 'Arguments', 'Source', 'AdditionalExitCodes', 'IgnoreNonZeroExitCodes'
|
||||||
|
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source, AdditionalExitCodes, IgnoreNonZeroExitCodes
|
||||||
|
|
||||||
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Force -Encoding UTF8
|
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Force -Encoding UTF8
|
||||||
[System.Windows.MessageBox]::Show("Applications saved successfully to `"$Path`".", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
[System.Windows.MessageBox]::Show("Applications saved successfully to `"$Path`".", "Save Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
||||||
}
|
}
|
||||||
@@ -193,14 +355,18 @@ function Import-BYOApplicationList {
|
|||||||
# Add items and sort by priority from the file
|
# Add items and sort by priority from the file
|
||||||
$sortedApps = $applications | Sort-Object Priority
|
$sortedApps = $applications | Sort-Object Priority
|
||||||
foreach ($app in $sortedApps) {
|
foreach ($app in $sortedApps) {
|
||||||
# Ensure all properties exist, add CopyStatus
|
$ignoreNonZero = if ($app.PSObject.Properties['IgnoreNonZeroExitCodes']) { $app.IgnoreNonZeroExitCodes } else { $false }
|
||||||
$appObject = [PSCustomObject]@{
|
$appObject = [PSCustomObject]@{
|
||||||
Priority = $app.Priority # Keep original priority for now
|
IsSelected = $false
|
||||||
Name = $app.Name
|
Priority = $app.Priority
|
||||||
CommandLine = $app.CommandLine
|
Name = $app.Name
|
||||||
Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" } # Handle missing Arguments
|
CommandLine = $app.CommandLine
|
||||||
Source = $app.Source
|
Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" }
|
||||||
CopyStatus = "" # Initialize CopyStatus
|
Source = $app.Source
|
||||||
|
AdditionalExitCodes = if ($app.PSObject.Properties['AdditionalExitCodes']) { $app.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = $ignoreNonZero
|
||||||
|
IgnoreExitCodes = if ($ignoreNonZero) { "Yes" } else { "No" }
|
||||||
|
CopyStatus = ""
|
||||||
}
|
}
|
||||||
$listView.Items.Add($appObject)
|
$listView.Items.Add($appObject)
|
||||||
}
|
}
|
||||||
@@ -209,7 +375,7 @@ function Import-BYOApplicationList {
|
|||||||
Update-ListViewPriorities -ListView $listView
|
Update-ListViewPriorities -ListView $listView
|
||||||
# Update the Copy Apps button state
|
# Update the Copy Apps button state
|
||||||
Update-CopyButtonState -State $State
|
Update-CopyButtonState -State $State
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
[System.Windows.MessageBox]::Show("Applications imported successfully from `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
[System.Windows.MessageBox]::Show("Applications imported successfully from `"$Path`".", "Import Applications", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function Get-UIConfig {
|
|||||||
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
||||||
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
|
CreateCaptureMedia = $State.Controls.chkCreateCaptureMedia.IsChecked
|
||||||
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
|
CreateDeploymentMedia = $State.Controls.chkCreateDeploymentMedia.IsChecked
|
||||||
|
InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
|
||||||
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
|
CustomFFUNameTemplate = $State.Controls.txtCustomFFUNameTemplate.Text
|
||||||
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
|
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
|
||||||
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
|
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
|
||||||
@@ -70,7 +71,7 @@ function Get-UIConfig {
|
|||||||
OfficeConfigXMLFile = $State.Controls.txtOfficeConfigXMLFilePath.Text
|
OfficeConfigXMLFile = $State.Controls.txtOfficeConfigXMLFilePath.Text
|
||||||
OfficePath = $State.Controls.txtOfficePath.Text
|
OfficePath = $State.Controls.txtOfficePath.Text
|
||||||
Optimize = $State.Controls.chkOptimize.IsChecked
|
Optimize = $State.Controls.chkOptimize.IsChecked
|
||||||
OptionalFeatures = $State.Controls.txtOptionalFeatures.Text
|
OptionalFeatures = (($State.Controls.featureCheckBoxes.GetEnumerator() | Where-Object { $_.Value.IsChecked } | ForEach-Object { $_.Key } | Sort-Object) -join ';')
|
||||||
OrchestrationPath = "$($State.Controls.txtApplicationPath.Text)\Orchestration"
|
OrchestrationPath = "$($State.Controls.txtApplicationPath.Text)\Orchestration"
|
||||||
PEDriversFolder = $State.Controls.txtPEDriversFolder.Text
|
PEDriversFolder = $State.Controls.txtPEDriversFolder.Text
|
||||||
Processors = [int]$State.Controls.txtProcessors.Text
|
Processors = [int]$State.Controls.txtProcessors.Text
|
||||||
@@ -93,6 +94,7 @@ function Get-UIConfig {
|
|||||||
USBDriveList = @{}
|
USBDriveList = @{}
|
||||||
Username = $State.Controls.txtUsername.Text
|
Username = $State.Controls.txtUsername.Text
|
||||||
Threads = [int]$State.Controls.txtThreads.Text
|
Threads = [int]$State.Controls.txtThreads.Text
|
||||||
|
MaxUSBDrives = [int]$State.Controls.txtMaxUSBDrives.Text
|
||||||
Verbose = $State.Controls.chkVerbose.IsChecked
|
Verbose = $State.Controls.chkVerbose.IsChecked
|
||||||
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
|
VMHostIPAddress = $State.Controls.txtVMHostIPAddress.Text
|
||||||
VMLocation = $State.Controls.txtVMLocation.Text
|
VMLocation = $State.Controls.txtVMLocation.Text
|
||||||
@@ -260,6 +262,58 @@ function Invoke-LoadConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Select-VMSwitchFromConfig {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$State,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$ConfigContent
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select VM switch based on configuration; fall back to 'Other' with custom name.
|
||||||
|
$combo = $State.Controls.cmbVMSwitchName
|
||||||
|
if ($null -eq $combo) {
|
||||||
|
WriteLog "LoadConfig Error: 'cmbVMSwitchName' control not found."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$configSwitch = $ConfigContent.VMSwitchName
|
||||||
|
if ($null -eq $configSwitch -or [string]::IsNullOrWhiteSpace($configSwitch)) {
|
||||||
|
WriteLog "LoadConfig Info: VMSwitchName in config was empty or null. Leaving selection unchanged."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemFound = $false
|
||||||
|
foreach ($item in $combo.Items) {
|
||||||
|
if ($null -ne $item -and $item.ToString().Equals($configSwitch, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$itemFound = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($itemFound) {
|
||||||
|
$combo.SelectedItem = ($combo.Items | Where-Object { $_.ToString().Equals($configSwitch, [System.StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1)
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
|
WriteLog "LoadConfig: Selected existing VM switch '$configSwitch'."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Ensure 'Other' exists
|
||||||
|
$otherExists = $false
|
||||||
|
foreach ($item in $combo.Items) {
|
||||||
|
if ($null -ne $item -and $item.ToString() -eq 'Other') { $otherExists = $true; break }
|
||||||
|
}
|
||||||
|
if (-not $otherExists) { $combo.Items.Add('Other') | Out-Null }
|
||||||
|
|
||||||
|
# Select 'Other' and populate custom name
|
||||||
|
$combo.SelectedItem = 'Other'
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Text = $configSwitch
|
||||||
|
$State.Data.customVMSwitchName = $configSwitch
|
||||||
|
$State.Data.customVMHostIP = $ConfigContent.VMHostIPAddress
|
||||||
|
WriteLog "LoadConfig: VMSwitchName '$configSwitch' not found. Selected 'Other' and populated custom VM Switch Name textbox."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Update-UIFromConfig {
|
function Update-UIFromConfig {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@@ -277,6 +331,7 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
|
Set-UIValue -ControlName 'txtShareName' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ShareName' -State $State
|
||||||
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
|
Set-UIValue -ControlName 'txtUsername' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Username' -State $State
|
||||||
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
|
Set-UIValue -ControlName 'txtThreads' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Threads' -State $State
|
||||||
|
Set-UIValue -ControlName 'txtMaxUSBDrives' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'MaxUSBDrives' -State $State
|
||||||
Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State
|
Set-UIValue -ControlName 'chkBuildUSBDriveEnable' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'BuildUSBDrive' -State $State
|
||||||
Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State
|
Set-UIValue -ControlName 'chkCompactOS' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompactOS' -State $State
|
||||||
Set-UIValue -ControlName 'chkUpdateADK' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UpdateADK' -State $State
|
Set-UIValue -ControlName 'chkUpdateADK' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UpdateADK' -State $State
|
||||||
@@ -286,6 +341,7 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
|
Set-UIValue -ControlName 'chkPromptExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'PromptExternalHardDiskMedia' -State $State
|
||||||
Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State
|
Set-UIValue -ControlName 'chkCreateCaptureMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateCaptureMedia' -State $State
|
||||||
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
|
Set-UIValue -ControlName 'chkCreateDeploymentMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CreateDeploymentMedia' -State $State
|
||||||
|
Set-UIValue -ControlName 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
|
||||||
Set-UIValue -ControlName 'chkVerbose' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'Verbose' -State $State
|
Set-UIValue -ControlName 'chkVerbose' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'Verbose' -State $State
|
||||||
|
|
||||||
# USB Drive Modification group (Build Tab)
|
# USB Drive Modification group (Build Tab)
|
||||||
@@ -303,7 +359,7 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkRemoveUpdates' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveUpdates' -State $State
|
Set-UIValue -ControlName 'chkRemoveUpdates' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'RemoveUpdates' -State $State
|
||||||
|
|
||||||
# Hyper-V Settings
|
# Hyper-V Settings
|
||||||
Set-UIValue -ControlName 'cmbVMSwitchName' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'VMSwitchName' -State $State
|
Select-VMSwitchFromConfig -State $State -ConfigContent $ConfigContent
|
||||||
Set-UIValue -ControlName 'txtVMHostIPAddress' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMHostIPAddress' -State $State
|
Set-UIValue -ControlName 'txtVMHostIPAddress' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'VMHostIPAddress' -State $State
|
||||||
Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $State
|
Set-UIValue -ControlName 'txtDiskSize' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Disksize' -TransformValue { param($val) $val / 1GB } -State $State
|
||||||
Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $State
|
Set-UIValue -ControlName 'txtMemory' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'Memory' -TransformValue { param($val) $val / 1GB } -State $State
|
||||||
@@ -363,10 +419,9 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'cmbWindowsSKU' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsSKU' -State $State
|
Set-UIValue -ControlName 'cmbWindowsSKU' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsSKU' -State $State
|
||||||
Set-UIValue -ControlName 'cmbMediaType' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'MediaType' -State $State
|
Set-UIValue -ControlName 'cmbMediaType' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'MediaType' -State $State
|
||||||
Set-UIValue -ControlName 'txtProductKey' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ProductKey' -State $State
|
Set-UIValue -ControlName 'txtProductKey' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'ProductKey' -State $State
|
||||||
Set-UIValue -ControlName 'txtOptionalFeatures' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'OptionalFeatures' -State $State
|
|
||||||
|
|
||||||
# Update Optional Features checkboxes based on the loaded text
|
# Update Optional Features checkboxes
|
||||||
$loadedFeaturesString = $State.Controls.txtOptionalFeatures.Text
|
$loadedFeaturesString = $ConfigContent.OptionalFeatures
|
||||||
if (-not [string]::IsNullOrWhiteSpace($loadedFeaturesString)) {
|
if (-not [string]::IsNullOrWhiteSpace($loadedFeaturesString)) {
|
||||||
$loadedFeaturesArray = $loadedFeaturesString.Split(';')
|
$loadedFeaturesArray = $loadedFeaturesString.Split(';')
|
||||||
WriteLog "LoadConfig: Updating Optional Features checkboxes. Loaded features: $($loadedFeaturesArray -join ', ')"
|
WriteLog "LoadConfig: Updating Optional Features checkboxes. Loaded features: $($loadedFeaturesArray -join ', ')"
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ function Register-EventHandlers {
|
|||||||
$State.Controls.txtDiskSize,
|
$State.Controls.txtDiskSize,
|
||||||
$State.Controls.txtMemory,
|
$State.Controls.txtMemory,
|
||||||
$State.Controls.txtProcessors,
|
$State.Controls.txtProcessors,
|
||||||
$State.Controls.txtThreads
|
$State.Controls.txtThreads,
|
||||||
|
$State.Controls.txtMaxUSBDrives
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach the handlers to each relevant textbox
|
# Attach the handlers to each relevant textbox
|
||||||
@@ -72,6 +73,20 @@ function Register-EventHandlers {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add specific validation for the Max USB Drives textbox to ensure it's an integer >=0 (allow 0 meaning all)
|
||||||
|
if ($null -ne $State.Controls.txtMaxUSBDrives) {
|
||||||
|
$State.Controls.txtMaxUSBDrives.Add_LostFocus({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$textBox = $eventSource
|
||||||
|
$currentValue = 0
|
||||||
|
$isValidInteger = [int]::TryParse($textBox.Text, [ref]$currentValue)
|
||||||
|
if (-not $isValidInteger -or $currentValue -lt 0) {
|
||||||
|
$textBox.Text = '0'
|
||||||
|
WriteLog "Max USB Drives value was invalid or less than 0. Reset to 0 (process all)."
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
# Build Tab Event Handlers
|
# Build Tab Event Handlers
|
||||||
$State.Controls.btnBrowseFFUDevPath.Add_Click({
|
$State.Controls.btnBrowseFFUDevPath.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
@@ -192,7 +207,12 @@ function Register-EventHandlers {
|
|||||||
$selectedItem = $eventSource.SelectedItem
|
$selectedItem = $eventSource.SelectedItem
|
||||||
if ($selectedItem -eq 'Other') {
|
if ($selectedItem -eq 'Other') {
|
||||||
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Visible'
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP for custom
|
if ([string]::IsNullOrWhiteSpace($localState.Controls.txtCustomVMSwitchName.Text) -and $null -ne $localState.Data.customVMSwitchName) {
|
||||||
|
$localState.Controls.txtCustomVMSwitchName.Text = $localState.Data.customVMSwitchName
|
||||||
|
}
|
||||||
|
if ($null -ne $localState.Data.customVMHostIP -and -not [string]::IsNullOrWhiteSpace($localState.Data.customVMHostIP)) {
|
||||||
|
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.customVMHostIP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
$localState.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
@@ -205,6 +225,24 @@ function Register-EventHandlers {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Persist custom VM switch name/IP when user edits them while 'Other' is selected
|
||||||
|
$State.Controls.txtVMHostIPAddress.Add_LostFocus({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if ($localState.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
||||||
|
$localState.Data.customVMHostIP = $localState.Controls.txtVMHostIPAddress.Text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.txtCustomVMSwitchName.Add_LostFocus({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
if ($localState.Controls.cmbVMSwitchName.SelectedItem -eq 'Other') {
|
||||||
|
$localState.Data.customVMSwitchName = $localState.Controls.txtCustomVMSwitchName.Text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
# Windows Settings tab Event Handlers
|
# Windows Settings tab Event Handlers
|
||||||
$State.Controls.txtISOPath.Add_TextChanged({
|
$State.Controls.txtISOPath.Add_TextChanged({
|
||||||
param($eventSource, $textChangedEventArgs)
|
param($eventSource, $textChangedEventArgs)
|
||||||
@@ -356,6 +394,13 @@ function Register-EventHandlers {
|
|||||||
Add-BYOApplication -State $localState
|
Add-BYOApplication -State $localState
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$State.Controls.btnEditApplication.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Start-EditBYOApplication -State $localState
|
||||||
|
})
|
||||||
|
|
||||||
$State.Controls.btnSaveBYOApplications.Add_Click({
|
$State.Controls.btnSaveBYOApplications.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
@@ -398,12 +443,21 @@ function Register-EventHandlers {
|
|||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
|
|
||||||
|
# Before clearing, check if we are in edit mode and reset the state
|
||||||
|
if ($null -ne $localState.Data.editingBYOApplication) {
|
||||||
|
$localState.Data.editingBYOApplication = $null
|
||||||
|
$localState.Controls.btnAddApplication.Content = "Add Application"
|
||||||
|
}
|
||||||
|
|
||||||
Clear-ListViewContent -State $localState `
|
Clear-ListViewContent -State $localState `
|
||||||
-ListViewControl $localState.Controls.lstApplications `
|
-ListViewControl $localState.Controls.lstApplications `
|
||||||
-ConfirmationTitle "Clear BYO Applications" `
|
-ConfirmationTitle "Clear BYO Applications" `
|
||||||
-ConfirmationMessage "Are you sure you want to clear all 'Bring Your Own' applications?" `
|
-ConfirmationMessage "Are you sure you want to clear all 'Bring Your Own' applications?" `
|
||||||
-StatusMessage "BYO application list cleared." `
|
-StatusMessage "BYO application list cleared." `
|
||||||
-PostClearAction { Update-CopyButtonState -State $State }
|
-PostClearAction {
|
||||||
|
Update-CopyButtonState -State $State
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$State.Controls.btnCopyBYOApps.Add_Click({
|
$State.Controls.btnCopyBYOApps.Add_Click({
|
||||||
@@ -413,6 +467,13 @@ function Register-EventHandlers {
|
|||||||
Invoke-CopyBYOApps -State $localState -Button $eventSource
|
Invoke-CopyBYOApps -State $localState -Button $eventSource
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$State.Controls.btnRemoveSelectedBYOApps.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Remove-SelectedBYOApplications -State $localState
|
||||||
|
})
|
||||||
|
|
||||||
$State.Controls.btnMoveTop.Add_Click({
|
$State.Controls.btnMoveTop.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
@@ -441,6 +502,65 @@ function Register-EventHandlers {
|
|||||||
Move-ListViewItemBottom -ListView $localState.Controls.lstApplications
|
Move-ListViewItemBottom -ListView $localState.Controls.lstApplications
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$State.Controls.lstApplications.Add_PreviewKeyDown({
|
||||||
|
param($eventSource, $keyEvent)
|
||||||
|
if ($keyEvent.Key -eq 'Space') {
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Invoke-ListViewItemToggle -ListView $eventSource -State $localState -HeaderCheckBoxKeyName 'chkSelectAllBYOApps'
|
||||||
|
# Update button states after toggle
|
||||||
|
Update-BYOAppsActionButtonsState -State $localState
|
||||||
|
$keyEvent.Handled = $true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$State.Controls.lstApplications.Add_SelectionChanged({
|
||||||
|
param($eventSource, $selChangeEvent)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$headerChk = $localState.Controls.chkSelectAllBYOApps
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstApplications -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
# Update button states based on selection
|
||||||
|
Update-BYOAppsActionButtonsState -State $localState
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add a routed event handler to catch checkbox clicks within the ListView
|
||||||
|
$State.Controls.lstApplications.AddHandler(
|
||||||
|
[System.Windows.Controls.Primitives.ButtonBase]::ClickEvent,
|
||||||
|
[System.Windows.RoutedEventHandler] {
|
||||||
|
param($eventSource, $e)
|
||||||
|
# Check if the original source of the click was a CheckBox
|
||||||
|
$clickedCheckBox = $e.OriginalSource
|
||||||
|
if ($clickedCheckBox -is [System.Windows.Controls.CheckBox]) {
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$dataItem = $clickedCheckBox.DataContext
|
||||||
|
|
||||||
|
if ($null -ne $dataItem) {
|
||||||
|
# Defensively add the 'IsSelected' property if it's missing from the data object.
|
||||||
|
# This can happen in some complex UI scenarios or if the object was created without it.
|
||||||
|
if ($null -eq $dataItem.PSObject.Properties['IsSelected']) {
|
||||||
|
$dataItem | Add-Member -MemberType NoteProperty -Name 'IsSelected' -Value $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now that we're sure the property exists, set its value.
|
||||||
|
$dataItem.IsSelected = $clickedCheckBox.IsChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the state of the action buttons based on the new selection.
|
||||||
|
Update-BYOAppsActionButtonsState -State $localState
|
||||||
|
|
||||||
|
# Also, update the header checkbox to reflect the change.
|
||||||
|
$headerChk = $localState.Controls.chkSelectAllBYOApps
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstApplications -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Apps Script Variables Event Handlers
|
# Apps Script Variables Event Handlers
|
||||||
# Attach the handler to the script variables checkbox
|
# Attach the handler to the script variables checkbox
|
||||||
$State.Controls.chkDefineAppsScriptVariables.Add_Checked($appPanelUpdateHandler)
|
$State.Controls.chkDefineAppsScriptVariables.Add_Checked($appPanelUpdateHandler)
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.cmbWindowsSKU = $window.FindName('cmbWindowsSKU')
|
$State.Controls.cmbWindowsSKU = $window.FindName('cmbWindowsSKU')
|
||||||
$State.Controls.cmbMediaType = $window.FindName('cmbMediaType')
|
$State.Controls.cmbMediaType = $window.FindName('cmbMediaType')
|
||||||
$State.Controls.MediaTypeStackPanel = $window.FindName('MediaTypeStackPanel')
|
$State.Controls.MediaTypeStackPanel = $window.FindName('MediaTypeStackPanel')
|
||||||
$State.Controls.txtOptionalFeatures = $window.FindName('txtOptionalFeatures')
|
|
||||||
$State.Controls.featuresPanel = $window.FindName('stackFeaturesContainer')
|
$State.Controls.featuresPanel = $window.FindName('stackFeaturesContainer')
|
||||||
$State.Controls.chkDownloadDrivers = $window.FindName('chkDownloadDrivers')
|
$State.Controls.chkDownloadDrivers = $window.FindName('chkDownloadDrivers')
|
||||||
$State.Controls.cmbMake = $window.FindName('cmbMake')
|
$State.Controls.cmbMake = $window.FindName('cmbMake')
|
||||||
@@ -89,10 +88,14 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.txtAppCommandLine = $window.FindName('txtAppCommandLine')
|
$State.Controls.txtAppCommandLine = $window.FindName('txtAppCommandLine')
|
||||||
$State.Controls.txtAppArguments = $window.FindName('txtAppArguments')
|
$State.Controls.txtAppArguments = $window.FindName('txtAppArguments')
|
||||||
$State.Controls.txtAppSource = $window.FindName('txtAppSource')
|
$State.Controls.txtAppSource = $window.FindName('txtAppSource')
|
||||||
|
$State.Controls.txtAppAdditionalExitCodes = $window.FindName('txtAppAdditionalExitCodes')
|
||||||
|
$State.Controls.chkIgnoreExitCodes = $window.FindName('chkIgnoreExitCodes')
|
||||||
$State.Controls.btnAddApplication = $window.FindName('btnAddApplication')
|
$State.Controls.btnAddApplication = $window.FindName('btnAddApplication')
|
||||||
$State.Controls.btnSaveBYOApplications = $window.FindName('btnSaveBYOApplications')
|
$State.Controls.btnSaveBYOApplications = $window.FindName('btnSaveBYOApplications')
|
||||||
$State.Controls.btnLoadBYOApplications = $window.FindName('btnLoadBYOApplications')
|
$State.Controls.btnLoadBYOApplications = $window.FindName('btnLoadBYOApplications')
|
||||||
|
$State.Controls.btnEditApplication = $window.FindName('btnEditApplication')
|
||||||
$State.Controls.btnClearBYOApplications = $window.FindName('btnClearBYOApplications')
|
$State.Controls.btnClearBYOApplications = $window.FindName('btnClearBYOApplications')
|
||||||
|
$State.Controls.btnRemoveSelectedBYOApps = $window.FindName('btnRemoveSelectedBYOApps')
|
||||||
$State.Controls.btnCopyBYOApps = $window.FindName('btnCopyBYOApps')
|
$State.Controls.btnCopyBYOApps = $window.FindName('btnCopyBYOApps')
|
||||||
$State.Controls.lstApplications = $window.FindName('lstApplications')
|
$State.Controls.lstApplications = $window.FindName('lstApplications')
|
||||||
$State.Controls.btnMoveTop = $window.FindName('btnMoveTop')
|
$State.Controls.btnMoveTop = $window.FindName('btnMoveTop')
|
||||||
@@ -111,11 +114,13 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.txtShareName = $window.FindName('txtShareName')
|
$State.Controls.txtShareName = $window.FindName('txtShareName')
|
||||||
$State.Controls.txtUsername = $window.FindName('txtUsername')
|
$State.Controls.txtUsername = $window.FindName('txtUsername')
|
||||||
$State.Controls.txtThreads = $window.FindName('txtThreads')
|
$State.Controls.txtThreads = $window.FindName('txtThreads')
|
||||||
|
$State.Controls.txtMaxUSBDrives = $window.FindName('txtMaxUSBDrives')
|
||||||
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
|
$State.Controls.chkCompactOS = $window.FindName('chkCompactOS')
|
||||||
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
|
$State.Controls.chkOptimize = $window.FindName('chkOptimize')
|
||||||
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
|
$State.Controls.chkAllowVHDXCaching = $window.FindName('chkAllowVHDXCaching')
|
||||||
$State.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia')
|
$State.Controls.chkCreateCaptureMedia = $window.FindName('chkCreateCaptureMedia')
|
||||||
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
|
$State.Controls.chkCreateDeploymentMedia = $window.FindName('chkCreateDeploymentMedia')
|
||||||
|
$State.Controls.chkInjectUnattend = $window.FindName('chkInjectUnattend')
|
||||||
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
|
$State.Controls.chkVerbose = $window.FindName('chkVerbose')
|
||||||
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
|
$State.Controls.chkCopyAutopilot = $window.FindName('chkCopyAutopilot')
|
||||||
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
|
$State.Controls.chkCopyUnattend = $window.FindName('chkCopyUnattend')
|
||||||
@@ -223,11 +228,13 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
|
$State.Controls.txtShareName.Text = $State.Defaults.generalDefaults.ShareName
|
||||||
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
|
$State.Controls.txtUsername.Text = $State.Defaults.generalDefaults.Username
|
||||||
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
|
$State.Controls.txtThreads.Text = $State.Defaults.generalDefaults.Threads
|
||||||
|
$State.Controls.txtMaxUSBDrives.Text = $State.Defaults.generalDefaults.MaxUSBDrives
|
||||||
$State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable
|
$State.Controls.chkBuildUSBDriveEnable.IsChecked = $State.Defaults.generalDefaults.BuildUSBDriveEnable
|
||||||
$State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS
|
$State.Controls.chkCompactOS.IsChecked = $State.Defaults.generalDefaults.CompactOS
|
||||||
$State.Controls.chkUpdateADK.IsChecked = $State.Defaults.generalDefaults.UpdateADK
|
$State.Controls.chkUpdateADK.IsChecked = $State.Defaults.generalDefaults.UpdateADK
|
||||||
$State.Controls.chkOptimize.IsChecked = $State.Defaults.generalDefaults.Optimize
|
$State.Controls.chkOptimize.IsChecked = $State.Defaults.generalDefaults.Optimize
|
||||||
$State.Controls.chkAllowVHDXCaching.IsChecked = $State.Defaults.generalDefaults.AllowVHDXCaching
|
$State.Controls.chkAllowVHDXCaching.IsChecked = $State.Defaults.generalDefaults.AllowVHDXCaching
|
||||||
|
$State.Controls.chkInjectUnattend.IsChecked = $State.Defaults.generalDefaults.InjectUnattend
|
||||||
$State.Controls.chkCreateCaptureMedia.IsChecked = $State.Defaults.generalDefaults.CreateCaptureMedia
|
$State.Controls.chkCreateCaptureMedia.IsChecked = $State.Defaults.generalDefaults.CreateCaptureMedia
|
||||||
$State.Controls.chkCreateDeploymentMedia.IsChecked = $State.Defaults.generalDefaults.CreateDeploymentMedia
|
$State.Controls.chkCreateDeploymentMedia.IsChecked = $State.Defaults.generalDefaults.CreateDeploymentMedia
|
||||||
$State.Controls.chkAllowExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.AllowExternalHardDiskMedia
|
$State.Controls.chkAllowExternalHardDiskMedia.IsChecked = $State.Defaults.generalDefaults.AllowExternalHardDiskMedia
|
||||||
@@ -266,7 +273,6 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.cmbWindowsLang.SelectedItem = $State.Defaults.windowsSettingsDefaults.DefaultWindowsLang
|
$State.Controls.cmbWindowsLang.SelectedItem = $State.Defaults.windowsSettingsDefaults.DefaultWindowsLang
|
||||||
$State.Controls.cmbMediaType.ItemsSource = $State.Defaults.windowsSettingsDefaults.AllowedMediaTypes
|
$State.Controls.cmbMediaType.ItemsSource = $State.Defaults.windowsSettingsDefaults.AllowedMediaTypes
|
||||||
$State.Controls.cmbMediaType.SelectedItem = $State.Defaults.windowsSettingsDefaults.DefaultMediaType
|
$State.Controls.cmbMediaType.SelectedItem = $State.Defaults.windowsSettingsDefaults.DefaultMediaType
|
||||||
$State.Controls.txtOptionalFeatures.Text = $State.Defaults.windowsSettingsDefaults.DefaultOptionalFeatures
|
|
||||||
$State.Controls.txtProductKey.Text = $State.Defaults.windowsSettingsDefaults.DefaultProductKey
|
$State.Controls.txtProductKey.Text = $State.Defaults.windowsSettingsDefaults.DefaultProductKey
|
||||||
|
|
||||||
# Updates tab defaults from General Defaults
|
# Updates tab defaults from General Defaults
|
||||||
@@ -454,6 +460,28 @@ function Initialize-DynamicUIElements {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# BYO Applications ListView setup
|
||||||
|
$byoAppsGridView = New-Object System.Windows.Controls.GridView
|
||||||
|
$State.Controls.lstApplications.View = $byoAppsGridView
|
||||||
|
|
||||||
|
# Set ListViewItem style to stretch content horizontally
|
||||||
|
$itemStyleBYOApps = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
$itemStyleBYOApps.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
|
$State.Controls.lstApplications.ItemContainerStyle = $itemStyleBYOApps
|
||||||
|
|
||||||
|
# Add the selectable column
|
||||||
|
Add-SelectableGridViewColumn -ListView $State.Controls.lstApplications -State $State -HeaderCheckBoxKeyName "chkSelectAllBYOApps" -ColumnWidth 60
|
||||||
|
|
||||||
|
# Add other sortable columns
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Priority" -binding "Priority" -width 60 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Name" -binding "Name" -width 150 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Command Line" -binding "CommandLine" -width 200 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Arguments" -binding "Arguments" -width 200 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Source" -binding "Source" -width 150 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Exit Codes" -binding "AdditionalExitCodes" -width 100 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Ignore Exit Codes" -binding "IgnoreExitCodes" -width 120 -headerHorizontalAlignment Left
|
||||||
|
Add-SortableColumn -gridView $byoAppsGridView -header "Copy Status" -binding "CopyStatus" -width 150 -headerHorizontalAlignment Left
|
||||||
|
|
||||||
# Apps Script Variables ListView setup
|
# Apps Script Variables ListView setup
|
||||||
# Bind ItemsSource to the data list
|
# Bind ItemsSource to the data list
|
||||||
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
$State.Controls.lstAppsScriptVariables.ItemsSource = $State.Data.appsScriptVariablesDataList.ToArray()
|
||||||
|
|||||||
@@ -608,7 +608,6 @@ function UpdateOptionalFeaturesString {
|
|||||||
foreach ($entry in $State.Controls.featureCheckBoxes.GetEnumerator()) {
|
foreach ($entry in $State.Controls.featureCheckBoxes.GetEnumerator()) {
|
||||||
if ($entry.Value.IsChecked) { $checkedFeatures += $entry.Key }
|
if ($entry.Value.IsChecked) { $checkedFeatures += $entry.Key }
|
||||||
}
|
}
|
||||||
$State.Controls.txtOptionalFeatures.Text = $checkedFeatures -join ";"
|
|
||||||
}
|
}
|
||||||
function BuildFeaturesGrid {
|
function BuildFeaturesGrid {
|
||||||
param (
|
param (
|
||||||
|
|||||||
@@ -385,7 +385,8 @@ function Start-WingetAppDownloadTask {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$OrchestrationPath,
|
[string]$OrchestrationPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue # Add queue parameter
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue, # Add queue parameter
|
||||||
|
[string]$WindowsArch
|
||||||
)
|
)
|
||||||
|
|
||||||
$appName = $ApplicationItemData.Name
|
$appName = $ApplicationItemData.Name
|
||||||
@@ -398,10 +399,6 @@ function Start-WingetAppDownloadTask {
|
|||||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||||
|
|
||||||
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
WriteLog "Starting download task for $($appName) with ID $($appId) from source $($source)."
|
||||||
# WriteLog "Apps Path: $($AppsPath)"
|
|
||||||
# WriteLog "AppList JSON Path: $($AppListJsonPath)"
|
|
||||||
# WriteLog "Windows Architecture: $($ApplicationItemData.Architecture)"
|
|
||||||
# WriteLog "Orchestration Path: $($OrchestrationPath)"
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Define paths
|
# Define paths
|
||||||
@@ -450,70 +447,35 @@ function Start-WingetAppDownloadTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Check previous Winget download
|
# 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency)
|
||||||
if (-not $appFound) {
|
if (-not $appFound -and $source -eq 'winget') {
|
||||||
if (-not $appFound) {
|
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
|
||||||
$wingetWin32jsonFile = Join-Path -Path $OrchestrationPath -ChildPath "WinGetWin32Apps.json"
|
if (Test-Path -Path $appFolder -PathType Container) {
|
||||||
if (Test-Path -Path $wingetWin32jsonFile) {
|
$contentFound = $false
|
||||||
try {
|
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
|
||||||
$wingetAppsJson = Get-Content -Path $wingetWin32jsonFile -Raw | ConvertFrom-Json
|
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
|
||||||
# Check if app already exists in WinGetWin32Apps.json
|
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
|
||||||
# For multi-arch apps, there might be entries like "AppName (x86)" and "AppName (x64)"
|
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
|
||||||
$existingWin32Entries = @($wingetAppsJson | Where-Object {
|
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||||
$_.Name -eq $appName -or
|
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||||
$_.Name -eq "$appName (x86)" -or
|
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
|
||||||
$_.Name -eq "$appName (x64)"
|
$contentFound = $true
|
||||||
})
|
|
||||||
|
|
||||||
if ($existingWin32Entries.Count -gt 0) {
|
|
||||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
|
|
||||||
$appContentFound = $false
|
|
||||||
|
|
||||||
# Check if it's a multi-arch app with subfolders
|
|
||||||
if ($ApplicationItemData.Architecture -eq 'x86 x64') {
|
|
||||||
$x86Folder = Join-Path -Path $appFolder -ChildPath "x86"
|
|
||||||
$x64Folder = Join-Path -Path $appFolder -ChildPath "x64"
|
|
||||||
|
|
||||||
if ((Test-Path -Path $x86Folder -PathType Container) -and (Test-Path -Path $x64Folder -PathType Container)) {
|
|
||||||
$x86Size = (Get-ChildItem -Path $x86Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
||||||
$x64Size = (Get-ChildItem -Path $x64Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
||||||
|
|
||||||
if ($x86Size -gt 1MB -and $x64Size -gt 1MB) {
|
|
||||||
$appContentFound = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Single architecture app
|
|
||||||
if (Test-Path -Path $appFolder -PathType Container) {
|
|
||||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
|
||||||
if ($folderSize -gt 1MB) {
|
|
||||||
$appContentFound = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($appContentFound) {
|
|
||||||
$appFound = $true
|
|
||||||
$status = "Not Downloaded: App already in $wingetWin32jsonFile and found in $appFolder"
|
|
||||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
|
||||||
WriteLog "Found '$appName' in WinGetWin32Apps.json and content exists in '$appFolder'. Skipping download to prevent duplicate entry."
|
|
||||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# App entry exists in WinGetWin32Apps.json but folder is missing or incomplete
|
|
||||||
$appFound = $true
|
|
||||||
$status = "App in '$wingetWin32jsonFile' but content folder '$appFolder' not found or incomplete. Remove entry from WinGetWin32Apps.json or restore content."
|
|
||||||
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
|
||||||
WriteLog $status
|
|
||||||
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 1 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
}
|
||||||
WriteLog "Warning: Could not read or parse '$wingetWin32jsonFile'. Error: $($_.Exception.Message)"
|
else {
|
||||||
|
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||||
|
if ($folderSize -gt 1MB) {
|
||||||
|
$contentFound = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($contentFound) {
|
||||||
|
$appFound = $true
|
||||||
|
$status = "Not Downloaded: Existing content found in $appFolder"
|
||||||
|
Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $appId -Status $status
|
||||||
|
WriteLog "Found existing content for '$appName' in '$appFolder'. Skipping download to prevent duplicate entry."
|
||||||
|
return [PSCustomObject]@{ Id = $appId; Status = $status; ResultCode = 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +596,7 @@ function Start-WingetAppDownloadTask {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
# Call Get-Application
|
# Call Get-Application
|
||||||
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -WindowsArch $ApplicationItemData.Architecture -OrchestrationPath $OrchestrationPath -ErrorAction Stop
|
$resultCode = Get-Application -AppName $appName -AppId $appId -Source $source -AppsPath $AppsPath -ApplicationArch $ApplicationItemData.Architecture -WindowsArch $WindowsArch -OrchestrationPath $OrchestrationPath -SkipWin32Json -ErrorAction Stop
|
||||||
|
|
||||||
# Determine status based on result code
|
# Determine status based on result code
|
||||||
switch ($resultCode) {
|
switch ($resultCode) {
|
||||||
@@ -753,9 +715,10 @@ function Invoke-WingetDownload {
|
|||||||
|
|
||||||
# Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing
|
# Create hashtable for task-specific arguments to pass to Invoke-ParallelProcessing
|
||||||
$taskArguments = @{
|
$taskArguments = @{
|
||||||
AppsPath = $localAppsPath
|
AppsPath = $localAppsPath
|
||||||
AppListJsonPath = $localAppListJsonPath
|
AppListJsonPath = $localAppListJsonPath
|
||||||
OrchestrationPath = $localOrchestrationPath
|
OrchestrationPath = $localOrchestrationPath
|
||||||
|
WindowsArch = $localWindowsArch
|
||||||
}
|
}
|
||||||
|
|
||||||
# Select only necessary properties before passing to Invoke-ParallelProcessing
|
# Select only necessary properties before passing to Invoke-ParallelProcessing
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function Get-GeneralDefaults {
|
|||||||
ShareName = "FFUCaptureShare"
|
ShareName = "FFUCaptureShare"
|
||||||
Username = "ffu_user"
|
Username = "ffu_user"
|
||||||
Threads = 5
|
Threads = 5
|
||||||
|
MaxUSBDrives = 5
|
||||||
BuildUSBDriveEnable = $false
|
BuildUSBDriveEnable = $false
|
||||||
CompactOS = $true
|
CompactOS = $true
|
||||||
Optimize = $true
|
Optimize = $true
|
||||||
@@ -130,6 +131,7 @@ function Get-GeneralDefaults {
|
|||||||
CopyAutopilot = $false
|
CopyAutopilot = $false
|
||||||
CopyUnattend = $false
|
CopyUnattend = $false
|
||||||
CopyPPKG = $false
|
CopyPPKG = $false
|
||||||
|
InjectUnattend = $false
|
||||||
CleanupAppsISO = $true
|
CleanupAppsISO = $true
|
||||||
CleanupCaptureISO = $true
|
CleanupCaptureISO = $true
|
||||||
CleanupDeployISO = $true
|
CleanupDeployISO = $true
|
||||||
@@ -202,6 +204,11 @@ function Update-ApplicationPanelVisibility {
|
|||||||
[string]$TriggeringControlName # Optional: to know which control initiated the change
|
[string]$TriggeringControlName # Optional: to know which control initiated the change
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If BYO Apps, Winget Apps, or Define Apps Script Variables is checked, force Install Apps to be checked
|
||||||
|
if ($State.Controls.chkBringYourOwnApps.IsChecked -or $State.Controls.chkInstallWingetApps.IsChecked -or $State.Controls.chkDefineAppsScriptVariables.IsChecked) {
|
||||||
|
$State.Controls.chkInstallApps.IsChecked = $true
|
||||||
|
}
|
||||||
|
|
||||||
$installAppsChecked = $State.Controls.chkInstallApps.IsChecked
|
$installAppsChecked = $State.Controls.chkInstallApps.IsChecked
|
||||||
|
|
||||||
# If the main 'Install Apps' is unchecked, everything below it gets hidden and reset.
|
# If the main 'Install Apps' is unchecked, everything below it gets hidden and reset.
|
||||||
@@ -268,6 +275,12 @@ function Update-InstallAppsState {
|
|||||||
# If no state was saved (e.g., it was never forced), ensure it's unchecked.
|
# If no state was saved (e.g., it was never forced), ensure it's unchecked.
|
||||||
$installAppsChk.IsChecked = $false
|
$installAppsChk.IsChecked = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# If BYO, Winget, or Apps Script Variables are checked, it overrides the restoration and keeps Install Apps checked.
|
||||||
|
if ($State.Controls.chkBringYourOwnApps.IsChecked -or $State.Controls.chkInstallWingetApps.IsChecked -or $State.Controls.chkDefineAppsScriptVariables.IsChecked) {
|
||||||
|
$installAppsChk.IsChecked = $true
|
||||||
|
}
|
||||||
|
|
||||||
$installAppsChk.IsEnabled = $true
|
$installAppsChk.IsEnabled = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,12 +203,25 @@ function Stop-Script {
|
|||||||
Read-Host "Press Enter to exit"
|
Read-Host "Press Enter to exit"
|
||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ComparableModelName {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Text
|
||||||
|
)
|
||||||
|
# Normalize model strings by converting any non-alphanumeric sequence to a single space, collapsing whitespace, and trimming.
|
||||||
|
if ($null -eq $Text) { return '' }
|
||||||
|
$normalized = ($Text -replace '[^A-Za-z0-9]+', ' ')
|
||||||
|
$normalized = ($normalized -replace '\s+', ' ').Trim()
|
||||||
|
return $normalized
|
||||||
|
}
|
||||||
|
|
||||||
#Get USB Drive and create log file
|
#Get USB Drive and create log file
|
||||||
$LogFileName = 'ScriptLog.txt'
|
$LogFileName = 'ScriptLog.txt'
|
||||||
$USBDrive = Get-USBDrive
|
$USBDrive = Get-USBDrive
|
||||||
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null
|
||||||
$LogFile = $USBDrive + $LogFilename
|
$LogFile = $USBDrive + $LogFilename
|
||||||
$version = '2507.1'
|
$version = '2508.1Preview'
|
||||||
WriteLog 'Begin Logging'
|
WriteLog 'Begin Logging'
|
||||||
WriteLog "Script version: $version"
|
WriteLog "Script version: $version"
|
||||||
|
|
||||||
@@ -337,8 +350,8 @@ If (Test-Path -Path $UnattendComputerNamePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Ask for device name if unattend exists
|
#Ask for device name if unattend exists
|
||||||
|
Write-SectionHeader 'Device Name Selection'
|
||||||
if ($Unattend -and $UnattendPrefix) {
|
if ($Unattend -and $UnattendPrefix) {
|
||||||
Write-SectionHeader 'Device Name Selection'
|
|
||||||
Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
|
Writelog 'Unattend file found with prefixes.txt. Getting prefixes.'
|
||||||
$UnattendPrefixes = @(Get-content $UnattendPrefixFile)
|
$UnattendPrefixes = @(Get-content $UnattendPrefixFile)
|
||||||
$UnattendPrefixCount = $UnattendPrefixes.Count
|
$UnattendPrefixCount = $UnattendPrefixes.Count
|
||||||
@@ -381,11 +394,10 @@ if ($Unattend -and $UnattendPrefix) {
|
|||||||
$computername = $computername.substring(0, 15)
|
$computername = $computername.substring(0, 15)
|
||||||
}
|
}
|
||||||
$computername = Set-Computername($computername)
|
$computername = Set-Computername($computername)
|
||||||
Writelog "Computer name set to $computername"
|
Writelog "Computer name will be set to $computername"
|
||||||
Write-Host "Computer name set to $computername"
|
Write-Host "Computer name will be set to $computername"
|
||||||
}
|
}
|
||||||
elseif ($Unattend -and $UnattendComputerName) {
|
elseif ($Unattend -and $UnattendComputerName) {
|
||||||
Write-SectionHeader 'Device Name Selection'
|
|
||||||
Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.'
|
Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.'
|
||||||
$SerialComputerNames = Import-Csv -Path $UnattendComputerNameFile.FullName -Delimiter ","
|
$SerialComputerNames = Import-Csv -Path $UnattendComputerNameFile.FullName -Delimiter ","
|
||||||
|
|
||||||
@@ -395,25 +407,25 @@ elseif ($Unattend -and $UnattendComputerName) {
|
|||||||
If ($SCName) {
|
If ($SCName) {
|
||||||
[string]$computername = $SCName.ComputerName
|
[string]$computername = $SCName.ComputerName
|
||||||
$computername = Set-Computername($computername)
|
$computername = Set-Computername($computername)
|
||||||
Writelog "Computer name set to $computername"
|
Writelog "Computer name will be set to $computername"
|
||||||
Write-Host "Computer name set to $computername"
|
Write-Host "Computer name will be set to $computername"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
||||||
Write-Host 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
Write-Host 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.'
|
||||||
[string]$computername = ("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ })))
|
[string]$computername = ("FFU-" + ( -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ })))
|
||||||
$computername = Set-Computername($computername)
|
$computername = Set-Computername($computername)
|
||||||
Writelog "Computer name set to $computername"
|
Writelog "Computer name will be set to $computername"
|
||||||
Write-Host "Computer name set to $computername"
|
Write-Host "Computer name will be set to $computername"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif ($Unattend) {
|
elseif ($Unattend) {
|
||||||
Writelog 'Unattend file found with no prefixes.txt, asking for name'
|
Writelog 'Unattend file found with no prefixes.txt, asking for name'
|
||||||
Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.'
|
Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.'
|
||||||
[string]$computername = Read-Host 'Enter device name'
|
[string]$computername = Read-Host 'Enter device name'
|
||||||
Set-Computername($computername)
|
$computername = Set-Computername($computername)
|
||||||
Writelog "Computer name set to $computername"
|
Writelog "Computer name will be set to $computername"
|
||||||
Write-Host "Computer name set to $computername"
|
Write-Host "Computer name will be set to $computername"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
|
WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.'
|
||||||
@@ -546,14 +558,18 @@ if (Test-Path -Path $driverMappingPath -PathType Leaf) {
|
|||||||
WriteLog "Detected System: Manufacturer='$systemManufacturer', Model='$systemModel'"
|
WriteLog "Detected System: Manufacturer='$systemManufacturer', Model='$systemModel'"
|
||||||
|
|
||||||
# Load and parse the mapping file, ensuring it's always an array
|
# Load and parse the mapping file, ensuring it's always an array
|
||||||
$driverMappings = @(Get-Content -Path $driverMappingPath -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue)
|
$driverMappings = Get-Content -Path $driverMappingPath | Out-String | ConvertFrom-Json -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# Find all matching rules and select the most specific one
|
# Find all matching rules and select the most specific one
|
||||||
$matchingRules = @()
|
$matchingRules = @()
|
||||||
foreach ($rule in $driverMappings) {
|
foreach ($rule in $driverMappings) {
|
||||||
# Use -like for wildcard matching.
|
# Use -like for wildcard matching.
|
||||||
|
# Prepare normalized model strings (ignore special characters and collapse whitespace)
|
||||||
|
$systemModelNorm = ConvertTo-ComparableModelName -Text $systemModel
|
||||||
|
$ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model
|
||||||
# This checks if the system model starts with the rule model, or vice-versa, for flexibility.
|
# This checks if the system model starts with the rule model, or vice-versa, for flexibility.
|
||||||
if ($systemManufacturer -like "$($rule.Manufacturer)*" -and ($systemModel -like "$($rule.Model)*" -or $rule.Model -like "$systemModel*")) {
|
if ($systemManufacturer -like "$($rule.Manufacturer)*" -and ($systemModelNorm -like "$($ruleModelNorm)*" -or $ruleModelNorm -like "$systemModelNorm*")) {
|
||||||
|
WriteLog "Match found: Manufacturer='$($rule.Manufacturer)', Model='$($rule.Model)' (Normalized: System='$systemModelNorm', Rule='$ruleModelNorm')"
|
||||||
$matchingRules += $rule
|
$matchingRules += $rule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -846,7 +862,7 @@ If ($computername) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Add Drivers
|
# Add Drivers
|
||||||
if ($null -ne $DriverSourcePath) {
|
if ($null -ne $DriverSourcePath) {
|
||||||
Write-SectionHeader -Title 'Installing Drivers'
|
Write-SectionHeader -Title 'Installing Drivers'
|
||||||
if ($DriverSourceType -eq 'WIM') {
|
if ($DriverSourceType -eq 'WIM') {
|
||||||
@@ -894,8 +910,11 @@ if ($null -ne $DriverSourcePath) {
|
|||||||
}
|
}
|
||||||
elseif ($DriverSourceType -eq 'Folder') {
|
elseif ($DriverSourceType -eq 'Folder') {
|
||||||
WriteLog "Injecting drivers from folder: $DriverSourcePath"
|
WriteLog "Injecting drivers from folder: $DriverSourcePath"
|
||||||
|
Write-Host "Injecting drivers from folder: $DriverSourcePath"
|
||||||
|
Write-Host "This may take a while, please be patient."
|
||||||
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:""$DriverSourcePath"" /Recurse"
|
Invoke-Process dism.exe "/image:W:\ /Add-Driver /Driver:""$DriverSourcePath"" /Recurse"
|
||||||
WriteLog "Driver injection from folder succeeded."
|
WriteLog "Driver injection from folder succeeded."
|
||||||
|
Write-Host "Driver injection from folder succeeded."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -37,36 +37,69 @@ Here's a detailed overview of the new UI process.
|
|||||||
[](https://youtu.be/oozG1aVcg9M "Reimage Windows Fast with FFU Builder 2507.1 Preview")
|
[](https://youtu.be/oozG1aVcg9M "Reimage Windows Fast with FFU Builder 2507.1 Preview")
|
||||||
|
|
||||||
Chapters:
|
Chapters:
|
||||||
|
|
||||||
[00:00](https://www.youtube.com/watch?v=oozG1aVcg9M&t=0s) Begin
|
[00:00](https://www.youtube.com/watch?v=oozG1aVcg9M&t=0s) Begin
|
||||||
|
|
||||||
[01:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=67s) Prereqs
|
[01:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=67s) Prereqs
|
||||||
|
|
||||||
[06:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=392s) Demo Begins
|
[06:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=392s) Demo Begins
|
||||||
|
|
||||||
[07:16](https://www.youtube.com/watch?v=oozG1aVcg9M&t=436s) Running the BuildFFUVM_UI.ps1 script
|
[07:16](https://www.youtube.com/watch?v=oozG1aVcg9M&t=436s) Running the BuildFFUVM_UI.ps1 script
|
||||||
|
|
||||||
[08:15](https://www.youtube.com/watch?v=oozG1aVcg9M&t=495s) UI Overview
|
[08:15](https://www.youtube.com/watch?v=oozG1aVcg9M&t=495s) UI Overview
|
||||||
|
|
||||||
[10:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=613s) Hyper-V Settings
|
[10:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=613s) Hyper-V Settings
|
||||||
|
|
||||||
[16:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=964s) Windows Settings
|
[16:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=964s) Windows Settings
|
||||||
|
|
||||||
[22:35](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1355s) Updates
|
[22:35](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1355s) Updates
|
||||||
|
|
||||||
[24:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1489s) Applications
|
[24:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1489s) Applications
|
||||||
|
|
||||||
[29:39](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1779s) Install Winget Applications
|
[29:39](https://www.youtube.com/watch?v=oozG1aVcg9M&t=1779s) Install Winget Applications
|
||||||
|
|
||||||
[45:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=2729s) Bring Your Own Applications
|
[45:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=2729s) Bring Your Own Applications
|
||||||
|
|
||||||
[54:14](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3254s) Apps Script Variables
|
[54:14](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3254s) Apps Script Variables
|
||||||
|
|
||||||
[57:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3463s) M365 Apps/Office
|
[57:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3463s) M365 Apps/Office
|
||||||
|
|
||||||
[59:01](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3541s) Drivers
|
[59:01](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3541s) Drivers
|
||||||
|
|
||||||
[01:01:22](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3682s) Drivers.json example
|
[01:01:22](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3682s) Drivers.json example
|
||||||
|
|
||||||
[01:02:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3727s) DriverMapping.json explanation
|
[01:02:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3727s) DriverMapping.json explanation
|
||||||
|
|
||||||
[01:06:08](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3968s) Driver WIM Compression
|
[01:06:08](https://www.youtube.com/watch?v=oozG1aVcg9M&t=3968s) Driver WIM Compression
|
||||||
|
|
||||||
[01:10:50](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4250s) Build
|
[01:10:50](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4250s) Build
|
||||||
|
|
||||||
[01:12:41](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4361s) Build USB Drive
|
[01:12:41](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4361s) Build USB Drive
|
||||||
|
|
||||||
[01:20:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4807s) Monitor
|
[01:20:07](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4807s) Monitor
|
||||||
|
|
||||||
[01:20:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4832s) Setting up the Demo Build
|
[01:20:32](https://www.youtube.com/watch?v=oozG1aVcg9M&t=4832s) Setting up the Demo Build
|
||||||
|
|
||||||
[01:24:10](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5050s) Save/Load Config Files
|
[01:24:10](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5050s) Save/Load Config Files
|
||||||
|
|
||||||
[01:25:11](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5111s) Kicking off the Demo Build/Going over the monitor tab
|
[01:25:11](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5111s) Kicking off the Demo Build/Going over the monitor tab
|
||||||
|
|
||||||
[01:32:26](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5546s) Demoing the new FFU Builder Orchestrator
|
[01:32:26](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5546s) Demoing the new FFU Builder Orchestrator
|
||||||
|
|
||||||
[01:35:25](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5725s) New captureffu.ps1 console output
|
[01:35:25](https://www.youtube.com/watch?v=oozG1aVcg9M&t=5725s) New captureffu.ps1 console output
|
||||||
|
|
||||||
[01:42:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6149s) Demo Build Complete
|
[01:42:29](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6149s) Demo Build Complete
|
||||||
|
|
||||||
[01:42:42](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6162s) How to configure a VM to test your newly built FFU
|
[01:42:42](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6162s) How to configure a VM to test your newly built FFU
|
||||||
|
|
||||||
[01:48:58](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6538s) The moment of truth: What does the new deployment experience look like?
|
[01:48:58](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6538s) The moment of truth: What does the new deployment experience look like?
|
||||||
|
|
||||||
[01:53:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6793s) How to bypass OOBE using a provisioning package
|
[01:53:13](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6793s) How to bypass OOBE using a provisioning package
|
||||||
|
|
||||||
[01:55:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6949s) Preview Focus Areas
|
[01:55:49](https://www.youtube.com/watch?v=oozG1aVcg9M&t=6949s) Preview Focus Areas
|
||||||
|
|
||||||
[02:04:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7444s) Known Issues/Things to fix before GA
|
[02:04:04](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7444s) Known Issues/Things to fix before GA
|
||||||
|
|
||||||
[02:05:38](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7538s) Providing Feedback
|
[02:05:38](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7538s) Providing Feedback
|
||||||
|
|
||||||
[02:06:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7603s) Thank you
|
[02:06:43](https://www.youtube.com/watch?v=oozG1aVcg9M&t=7603s) Thank you
|
||||||
|
|||||||
Reference in New Issue
Block a user