mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08c9214976 | |||
| c110dcd40e | |||
| eaa3e1e6af | |||
| 6562d16ce5 | |||
| 15a5b16b39 | |||
| d9c0c9c68e | |||
| d1ca123104 | |||
| f37647599a | |||
| cb14e84a26 | |||
| 8d7e4d1066 | |||
| c30ed923b6 | |||
| 50713188bf | |||
| e2ccd11f07 | |||
| f3316a017b | |||
| bdf1b63833 | |||
| 3ef26f2918 | |||
| 372360d739 | |||
| dc5877f398 | |||
| 49b2113fe1 | |||
| 556cfa1ee3 | |||
| 1ab4093d54 | |||
| 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 |
+120
@@ -1,5 +1,125 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
# 2509.1 UI Preview
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### [Refactor: Enhance artifact cleanup for disabled features](https://github.com/rbalsleyMSFT/FFU/commit/1ab4093d54b7d9bda9f47d7819694e66ae8de357)
|
||||||
|
|
||||||
|
Renames `Remove-DisabledUpdates` to `Remove-DisabledArtifacts` to better reflect its expanded scope.
|
||||||
|
|
||||||
|
This function now also removes Office installation scripts and downloaded content if the Office installation is disabled via the `$InstallOffice` flag.
|
||||||
|
|
||||||
|
The function call is moved to run before app installations to ensure artifacts are removed prior to the installation phase.
|
||||||
|
|
||||||
|
### [Removes the VM workaround for MCT ESD builds](https://github.com/rbalsleyMSFT/FFU/commit/dc5877f398316969299ee03800f3d07c7d98a9ab)
|
||||||
|
|
||||||
|
Comments out the logic that forces app installation when building from a downloaded ESD file. This workaround was implemented to prevent an OOBE reboot loop but is no longer required. This should speed up scenarios where you want to download the ESD media, install the latest CU and .NET CU, and capture the FFU.
|
||||||
|
|
||||||
|
### [Update default disk size to 50GB in FFU scripts and UI](https://github.com/rbalsleyMSFT/FFU/commit/372360d7392ad945be0db889a68e1fff0ed3b5d6)
|
||||||
|
|
||||||
|
Changed the default disk size parameter from 30GB to 50GB in BuildFFUVM.ps1 and FFUUI.Core.psm1 to accommodate larger virtual machines.
|
||||||
|
Updated tooltip and default value in the UI XAML file to reflect the new disk size.
|
||||||
|
|
||||||
|
### [Adds auto-loading of previous configuration on startup](https://github.com/rbalsleyMSFT/FFU/commit/3ef26f2918977906ebe14e328f015ce4f1941dc3)
|
||||||
|
|
||||||
|
Implements a new feature to automatically load the previously saved environment when the UI is launched.
|
||||||
|
|
||||||
|
This improves user experience by restoring the last saved configuration, including selected applications and drivers, eliminating the need to manually reload them on each run.
|
||||||
|
|
||||||
|
The process loads the main `FFUConfig.json` and then proceeds to load associated Winget, BYO App, and Driver lists if they are defined. UI elements and checkboxes are updated accordingly to reflect the loaded state.
|
||||||
|
|
||||||
|
### [Improves UI state after environment autoload](https://github.com/rbalsleyMSFT/FFU/commit/bdf1b63833c83171aed63e8fc16702078ccd577b)
|
||||||
|
|
||||||
|
Updates the visibility of UI panels for Winget and drivers when a previous environment is automatically loaded.
|
||||||
|
|
||||||
|
This ensures that if Winget apps or driver models are present, their corresponding UI sections are made visible. Additionally, it updates the "select all" checkbox state for Winget results and attempts to pre-select the hardware make for loaded drivers.
|
||||||
|
|
||||||
|
### [Add restore defaults and centralize cleanup logic](https://github.com/rbalsleyMSFT/FFU/commit/f3316a017b73bf12cf1a66e3d03a63e29c437cb1)
|
||||||
|
|
||||||
|
Introduces a "Restore Defaults" feature in the UI to reset the environment. This action removes generated configuration files, ISOs, downloaded apps, updates, drivers, and FFUs.
|
||||||
|
|
||||||
|
The post-build cleanup logic is refactored from the main build script into a new common function. This new function is used by both the standard build process and the new restore defaults feature, promoting code reuse and simplifying maintenance.
|
||||||
|
|
||||||
|
### [Add option to dynamically build PE drivers](https://github.com/rbalsleyMSFT/FFU/commit/e2ccd11f07217b389f1622a69794224412e046e1)
|
||||||
|
|
||||||
|
Thanks to @JonasKloseBW for the original code for this in https://github.com/rbalsleyMSFT/FFU/pull/115
|
||||||
|
|
||||||
|
Introduces a new parameter, `UseDriversAsPEDrivers`, that allows WinPE drivers to be sourced directly from the main driver repository.
|
||||||
|
|
||||||
|
When enabled, the script scans all available drivers, parses their INF files, and copies only the essential driver types (e.g., storage, mouse, keyboard, touchpad, system devices) needed for WinPE. This eliminates the need to maintain a separate, manually curated `PEDrivers` folder.
|
||||||
|
|
||||||
|
The UI is updated with a new checkbox that becomes visible when "Copy PE Drivers" is selected, making this a sub-option. Parameter validation is also adjusted to support this new workflow.
|
||||||
|
|
||||||
|
### [Improve model name normalization for driver mapping](https://github.com/rbalsleyMSFT/FFU/commit/50713188bffcb64f1b0c1f9eb89e02a300e3de98)
|
||||||
|
|
||||||
|
Enhances the model name normalization function to better handle variations in hardware model strings. This change introduces specific rules to canonicalize "All-in-One" and screen size variants (e.g., "-in" or "inch") for more reliable matching against driver mapping rules.
|
||||||
|
|
||||||
|
Additionally, optimizes performance by normalizing the system model once before the comparison loop. Logging is also added to show the original and normalized model strings for easier debugging.
|
||||||
|
|
||||||
|
### [Defer cleanup of compressed driver source folders](https://github.com/rbalsleyMSFT/FFU/commit/c30ed923b68b933f719b9a2941043b813bf4fd3f)
|
||||||
|
|
||||||
|
Implements a deferred cleanup mechanism for driver source folders when they are compressed to a WIM and also used for WinPE.
|
||||||
|
|
||||||
|
When drivers are compressed, the original source folders are now preserved if they are also needed for WinPE driver injection. A marker file is created in these preserved folders.
|
||||||
|
|
||||||
|
A new cleanup step is added after the WinPE media creation to remove these preserved folders, ensuring they are available when needed but not left behind permanently.
|
||||||
|
|
||||||
|
### [Refactor config loading and improve error handling](https://github.com/rbalsleyMSFT/FFU/commit/8d7e4d106620761d0ae1a5133f6d6ba301131471)
|
||||||
|
|
||||||
|
Extracts the logic for importing supplemental assets (Winget, BYO, Drivers) into a new reusable function. This function is now called by both the manual and automatic configuration loaders, reducing code duplication.
|
||||||
|
|
||||||
|
Enhances the manual configuration loading process with more robust error handling. It now provides specific user-facing error messages for file read failures, empty files, and invalid JSON, improving the user experience when loading a malformed configuration.
|
||||||
|
|
||||||
|
When loading a configuration, if optional supplemental files like AppList.json are referenced but not found, an informational message is now displayed to the user instead of failing silently.
|
||||||
|
|
||||||
|
### [Add robust sanitization for names used in paths](https://github.com/rbalsleyMSFT/FFU/commit/cb14e84a26acaf5863aa3bb094dbf18424798875)
|
||||||
|
|
||||||
|
Introduces a new common function, `ConvertTo-SafeName`, to sanitize strings by removing characters that are invalid in Windows file paths.
|
||||||
|
|
||||||
|
This function is now used consistently when creating directory and file names for drivers (Dell, HP, Lenovo, Microsoft) and applications to prevent path-related errors. It replaces several ad-hoc sanitization methods with a single, more robust implementation.
|
||||||
|
|
||||||
|
### [Includes exit code fields when using Copy Apps button](https://github.com/rbalsleyMSFT/FFU/commit/f37647599a318da29b62154bebff8c8a857d3002)
|
||||||
|
|
||||||
|
Adds persistence of AdditionalExitCodes and IgnoreNonZeroExitCodes when exporting the UI list to prevent losing custom exit handling settings and maintain parity with the primary save routine.
|
||||||
|
|
||||||
|
### [Sanitizes app names for storage and paths](https://github.com/rbalsleyMSFT/FFU/commit/d1ca1231045e38316733495e1fdb8590a225be67)
|
||||||
|
|
||||||
|
Applies name sanitization when persisting the app list and when building/checking Win32 and Store download directories.
|
||||||
|
Prevents invalid characters in folder names, aligns persisted names with on-disk structure, and improves detection of existing content to avoid redundant downloads and errors.
|
||||||
|
|
||||||
|
### [Adds exit-code overrides and UI for winget apps](https://github.com/rbalsleyMSFT/FFU/commit/d9c0c9c68ee1769230c9789b5c7cb84bcff4d642)
|
||||||
|
|
||||||
|
Adds per-app control for additional accepted exit codes and ignoring non‑zero exit codes to improve handling of installers with nonstandard returns.
|
||||||
|
|
||||||
|
Exposes editable fields in the app list UI, persists them across search defaults, import/export, and pre-download save, and applies overrides during app resolution to honor configured behavior.
|
||||||
|
|
||||||
|
### [Adds UI/CLI to copy additional FFUs to USB build](https://github.com/rbalsleyMSFT/FFU/commit/15a5b16b39887b71ae545c638d57183c97bdf629)
|
||||||
|
|
||||||
|
- Enables selecting multiple existing FFU images to include on the deployment USB for easier distribution and testing.
|
||||||
|
- Adds a UI option with selectable, sortable list from the capture folder, refresh support, and persisted selections.
|
||||||
|
- Validates that selections exist when the option is enabled to prevent empty runs.
|
||||||
|
- Supports unattended/CLI flows by prompting early or accepting a preselected list for USB creation; deduplicates and logs chosen files.
|
||||||
|
- Always includes the just-built (or latest available) FFU as a base.
|
||||||
|
- Improves no-FFU handling and streamlines multi-FFU selection workflow.
|
||||||
|
|
||||||
|
### [Standardizes JSON output: depth, UTF-8, key order](https://github.com/rbalsleyMSFT/FFU/commit/6562d16ce500197b428b51915332c6649df302df)
|
||||||
|
|
||||||
|
- Sorts top-level config keys before serialization for deterministic files and cleaner diffs.
|
||||||
|
- Increases JSON depth to 10 to retain nested settings.
|
||||||
|
- Writes JSON as UTF-8 via Set-Content for consistent encoding.
|
||||||
|
- Applies across config export and UI save flows.
|
||||||
|
|
||||||
|
### [Adds Windows 11 25H2 mapping](https://github.com/rbalsleyMSFT/FFU/commit/eaa3e1e6af5c25e0f8b185f8107e017782b0f00f)
|
||||||
|
|
||||||
|
Extends supported Windows 11 releases to include 25H2. Default is still 24H2.
|
||||||
|
|
||||||
|
* Update USBImagingToolCreator.ps1 by @jrollmann in https://github.com/rbalsleyMSFT/FFU/pull/262
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
|
||||||
|
* @jrollmann made their first contribution in https://github.com/rbalsleyMSFT/FFU/pull/262
|
||||||
|
|
||||||
# 2507.1 UI Preview
|
# 2507.1 UI Preview
|
||||||
|
|
||||||
Waaay too many to list. Just watch the Youtube video in the Readme :)
|
Waaay too many to list. Just watch the Youtube video in the Readme :)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -109,16 +138,59 @@ function Install-Applications {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check for 'PAUSE' command
|
||||||
|
if ($app.CommandLine -eq 'PAUSE') {
|
||||||
|
Write-Host "Pausing script as requested by '$($app.Name)'. Press Enter to continue..."
|
||||||
|
$null = Read-Host
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Construct the argument list properly, handling potential array vs string
|
# Normalize arguments: treat null/empty/whitespace as no arguments
|
||||||
$argumentsToPass = if ($app.Arguments -is [array]) { $app.Arguments } else { @($app.Arguments) }
|
$argumentsToPass = $null
|
||||||
|
if ($null -ne $app.Arguments) {
|
||||||
Write-Host "Running command: $($app.CommandLine) $($argumentsToPass -join ' ')"
|
if ($app.Arguments -is [array]) {
|
||||||
$result = Invoke-Process -FilePath $($app.CommandLine) -ArgumentList $argumentsToPass
|
$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
|
||||||
|
}
|
||||||
|
|||||||
+1168
-181
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"
|
||||||
@@ -123,6 +126,14 @@ $window.Add_Loaded({
|
|||||||
Initialize-UIDefaults -State $script:uiState
|
Initialize-UIDefaults -State $script:uiState
|
||||||
Initialize-DynamicUIElements -State $script:uiState
|
Initialize-DynamicUIElements -State $script:uiState
|
||||||
Register-EventHandlers -State $script:uiState
|
Register-EventHandlers -State $script:uiState
|
||||||
|
|
||||||
|
# Attempt automatic load of previous environment (silent)
|
||||||
|
try {
|
||||||
|
Invoke-AutoLoadPreviousEnvironment -State $script:uiState
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Auto-load previous environment failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +143,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
|
||||||
@@ -151,8 +400,21 @@ $script:uiState.Controls.btnRun.Add_Click({
|
|||||||
|
|
||||||
# Gather config on the UI thread before starting the job
|
# Gather config on the UI thread before starting the job
|
||||||
$config = Get-UIConfig -State $script:uiState
|
$config = Get-UIConfig -State $script:uiState
|
||||||
|
|
||||||
|
# Validate Additional FFU selection if enabled
|
||||||
|
if ($config.BuildUSBDrive -and $config.CopyAdditionalFFUFiles -and (($null -eq $config.AdditionalFFUFiles) -or ($config.AdditionalFFUFiles.Count -eq 0))) {
|
||||||
|
[System.Windows.MessageBox]::Show("Please select at least one additional FFU file to copy, or uncheck 'Copy Additional FFU Files'.", "Selection Required", "OK", "Warning") | Out-Null
|
||||||
|
$btnRun.IsEnabled = $true
|
||||||
|
$script:uiState.Controls.txtStatus.Text = "Build canceled: Additional FFU selection required."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
$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
|
# Sort top-level keys alphabetically for consistent output
|
||||||
|
$sortedConfig = [ordered]@{}
|
||||||
|
foreach ($k in ($config.Keys | Sort-Object)) { $sortedConfig[$k] = $config[$k] }
|
||||||
|
$sortedConfig | 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 +545,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 +576,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 +616,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -115,9 +115,9 @@
|
|||||||
<TextBox x:Name="txtVMHostIPAddress" Grid.Row="2" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch"/>
|
<TextBox x:Name="txtVMHostIPAddress" Grid.Row="2" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch"/>
|
||||||
<!-- Row 3: Disk Size (GB) -->
|
<!-- Row 3: Disk Size (GB) -->
|
||||||
<StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
|
<StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
|
||||||
<TextBlock Text="Disk Size (GB)" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk."/>
|
<TextBlock Text="Disk Size (GB)" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBox x:Name="txtDiskSize" Grid.Row="3" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="30" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk."/>
|
<TextBox x:Name="txtDiskSize" Grid.Row="3" Grid.Column="1" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="50" ToolTip="Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk."/>
|
||||||
<!-- Row 4: Memory (GB) -->
|
<!-- Row 4: Memory (GB) -->
|
||||||
<StackPanel Grid.Row="4" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
|
<StackPanel Grid.Row="4" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5">
|
||||||
<TextBlock Text="Memory (GB)" ToolTip="Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Default is 4GB."/>
|
<TextBlock Text="Memory (GB)" ToolTip="Amount of memory to allocate for the virtual machine. Recommended to use 8GB if possible, especially for Windows 11. Default is 4GB."/>
|
||||||
@@ -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>
|
||||||
@@ -639,9 +628,10 @@
|
|||||||
<CheckBox x:Name="chkCompressDriversToWIM" Content="Compress Driver Model Folder to WIM" Margin="0,0,5,0" ToolTip="When set to $true, will compress each downloaded driver model folder into a separate WIM file within the Drivers folder. This is useful with Copy Drivers to USB drive."/>
|
<CheckBox x:Name="chkCompressDriversToWIM" Content="Compress Driver Model Folder to WIM" Margin="0,0,5,0" ToolTip="When set to $true, will compress each downloaded driver model folder into a separate WIM file within the Drivers folder. This is useful with Copy Drivers to USB drive."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Row 12: Copy PE Drivers Checkbox -->
|
<!-- Row 12: PE Driver Options (UseDriversAsPEDrivers is a dependent sub-option) -->
|
||||||
<StackPanel Grid.Row="12" Orientation="Horizontal" Margin="5">
|
<StackPanel Grid.Row="12" Margin="5">
|
||||||
<CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,5,0" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/>
|
<CheckBox x:Name="chkCopyPEDrivers" Content="Copy PE Drivers" Margin="0,0,0,5" ToolTip="When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false."/>
|
||||||
|
<CheckBox x:Name="chkUseDriversAsPEDrivers" Content="Use Drivers Folder as PE Drivers Source" Margin="25,0,0,0" Visibility="Collapsed" ToolTip="When set to $true (and Copy PE Drivers is also checked), bypasses the PE Drivers Folder path and instead scans the Drivers folder to gather only required WinPE drivers. Hidden unless Copy PE Drivers is checked."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
@@ -751,6 +741,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>
|
||||||
|
|
||||||
@@ -765,6 +756,35 @@
|
|||||||
<CheckBox x:Name="chkCopyAutopilot" Content="Copy Autopilot Profile" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Autopilot profile to the USB drive."/>
|
<CheckBox x:Name="chkCopyAutopilot" Content="Copy Autopilot Profile" Margin="5" VerticalAlignment="Center" Tag="When set to $true, will copy the Autopilot profile 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="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."/>
|
||||||
|
<CheckBox x:Name="chkCopyAdditionalFFUFiles" Content="Copy Additional FFU Files" Margin="5" VerticalAlignment="Center" Tag="When set to $true, allows selecting existing FFU files in the capture folder to also copy to the USB drive."/>
|
||||||
|
|
||||||
|
<!-- Additional FFU Selection Section -->
|
||||||
|
<Grid x:Name="additionalFFUPanel" Margin="5,0,0,10" Visibility="Collapsed">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Header row -->
|
||||||
|
<DockPanel Grid.Row="0" Margin="0,5" LastChildFill="False">
|
||||||
|
<TextBlock Text="Additional FFU Files" DockPanel.Dock="Left" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<Button x:Name="btnRefreshAdditionalFFUs" Content="Refresh" DockPanel.Dock="Left" Padding="10,5" ToolTip="Refresh the list of FFU files from the capture folder"/>
|
||||||
|
</DockPanel>
|
||||||
|
<!-- ListView row -->
|
||||||
|
<ListView x:Name="lstAdditionalFFUs" Grid.Row="1" Margin="0,5" Height="150">
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Header="FFU Name" DisplayMemberBinding="{Binding Name}" Width="300"/>
|
||||||
|
<GridViewColumn Header="Last Modified" DisplayMemberBinding="{Binding LastModified}" Width="200"/>
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
@@ -820,6 +840,7 @@
|
|||||||
<TextBlock x:Name="txtStatus" Grid.Row="2" Margin="0,5,0,0"/>
|
<TextBlock x:Name="txtStatus" Grid.Row="2" Margin="0,5,0,0"/>
|
||||||
<!-- Buttons (Build Config File / Load Config File / Build FFU) -->
|
<!-- Buttons (Build Config File / Load Config File / Build FFU) -->
|
||||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,20,20">
|
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,20,20">
|
||||||
|
<Button x:Name="btnRestoreDefaults" Content="Restore Defaults" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
|
||||||
<Button x:Name="btnBuildConfig" Content="Save Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
|
<Button x:Name="btnBuildConfig" Content="Save Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
|
||||||
<Button x:Name="btnLoadConfig" Content="Load Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
|
<Button x:Name="btnLoadConfig" Content="Load Config File" Width="150" Margin="0,0,10,0" FontSize="14" Padding="10,5"/>
|
||||||
<Button x:Name="btnRun" Content="Build FFU" Width="120" FontSize="14" Padding="10,5"/>
|
<Button x:Name="btnRun" Content="Build FFU" Width="120" FontSize="14" Padding="10,5"/>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Provides shared cleanup functionality for both UI and build script.
|
||||||
|
|
||||||
|
function Invoke-FFUPostBuildCleanup {
|
||||||
|
param(
|
||||||
|
[string]$RootPath,
|
||||||
|
[string]$AppsPath,
|
||||||
|
[string]$DriversPath,
|
||||||
|
[string]$FFUCapturePath,
|
||||||
|
[string]$CaptureISOPath,
|
||||||
|
[string]$DeployISOPath,
|
||||||
|
[string]$AppsISOPath,
|
||||||
|
[bool]$RemoveCaptureISO = $false,
|
||||||
|
[bool]$RemoveDeployISO = $false,
|
||||||
|
[bool]$RemoveAppsISO = $false,
|
||||||
|
[bool]$RemoveDrivers = $false,
|
||||||
|
[bool]$RemoveFFU = $false,
|
||||||
|
[bool]$RemoveApps = $false,
|
||||||
|
[bool]$RemoveUpdates = $false
|
||||||
|
)
|
||||||
|
$originalProgressPreference = $ProgressPreference
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
try {
|
||||||
|
WriteLog "CommonCleanup: Starting cleanup (CaptureISO=$RemoveCaptureISO DeployISO=$RemoveDeployISO AppsISO=$RemoveAppsISO Drivers=$RemoveDrivers FFU=$RemoveFFU Apps=$RemoveApps Updates=$RemoveUpdates)."
|
||||||
|
|
||||||
|
# Primary ISO paths (new naming/location)
|
||||||
|
if ($RemoveCaptureISO -and -not [string]::IsNullOrWhiteSpace($CaptureISOPath) -and (Test-Path -LiteralPath $CaptureISOPath)) {
|
||||||
|
WriteLog "CommonCleanup: Removing $CaptureISOPath"
|
||||||
|
try { Remove-Item -LiteralPath $CaptureISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $CaptureISOPath : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
if ($RemoveDeployISO -and -not [string]::IsNullOrWhiteSpace($DeployISOPath) -and (Test-Path -LiteralPath $DeployISOPath)) {
|
||||||
|
WriteLog "CommonCleanup: Removing $DeployISOPath"
|
||||||
|
try { Remove-Item -LiteralPath $DeployISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $DeployISOPath : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
if ($RemoveAppsISO -and -not [string]::IsNullOrWhiteSpace($AppsISOPath) -and (Test-Path -LiteralPath $AppsISOPath)) {
|
||||||
|
WriteLog "CommonCleanup: Removing $AppsISOPath"
|
||||||
|
try { Remove-Item -LiteralPath $AppsISOPath -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $AppsISOPath : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Legacy / root-level WinPE ISOs (pattern-based)
|
||||||
|
if ($RemoveCaptureISO) {
|
||||||
|
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Capture*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
try { WriteLog "CommonCleanup: Removing legacy capture ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy capture ISO $($_.FullName) : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($RemoveDeployISO) {
|
||||||
|
Get-ChildItem -LiteralPath $RootPath -Filter 'WinPE_FFU_Deploy*.iso' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
try { WriteLog "CommonCleanup: Removing legacy deploy ISO $($_.FullName)"; Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing legacy deploy ISO $($_.FullName) : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveDrivers -and -not [string]::IsNullOrWhiteSpace($DriversPath) -and (Test-Path -LiteralPath $DriversPath -PathType Container)) {
|
||||||
|
WriteLog "CommonCleanup: Removing contents of $DriversPath"
|
||||||
|
try { Get-ChildItem -LiteralPath $DriversPath -Force -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } catch { WriteLog "CommonCleanup: Driver content cleanup issue: $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveFFU -and -not [string]::IsNullOrWhiteSpace($FFUCapturePath) -and (Test-Path -LiteralPath $FFUCapturePath -PathType Container)) {
|
||||||
|
WriteLog "CommonCleanup: Removing FFU files in $FFUCapturePath"
|
||||||
|
Get-ChildItem -LiteralPath $FFUCapturePath -Filter *.ffu -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing FFU $($_.FullName) : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveApps -and -not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath -PathType Container)) {
|
||||||
|
$win32 = Join-Path $AppsPath 'Win32'
|
||||||
|
$store = Join-Path $AppsPath 'MSStore'
|
||||||
|
if (Test-Path -LiteralPath $win32) {
|
||||||
|
WriteLog "CommonCleanup: Removing $win32"
|
||||||
|
try { Remove-Item -LiteralPath $win32 -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $win32 : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $store) {
|
||||||
|
WriteLog "CommonCleanup: Removing $store"
|
||||||
|
try { Remove-Item -LiteralPath $store -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $store : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
$office = Join-Path $AppsPath 'Office'
|
||||||
|
if (Test-Path -LiteralPath $office) {
|
||||||
|
WriteLog "CommonCleanup: Cleaning Office artifacts"
|
||||||
|
$officeSub = Join-Path $office 'Office'
|
||||||
|
if (Test-Path -LiteralPath $officeSub) {
|
||||||
|
try { Remove-Item -LiteralPath $officeSub -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $officeSub : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
$setupExe = Join-Path $office 'setup.exe'
|
||||||
|
if (Test-Path -LiteralPath $setupExe) {
|
||||||
|
try { Remove-Item -LiteralPath $setupExe -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $setupExe : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RemoveUpdates -and -not [string]::IsNullOrWhiteSpace($AppsPath) -and (Test-Path -LiteralPath $AppsPath)) {
|
||||||
|
$updateDirs = @('Defender', 'Edge', 'MSRT', 'OneDrive', '.NET', 'CU', 'Microcode')
|
||||||
|
foreach ($d in $updateDirs) {
|
||||||
|
$target = Join-Path $AppsPath $d
|
||||||
|
if (Test-Path -LiteralPath $target) {
|
||||||
|
WriteLog "CommonCleanup: Removing update folder $target"
|
||||||
|
try { Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction Stop } catch { WriteLog "CommonCleanup: Failed removing $target : $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "CommonCleanup: Completed."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "CommonCleanup: Fatal cleanup error $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$ProgressPreference = $originalProgressPreference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-FFUPostBuildCleanup
|
||||||
@@ -194,4 +194,22 @@ function Set-Progress {
|
|||||||
WriteLog "[PROGRESS] $Percentage | $Message"
|
WriteLog "[PROGRESS] $Percentage | $Message"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConvertTo-SafeName {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
# Replace invalid Windows filename characters (<>:"/\|?* and control chars) with a dash
|
||||||
|
$sanitized = $Name -replace '[<>:\"/\\|?*\x00-\x1F]', '-'
|
||||||
|
# Collapse multiple consecutive dashes
|
||||||
|
$sanitized = $sanitized -replace '-{2,}', '-'
|
||||||
|
# Trim leading/trailing spaces, periods, and dashes
|
||||||
|
$sanitized = $sanitized.Trim(' ','.','-')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($sanitized)) {
|
||||||
|
$sanitized = 'Unnamed'
|
||||||
|
}
|
||||||
|
return $sanitized
|
||||||
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function *
|
Export-ModuleMember -Function *
|
||||||
@@ -22,7 +22,10 @@ function Compress-DriverFolderToWim {
|
|||||||
[string]$WimName, # Optional, defaults to folder name
|
[string]$WimName, # Optional, defaults to folder name
|
||||||
|
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[string]$WimDescription # Optional, defaults to folder name
|
[string]$WimDescription, # Optional, defaults to folder name
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$PreserveSource = $false # When $true, do not delete source folder; create marker for deferred cleanup
|
||||||
)
|
)
|
||||||
|
|
||||||
WriteLog "Starting compression of folder '$SourceFolderPath' to '$DestinationWimPath'."
|
WriteLog "Starting compression of folder '$SourceFolderPath' to '$DestinationWimPath'."
|
||||||
@@ -66,14 +69,29 @@ function Compress-DriverFolderToWim {
|
|||||||
WriteLog "Successfully compressed '$SourceFolderPath' to '$DestinationWimPath' using dism.exe."
|
WriteLog "Successfully compressed '$SourceFolderPath' to '$DestinationWimPath' using dism.exe."
|
||||||
|
|
||||||
# Remove the source folder after successful compression
|
# Remove the source folder after successful compression
|
||||||
WriteLog "Removing source driver folder: $SourceFolderPath"
|
if ($PreserveSource) {
|
||||||
try {
|
WriteLog "Preserving source driver folder for deferred WinPE driver harvesting: $SourceFolderPath"
|
||||||
Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop
|
try {
|
||||||
WriteLog "Successfully removed source folder '$SourceFolderPath'."
|
$markerFile = Join-Path -Path $SourceFolderPath -ChildPath '__PreservedForPEDrivers.txt'
|
||||||
|
if (-not (Test-Path -Path $markerFile -PathType Leaf)) {
|
||||||
|
New-Item -Path $markerFile -ItemType File -Force | Out-Null
|
||||||
|
WriteLog "Created preservation marker file: $markerFile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to create preservation marker in $SourceFolderPath. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
else {
|
||||||
WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)"
|
WriteLog "Removing source driver folder: $SourceFolderPath"
|
||||||
# Do not fail the whole operation, just log a warning.
|
try {
|
||||||
|
Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop
|
||||||
|
WriteLog "Successfully removed source folder '$SourceFolderPath'."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)"
|
||||||
|
# Do not fail the whole operation, just log a warning.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $true # Indicate success
|
return $true # Indicate success
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -208,7 +209,8 @@ function Invoke-ParallelProcessing {
|
|||||||
-Headers $localJobArgs['Headers'] `
|
-Headers $localJobArgs['Headers'] `
|
||||||
-UserAgent $localJobArgs['UserAgent'] `
|
-UserAgent $localJobArgs['UserAgent'] `
|
||||||
-ProgressQueue $localProgressQueue `
|
-ProgressQueue $localProgressQueue `
|
||||||
-CompressToWim $localJobArgs['CompressToWim']
|
-CompressToWim $localJobArgs['CompressToWim'] `
|
||||||
|
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
|
||||||
}
|
}
|
||||||
'Dell' {
|
'Dell' {
|
||||||
$taskResult = Save-DellDriversTask -DriverItemData $currentItem `
|
$taskResult = Save-DellDriversTask -DriverItemData $currentItem `
|
||||||
@@ -216,7 +218,8 @@ function Invoke-ParallelProcessing {
|
|||||||
-WindowsArch $localJobArgs['WindowsArch'] `
|
-WindowsArch $localJobArgs['WindowsArch'] `
|
||||||
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
||||||
-ProgressQueue $localProgressQueue `
|
-ProgressQueue $localProgressQueue `
|
||||||
-CompressToWim $localJobArgs['CompressToWim']
|
-CompressToWim $localJobArgs['CompressToWim'] `
|
||||||
|
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
|
||||||
}
|
}
|
||||||
'HP' {
|
'HP' {
|
||||||
$taskResult = Save-HPDriversTask -DriverItemData $currentItem `
|
$taskResult = Save-HPDriversTask -DriverItemData $currentItem `
|
||||||
@@ -225,7 +228,8 @@ function Invoke-ParallelProcessing {
|
|||||||
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
-WindowsRelease $localJobArgs['WindowsRelease'] `
|
||||||
-WindowsVersion $localJobArgs['WindowsVersion'] `
|
-WindowsVersion $localJobArgs['WindowsVersion'] `
|
||||||
-ProgressQueue $localProgressQueue `
|
-ProgressQueue $localProgressQueue `
|
||||||
-CompressToWim $localJobArgs['CompressToWim']
|
-CompressToWim $localJobArgs['CompressToWim'] `
|
||||||
|
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
|
||||||
}
|
}
|
||||||
'Lenovo' {
|
'Lenovo' {
|
||||||
$taskResult = Save-LenovoDriversTask -DriverItemData $currentItem `
|
$taskResult = Save-LenovoDriversTask -DriverItemData $currentItem `
|
||||||
@@ -234,7 +238,8 @@ function Invoke-ParallelProcessing {
|
|||||||
-Headers $localJobArgs['Headers'] `
|
-Headers $localJobArgs['Headers'] `
|
||||||
-UserAgent $localJobArgs['UserAgent'] `
|
-UserAgent $localJobArgs['UserAgent'] `
|
||||||
-ProgressQueue $localProgressQueue `
|
-ProgressQueue $localProgressQueue `
|
||||||
-CompressToWim $localJobArgs['CompressToWim']
|
-CompressToWim $localJobArgs['CompressToWim'] `
|
||||||
|
-PreserveSourceOnCompress $localJobArgs['PreserveSourceOnCompress']
|
||||||
}
|
}
|
||||||
default {
|
default {
|
||||||
$unsupportedMakeMessage = "Error: Unsupported Make '$make' for driver download."
|
$unsupportedMakeMessage = "Error: Unsupported Make '$make' for driver download."
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -83,11 +108,13 @@ function Get-Application {
|
|||||||
|
|
||||||
# Determine app type and folder path
|
# Determine app type and folder path
|
||||||
$appIsWin32 = ($Source -eq 'msstore' -and $AppId.StartsWith("XP"))
|
$appIsWin32 = ($Source -eq 'msstore' -and $AppId.StartsWith("XP"))
|
||||||
|
$sanitizedAppName = ConvertTo-SafeName -Name $AppName
|
||||||
|
if ($sanitizedAppName -ne $AppName) { WriteLog "Sanitized app name: '$AppName' -> '$sanitizedAppName'" }
|
||||||
if ($Source -eq 'winget' -or $appIsWin32) {
|
if ($Source -eq 'winget' -or $appIsWin32) {
|
||||||
$appBaseFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $AppName
|
$appBaseFolderPath = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$appBaseFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $AppName
|
$appBaseFolderPath = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
|
||||||
}
|
}
|
||||||
|
|
||||||
# If downloading multiple archs for a Win32 app, create a subfolder
|
# If downloading multiple archs for a Win32 app, create a subfolder
|
||||||
@@ -196,8 +223,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 +255,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 +391,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): $_"
|
||||||
@@ -314,11 +405,11 @@ function Get-Apps {
|
|||||||
if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) {
|
if (-not (Test-Path -Path $storeAppsFolder -PathType Container)) {
|
||||||
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
|
New-Item -Path $storeAppsFolder -ItemType Directory -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +417,76 @@ 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))
|
||||||
|
$hasAdd = ($app.PSObject.Properties['AdditionalExitCodes'] -and -not [string]::IsNullOrWhiteSpace($app.AdditionalExitCodes))
|
||||||
|
$hasIgnore = ($app.PSObject.Properties['IgnoreNonZeroExitCodes'])
|
||||||
|
if ($hasCmd -or $hasArgs -or $hasAdd -or $hasIgnore) {
|
||||||
|
$overrideMap[$app.name] = @{
|
||||||
|
CommandLine = if ($hasCmd) { $app.CommandLine } else { $null }
|
||||||
|
Arguments = if ($hasArgs) { $app.Arguments } else { $null }
|
||||||
|
AdditionalExitCodes = if ($hasAdd) { $app.AdditionalExitCodes } else { $null }
|
||||||
|
IgnoreNonZeroExitCodes = if ($hasIgnore) { [bool]$app.IgnoreNonZeroExitCodes } 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 ($ov.ContainsKey('AdditionalExitCodes') -and $null -ne $ov.AdditionalExitCodes) {
|
||||||
|
WriteLog "Override (AppList.json) AdditionalExitCodes for $($entry.Name)"
|
||||||
|
$entry | Add-Member -NotePropertyName AdditionalExitCodes -NotePropertyValue $ov.AdditionalExitCodes -Force
|
||||||
|
$changed = $true
|
||||||
|
}
|
||||||
|
if ($ov.ContainsKey('IgnoreNonZeroExitCodes') -and $null -ne $ov.IgnoreNonZeroExitCodes) {
|
||||||
|
WriteLog "Override (AppList.json) IgnoreNonZeroExitCodes for $($entry.Name)"
|
||||||
|
$entry | Add-Member -NotePropertyName IgnoreNonZeroExitCodes -NotePropertyValue ([bool]$ov.IgnoreNonZeroExitCodes) -Force
|
||||||
|
$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,42 +570,153 @@ 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") {
|
|
||||||
$silentInstallCommand = "msiexec"
|
|
||||||
$silentInstallSwitch = "/i `"$basePath\$installer`" $silentInstallSwitch"
|
|
||||||
}
|
}
|
||||||
|
elseif ($installerExt -ieq ".msi") {
|
||||||
|
$silentInstallCommand = "msiexec"
|
||||||
|
$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
|
||||||
$wingetWin32AppsJson = "$OrchestrationPath\WinGetWin32Apps.json"
|
$wingetWin32AppsJson = "$OrchestrationPath\WinGetWin32Apps.json"
|
||||||
|
|
||||||
# Initialize or load existing JSON data
|
# Initialize or load existing JSON data
|
||||||
if (Test-Path -Path $wingetWin32AppsJson) {
|
if (Test-Path -Path $wingetWin32AppsJson) {
|
||||||
[array]$appsData = Get-Content -Path $wingetWin32AppsJson -Raw | ConvertFrom-Json
|
[array]$appsData = Get-Content -Path $wingetWin32AppsJson -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
# Get highest priority value
|
# Get highest priority value
|
||||||
if ($appsData.Count -gt 0) {
|
if ($appsData.Count -gt 0) {
|
||||||
$highestPriority = $appsData.Count + 1
|
$highestPriority = $appsData.Count + 1
|
||||||
@@ -454,7 +726,7 @@ function Add-Win32SilentInstallCommand {
|
|||||||
$appsData = @()
|
$appsData = @()
|
||||||
$highestPriority = 1
|
$highestPriority = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create new app entry
|
# Create new app entry
|
||||||
$newApp = [PSCustomObject]@{
|
$newApp = [PSCustomObject]@{
|
||||||
Priority = $highestPriority
|
Priority = $highestPriority
|
||||||
@@ -462,12 +734,12 @@ function Add-Win32SilentInstallCommand {
|
|||||||
CommandLine = $silentInstallCommand
|
CommandLine = $silentInstallCommand
|
||||||
Arguments = $silentInstallSwitch
|
Arguments = $silentInstallSwitch
|
||||||
}
|
}
|
||||||
|
|
||||||
$appsData += $newApp
|
$appsData += $newApp
|
||||||
$appsData | ConvertTo-Json -Depth 10 | Set-Content -Path $wingetWin32AppsJson
|
$appsData | ConvertTo-Json -Depth 10 | Set-Content -Path $wingetWin32AppsJson
|
||||||
|
|
||||||
WriteLog "Added $($newApp.Name) to WinGetWin32Apps.json with priority $highestPriority"
|
WriteLog "Added $($newApp.Name) to WinGetWin32Apps.json with priority $highestPriority"
|
||||||
|
|
||||||
# Return 0 for success
|
# Return 0 for success
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ Description = 'Common functions shared between FFU Builder UI and the BuildFFUVM
|
|||||||
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
||||||
NestedModules = @('FFU.Common.Drivers.psm1',
|
NestedModules = @('FFU.Common.Drivers.psm1',
|
||||||
'FFU.Common.Winget.psm1',
|
'FFU.Common.Winget.psm1',
|
||||||
'FFU.Common.Parallel.psm1')
|
'FFU.Common.Parallel.psm1',
|
||||||
|
'FFU.Common.Cleanup.psm1')
|
||||||
|
|
||||||
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
|
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
|
||||||
FunctionsToExport = '*'
|
FunctionsToExport = '*'
|
||||||
|
|||||||
@@ -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,8 +375,8 @@ 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 {
|
||||||
[System.Windows.MessageBox]::Show("Failed to import applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
|
[System.Windows.MessageBox]::Show("Failed to import applications: $_", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
|
||||||
@@ -230,8 +396,8 @@ function Invoke-CopyBYOApps {
|
|||||||
|
|
||||||
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 CopyStatus when saving and ensure Priority is an integer; include AdditionalExitCodes and IgnoreNonZeroExitCodes for parity with Save-BYOApplicationList
|
||||||
$applications = $listView.Items | Sort-Object Priority | Select-Object @{N = 'Priority'; E = { [int]$_.Priority } }, Name, CommandLine, Arguments, Source
|
$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 $userAppListPath -Force -Encoding UTF8
|
$applications | ConvertTo-Json -Depth 5 | Set-Content -Path $userAppListPath -Force -Encoding UTF8
|
||||||
WriteLog "Successfully updated UserAppList.json with all applications from the UI."
|
WriteLog "Successfully updated UserAppList.json with all applications from the UI."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,13 @@ function Get-UIConfig {
|
|||||||
CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked
|
CopyDrivers = $State.Controls.chkCopyDrivers.IsChecked
|
||||||
CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked
|
CopyOfficeConfigXML = $State.Controls.chkCopyOfficeConfigXML.IsChecked
|
||||||
CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked
|
CopyPEDrivers = $State.Controls.chkCopyPEDrivers.IsChecked
|
||||||
|
UseDriversAsPEDrivers = $State.Controls.chkUseDriversAsPEDrivers.IsChecked
|
||||||
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
|
CopyPPKG = $State.Controls.chkCopyPPKG.IsChecked
|
||||||
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
CopyUnattend = $State.Controls.chkCopyUnattend.IsChecked
|
||||||
|
CopyAdditionalFFUFiles = $State.Controls.chkCopyAdditionalFFUFiles.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 +73,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 +96,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
|
||||||
@@ -112,6 +116,16 @@ function Get-UIConfig {
|
|||||||
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
|
$State.Controls.lstUSBDrives.Items | Where-Object { $_.IsSelected } | ForEach-Object {
|
||||||
$config.USBDriveList[$_.Model] = $_.SerialNumber
|
$config.USBDriveList[$_.Model] = $_.SerialNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Additional FFU file selections
|
||||||
|
$config.AdditionalFFUFiles = @()
|
||||||
|
if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
|
||||||
|
$config.AdditionalFFUFiles = @(
|
||||||
|
$State.Controls.lstAdditionalFFUs.Items |
|
||||||
|
Where-Object { $_.IsSelected } |
|
||||||
|
ForEach-Object { $_.FullName }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return $config
|
return $config
|
||||||
}
|
}
|
||||||
@@ -240,19 +254,39 @@ function Invoke-LoadConfiguration {
|
|||||||
WriteLog "Load configuration cancelled by user."
|
WriteLog "Load configuration cancelled by user."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteLog "Loading configuration from: $filePath"
|
WriteLog "Loading configuration from: $filePath"
|
||||||
$configContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json
|
$raw = $null
|
||||||
|
try {
|
||||||
|
$raw = Get-Content -Path $filePath -Raw -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "LoadConfig Error: Failed reading file $filePath : $($_.Exception.Message)"
|
||||||
|
[System.Windows.MessageBox]::Show("Failed to read the configuration file.`n$($_.Exception.Message)", "Load Error", "OK", "Error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
WriteLog "LoadConfig Error: File $filePath is empty."
|
||||||
|
[System.Windows.MessageBox]::Show("The selected configuration file is empty.", "Load Error", "OK", "Error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$configContent = $null
|
||||||
|
try {
|
||||||
|
$configContent = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "LoadConfig Error: JSON parse failure for $filePath : $($_.Exception.Message)"
|
||||||
|
[System.Windows.MessageBox]::Show("Failed to parse the configuration file (invalid JSON).`n$($_.Exception.Message)", "Load Error", "OK", "Error")
|
||||||
|
return
|
||||||
|
}
|
||||||
if ($null -eq $configContent) {
|
if ($null -eq $configContent) {
|
||||||
WriteLog "LoadConfig Error: configContent is null after parsing $filePath. File might be empty or malformed."
|
WriteLog "LoadConfig Error: Parsed config object is null after $filePath."
|
||||||
[System.Windows.MessageBox]::Show("Failed to parse the configuration file. It might be empty or not valid JSON.", "Load Error", "OK", "Error")
|
[System.Windows.MessageBox]::Show("Parsed configuration object was null.", "Load Error", "OK", "Error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')"
|
WriteLog "LoadConfig: Successfully parsed config file. Top-level keys: $($configContent.PSObject.Properties.Name -join ', ')"
|
||||||
|
|
||||||
# Apply the configuration to the UI
|
|
||||||
Update-UIFromConfig -ConfigContent $configContent -State $State
|
Update-UIFromConfig -ConfigContent $configContent -State $State
|
||||||
|
$State.Data.lastConfigFilePath = $filePath
|
||||||
|
Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$true
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())"
|
WriteLog "LoadConfig FATAL Error: $($_.Exception.ToString())"
|
||||||
@@ -260,6 +294,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 +363,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
|
||||||
@@ -284,15 +371,17 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowVHDXCaching' -State $State
|
Set-UIValue -ControlName 'chkAllowVHDXCaching' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowVHDXCaching' -State $State
|
||||||
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
|
Set-UIValue -ControlName 'chkAllowExternalHardDiskMedia' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'AllowExternalHardDiskMedia' -State $State
|
||||||
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 'chkCopyAdditionalFFUFiles' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAdditionalFFUFiles' -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)
|
||||||
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
|
Set-UIValue -ControlName 'chkCopyAutopilot' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyAutopilot' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
|
Set-UIValue -ControlName 'chkCopyUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyUnattend' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
|
Set-UIValue -ControlName 'chkCopyPPKG' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPPKG' -State $State
|
||||||
|
|
||||||
# Post Build Cleanup group (Build Tab)
|
# Post Build Cleanup group (Build Tab)
|
||||||
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
|
Set-UIValue -ControlName 'chkCleanupAppsISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupAppsISO' -State $State
|
||||||
Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupCaptureISO' -State $State
|
Set-UIValue -ControlName 'chkCleanupCaptureISO' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CleanupCaptureISO' -State $State
|
||||||
@@ -303,7 +392,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 +452,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
|
||||||
# Update Optional Features checkboxes based on the loaded text
|
$loadedFeaturesString = $ConfigContent.OptionalFeatures
|
||||||
$loadedFeaturesString = $State.Controls.txtOptionalFeatures.Text
|
|
||||||
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 ', ')"
|
||||||
@@ -405,6 +493,7 @@ function Update-UIFromConfig {
|
|||||||
Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'PEDriversFolder' -State $State
|
Set-UIValue -ControlName 'txtPEDriversFolder' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'PEDriversFolder' -State $State
|
||||||
Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DriversJsonPath' -State $State
|
Set-UIValue -ControlName 'txtDriversJsonPath' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'DriversJsonPath' -State $State
|
||||||
Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPEDrivers' -State $State
|
Set-UIValue -ControlName 'chkCopyPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CopyPEDrivers' -State $State
|
||||||
|
Set-UIValue -ControlName 'chkUseDriversAsPEDrivers' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'UseDriversAsPEDrivers' -State $State
|
||||||
Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompressDownloadedDriversToWim' -State $State
|
Set-UIValue -ControlName 'chkCompressDriversToWIM' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'CompressDownloadedDriversToWim' -State $State
|
||||||
|
|
||||||
# Updates tab
|
# Updates tab
|
||||||
@@ -577,8 +666,46 @@ function Update-UIFromConfig {
|
|||||||
else {
|
else {
|
||||||
WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met."
|
WriteLog "LoadConfig: Condition to auto-check 'Select Specific USB Drives' was NOT met."
|
||||||
}
|
}
|
||||||
WriteLog "LoadConfig: Configuration loading process finished."
|
# Populate additional FFU list and apply selections
|
||||||
}
|
try {
|
||||||
|
if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) {
|
||||||
|
$State.Controls.additionalFFUPanel.Visibility = 'Visible'
|
||||||
|
if ($State.Controls.btnRefreshAdditionalFFUs) {
|
||||||
|
$State.Controls.btnRefreshAdditionalFFUs.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent))
|
||||||
|
}
|
||||||
|
$selectedFiles = @()
|
||||||
|
$addFFUKeyExists = $false
|
||||||
|
if ($ConfigContent -is [System.Management.Automation.PSCustomObject] -and $null -ne $ConfigContent.PSObject.Properties) {
|
||||||
|
if (($ConfigContent.PSObject.Properties.Match('AdditionalFFUFiles')).Count -gt 0) {
|
||||||
|
$addFFUKeyExists = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($addFFUKeyExists -and $null -ne $ConfigContent.AdditionalFFUFiles) {
|
||||||
|
$selectedFiles = @($ConfigContent.AdditionalFFUFiles)
|
||||||
|
}
|
||||||
|
if ($selectedFiles.Count -gt 0) {
|
||||||
|
foreach ($item in $State.Controls.lstAdditionalFFUs.Items) {
|
||||||
|
if ($selectedFiles -contains $item.FullName) {
|
||||||
|
$item.IsSelected = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$State.Controls.lstAdditionalFFUs.Items.Refresh()
|
||||||
|
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$State.Controls.additionalFFUPanel.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "LoadConfig: Error applying Additional FFU selections: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "LoadConfig: Configuration loading process finished."
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-SaveConfiguration {
|
function Invoke-SaveConfiguration {
|
||||||
param(
|
param(
|
||||||
@@ -600,7 +727,10 @@ function Invoke-SaveConfiguration {
|
|||||||
-DefaultExt ".json"
|
-DefaultExt ".json"
|
||||||
|
|
||||||
if ($savePath) {
|
if ($savePath) {
|
||||||
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $savePath -Encoding UTF8
|
# Sort top-level keys alphabetically for consistent output
|
||||||
|
$sortedConfig = [ordered]@{}
|
||||||
|
foreach ($k in ($config.Keys | Sort-Object)) { $sortedConfig[$k] = $config[$k] }
|
||||||
|
$sortedConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $savePath -Encoding UTF8
|
||||||
[System.Windows.MessageBox]::Show("Configuration file saved to:`n$savePath", "Success", "OK", "Information")
|
[System.Windows.MessageBox]::Show("Configuration file saved to:`n$savePath", "Success", "OK", "Information")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -609,4 +739,420 @@ function Invoke-SaveConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Invoke-RestoreDefaults {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
$rootPath = $State.FFUDevelopmentPath
|
||||||
|
|
||||||
|
# Normalize potential array values to single strings
|
||||||
|
function Normalize-PathScalar {
|
||||||
|
param([object]$value)
|
||||||
|
if ($null -eq $value) { return $null }
|
||||||
|
if ($value -is [System.Array]) {
|
||||||
|
foreach ($v in $value) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$v)) {
|
||||||
|
return [string]$v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return [string]$value
|
||||||
|
}
|
||||||
|
|
||||||
|
$appsPath = Join-Path $rootPath 'Apps'
|
||||||
|
$driversRaw = Normalize-PathScalar -value $State.Controls.txtDriversFolder.Text
|
||||||
|
if ([string]::IsNullOrWhiteSpace($driversRaw)) {
|
||||||
|
$driversPath = Join-Path $rootPath 'Drivers'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$driversPath = $driversRaw
|
||||||
|
}
|
||||||
|
$ffuCaptureRaw = Normalize-PathScalar -value $State.Controls.txtFFUCaptureLocation.Text
|
||||||
|
$ffuCapturePath = if ([string]::IsNullOrWhiteSpace($ffuCaptureRaw)) { Join-Path $rootPath 'FFU' } else { $ffuCaptureRaw }
|
||||||
|
|
||||||
|
$captureISOPath = Join-Path $rootPath 'WinPECaptureFFUFiles\WinPE-Capture.iso'
|
||||||
|
$deployISOPath = Join-Path $rootPath 'WinPEDeployFFUFiles\WinPE-Deploy.iso'
|
||||||
|
$appsISOPath = Join-Path $rootPath 'Apps.iso'
|
||||||
|
|
||||||
|
$msg = "Restore Defaults will:`n`n- Delete generated config and app/driver list JSON files`n- Remove ISO files (Capture, Deploy, Apps) if present`n- Remove Apps/Update/downloaded artifacts`n- Remove driver folder contents (not the folder)`n- Remove FFU files in the capture folder`n`nSample/template files and VM/VHDX cache are NOT removed.`n`nProceed?"
|
||||||
|
$result = [System.Windows.MessageBox]::Show($msg, "Confirm Restore Defaults", "YesNo", "Warning")
|
||||||
|
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
|
||||||
|
WriteLog "RestoreDefaults: User cancelled."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog "RestoreDefaults: Starting environment reset."
|
||||||
|
WriteLog "RestoreDefaults: Paths -> Apps=$appsPath Drivers=$driversPath FFUCapture=$ffuCapturePath"
|
||||||
|
|
||||||
|
# Remove JSON artifact files if present
|
||||||
|
$artifactFiles = @(
|
||||||
|
(Join-Path $rootPath 'config\FFUConfig.json'),
|
||||||
|
(Join-Path $appsPath 'AppList.json'),
|
||||||
|
(Join-Path $driversPath 'Drivers.json'),
|
||||||
|
(Join-Path $appsPath 'UserAppList.json')
|
||||||
|
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||||
|
|
||||||
|
foreach ($file in $artifactFiles) {
|
||||||
|
if ((-not [string]::IsNullOrWhiteSpace($file)) -and (Test-Path -LiteralPath $file)) {
|
||||||
|
try {
|
||||||
|
WriteLog "RestoreDefaults: Removing $file"
|
||||||
|
Remove-Item -LiteralPath $file -Force -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "RestoreDefaults: Failed removing $file : $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Force all cleanup flags true
|
||||||
|
Invoke-FFUPostBuildCleanup `
|
||||||
|
-RootPath $rootPath `
|
||||||
|
-AppsPath $appsPath `
|
||||||
|
-DriversPath $driversPath `
|
||||||
|
-FFUCapturePath $ffuCapturePath `
|
||||||
|
-CaptureISOPath $captureISOPath `
|
||||||
|
-DeployISOPath $deployISOPath `
|
||||||
|
-AppsISOPath $appsISOPath `
|
||||||
|
-RemoveCaptureISO:$true `
|
||||||
|
-RemoveDeployISO:$true `
|
||||||
|
-RemoveAppsISO:$true `
|
||||||
|
-RemoveDrivers:$true `
|
||||||
|
-RemoveFFU:$true `
|
||||||
|
-RemoveApps:$true `
|
||||||
|
-RemoveUpdates:$true
|
||||||
|
|
||||||
|
# Clear UI lists / state
|
||||||
|
if ($null -ne $State.Data.allDriverModels) { $State.Data.allDriverModels.Clear() }
|
||||||
|
if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Items.Refresh() }
|
||||||
|
if ($null -ne $State.Controls.lstApplications) {
|
||||||
|
try {
|
||||||
|
if ($State.Controls.lstApplications.ItemsSource) { $State.Controls.lstApplications.ItemsSource = $null }
|
||||||
|
$State.Controls.lstApplications.Items.Clear()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if ($null -ne $State.Controls.lstWingetResults) {
|
||||||
|
try {
|
||||||
|
if ($State.Controls.lstWingetResults.ItemsSource) { $State.Controls.lstWingetResults.ItemsSource = $null }
|
||||||
|
$State.Controls.lstWingetResults.Items.Clear()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if ($null -ne $State.Controls.lstAppsScriptVariables) {
|
||||||
|
try {
|
||||||
|
if ($State.Controls.lstAppsScriptVariables.ItemsSource) { $State.Controls.lstAppsScriptVariables.ItemsSource = $null }
|
||||||
|
$State.Controls.lstAppsScriptVariables.Items.Clear()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$State.Data.lastConfigFilePath = $null
|
||||||
|
|
||||||
|
Initialize-UIDefaults -State $State
|
||||||
|
|
||||||
|
WriteLog "RestoreDefaults: Completed."
|
||||||
|
[System.Windows.MessageBox]::Show("Environment restored to defaults.", "Restore Defaults", "OK", "Information")
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "RestoreDefaults: Failed with $($_.Exception.Message)"
|
||||||
|
[System.Windows.MessageBox]::Show("Restore Defaults failed:`n$($_.Exception.Message)", "Error", "OK", "Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AutoLoadPreviousEnvironment {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$State
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
$ffuDevRoot = $State.FFUDevelopmentPath
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ffuDevRoot)) {
|
||||||
|
WriteLog "AutoLoad: FFUDevelopmentPath not set; skipping."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$configPath = Join-Path $ffuDevRoot "config\FFUConfig.json"
|
||||||
|
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||||
|
WriteLog "AutoLoad: No existing FFUConfig.json found at $configPath."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteLog "AutoLoad: Found config file at $configPath. Parsing..."
|
||||||
|
$raw = Get-Content -Path $configPath -Raw -ErrorAction SilentlyContinue
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
WriteLog "AutoLoad: Config file empty; aborting."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$configContent = $null
|
||||||
|
try {
|
||||||
|
$configContent = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "AutoLoad: JSON parse failed: $($_.Exception.Message)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($null -eq $configContent) {
|
||||||
|
WriteLog "AutoLoad: Parsed object null; aborting."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteLog "AutoLoad: Applying core configuration."
|
||||||
|
Update-UIFromConfig -ConfigContent $configContent -State $State
|
||||||
|
$State.Data.lastConfigFilePath = $configPath
|
||||||
|
Import-ConfigSupplementalAssets -ConfigContent $configContent -State $State -ShowWarnings:$false
|
||||||
|
WriteLog "AutoLoad: Completed supplemental import with warnings disabled."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "AutoLoad: Unexpected failure: $($_.Exception.ToString())"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-ConfigSupplementalAssets {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$ConfigContent,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$State,
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$ShowWarnings = $false
|
||||||
|
)
|
||||||
|
WriteLog "SupplementalImport: Starting import of helper assets."
|
||||||
|
$loadedWinget = $false
|
||||||
|
$loadedBYO = $false
|
||||||
|
$loadedDrivers = $false
|
||||||
|
$missing = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
# Winget AppList
|
||||||
|
$appListPath = $null
|
||||||
|
if ($ConfigContent.PSObject.Properties.Match('AppListPath').Count -gt 0) {
|
||||||
|
$appListPath = $ConfigContent.AppListPath
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($appListPath)) {
|
||||||
|
if (Test-Path -LiteralPath $appListPath) {
|
||||||
|
WriteLog "SupplementalImport: Loading Winget AppList from $appListPath"
|
||||||
|
try {
|
||||||
|
$importedAppsData = Get-Content -Path $appListPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
if ($null -ne $importedAppsData -and $null -ne $importedAppsData.apps) {
|
||||||
|
$defaultArch = $State.Controls.cmbWindowsArch.SelectedItem
|
||||||
|
$appsBuffer = [System.Collections.Generic.List[object]]::new()
|
||||||
|
foreach ($appInfo in $importedAppsData.apps) {
|
||||||
|
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else {
|
||||||
|
if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch }
|
||||||
|
}
|
||||||
|
$appsBuffer.Add([PSCustomObject]@{
|
||||||
|
IsSelected = $true
|
||||||
|
Name = $appInfo.name
|
||||||
|
Id = $appInfo.id
|
||||||
|
Version = ""
|
||||||
|
Source = $appInfo.source
|
||||||
|
Architecture = $arch
|
||||||
|
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
|
||||||
|
DownloadStatus = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$State.Controls.lstWingetResults.ItemsSource = $appsBuffer.ToArray()
|
||||||
|
$loadedWinget = $true
|
||||||
|
if ($null -ne $State.Controls.wingetSearchPanel) {
|
||||||
|
$State.Controls.wingetSearchPanel.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
if ($null -ne $State.Controls.chkSelectAllWingetResults -and (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue)) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstWingetResults -HeaderCheckBox $State.Controls.chkSelectAllWingetResults
|
||||||
|
}
|
||||||
|
WriteLog "SupplementalImport: Winget list loaded with $($appsBuffer.Count) entries."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: Winget AppList missing 'apps' array."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "SupplementalImport: Failed loading Winget AppList ($appListPath): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: Winget AppList file missing: $appListPath"
|
||||||
|
$missing.Add("Winget AppList (AppListPath): $appListPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: AppListPath not defined in config."
|
||||||
|
}
|
||||||
|
|
||||||
|
# UserAppList (BYO)
|
||||||
|
$userAppListPath = $null
|
||||||
|
if ($ConfigContent.PSObject.Properties.Match('UserAppListPath').Count -gt 0) {
|
||||||
|
$userAppListPath = $ConfigContent.UserAppListPath
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($userAppListPath)) {
|
||||||
|
if (Test-Path -LiteralPath $userAppListPath) {
|
||||||
|
WriteLog "SupplementalImport: Loading UserAppList from $userAppListPath"
|
||||||
|
try {
|
||||||
|
$applications = Get-Content -Path $userAppListPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
if ($applications) {
|
||||||
|
$listView = $State.Controls.lstApplications
|
||||||
|
$listView.Items.Clear()
|
||||||
|
$sortedApps = $applications | Sort-Object Priority
|
||||||
|
foreach ($app in $sortedApps) {
|
||||||
|
$ignoreNonZero = if ($app.PSObject.Properties['IgnoreNonZeroExitCodes']) { $app.IgnoreNonZeroExitCodes } else { $false }
|
||||||
|
$listView.Items.Add([PSCustomObject]@{
|
||||||
|
IsSelected = $false
|
||||||
|
Priority = $app.Priority
|
||||||
|
Name = $app.Name
|
||||||
|
CommandLine = $app.CommandLine
|
||||||
|
Arguments = if ($app.PSObject.Properties['Arguments']) { $app.Arguments } else { "" }
|
||||||
|
Source = $app.Source
|
||||||
|
AdditionalExitCodes = if ($app.PSObject.Properties['AdditionalExitCodes']) { $app.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = $ignoreNonZero
|
||||||
|
IgnoreExitCodes = if ($ignoreNonZero) { "Yes" } else { "No" }
|
||||||
|
CopyStatus = ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-ListViewPriorities -ErrorAction SilentlyContinue) {
|
||||||
|
Update-ListViewPriorities -ListView $listView
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) {
|
||||||
|
Update-CopyButtonState -State $State
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-BYOAppsActionButtonsState -ErrorAction SilentlyContinue) {
|
||||||
|
Update-BYOAppsActionButtonsState -State $State
|
||||||
|
}
|
||||||
|
$loadedBYO = $true
|
||||||
|
WriteLog "SupplementalImport: UserAppList loaded with $($listView.Items.Count) entries."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: UserAppList JSON empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "SupplementalImport: Failed loading UserAppList ($userAppListPath): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: UserAppList file missing: $userAppListPath"
|
||||||
|
$missing.Add("UserAppList (UserAppListPath): $userAppListPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: UserAppListPath not defined in config."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drivers JSON
|
||||||
|
$driversJsonPath = $null
|
||||||
|
if ($ConfigContent.PSObject.Properties.Match('DriversJsonPath').Count -gt 0) {
|
||||||
|
$driversJsonPath = $ConfigContent.DriversJsonPath
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($driversJsonPath)) {
|
||||||
|
if (Test-Path -LiteralPath $driversJsonPath) {
|
||||||
|
WriteLog "SupplementalImport: Loading Drivers JSON from $driversJsonPath"
|
||||||
|
try {
|
||||||
|
$rawDrivers = Get-Content -Path $driversJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
if ($rawDrivers -and $rawDrivers.PSObject.Properties.Count -gt 0) {
|
||||||
|
$State.Data.allDriverModels.Clear()
|
||||||
|
foreach ($makeProp in $rawDrivers.PSObject.Properties) {
|
||||||
|
$makeName = $makeProp.Name
|
||||||
|
$makeObject = $makeProp.Value
|
||||||
|
if ($null -eq $makeObject -or -not ($makeObject.PSObject.Properties['Models'])) { continue }
|
||||||
|
$models = $makeObject.Models
|
||||||
|
if ($models -and ($models -is [System.Collections.IEnumerable])) {
|
||||||
|
foreach ($modelEntry in $models) {
|
||||||
|
if ($null -eq $modelEntry -or -not ($modelEntry.PSObject.Properties['Name'])) { continue }
|
||||||
|
$modelName = $modelEntry.Name
|
||||||
|
if ([string]::IsNullOrWhiteSpace($modelName)) { continue }
|
||||||
|
$driverObj = [PSCustomObject]@{
|
||||||
|
IsSelected = $true
|
||||||
|
Make = $makeName
|
||||||
|
Model = $modelName
|
||||||
|
DownloadStatus = if ($modelEntry.PSObject.Properties['DownloadStatus']) { $modelEntry.DownloadStatus } else { "" }
|
||||||
|
Link = if ($modelEntry.PSObject.Properties['Link']) { $modelEntry.Link } else { $null }
|
||||||
|
ProductName = if ($modelEntry.PSObject.Properties['ProductName']) { $modelEntry.ProductName } else { $null }
|
||||||
|
MachineType = if ($modelEntry.PSObject.Properties['MachineType']) { $modelEntry.MachineType } else { $null }
|
||||||
|
Id = if ($modelEntry.PSObject.Properties['Id']) { $modelEntry.Id } else { $null }
|
||||||
|
}
|
||||||
|
$State.Data.allDriverModels.Add($driverObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$State.Controls.lstDriverModels.ItemsSource = $State.Data.allDriverModels
|
||||||
|
if (Get-Command -Name Update-SelectAllHeaderCheckBoxState -ErrorAction SilentlyContinue) {
|
||||||
|
$headerChk = $State.Controls.chkSelectAllDriverModels
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstDriverModels -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($State.Data.allDriverModels.Count -gt 0) {
|
||||||
|
if ($null -ne $State.Controls.spModelFilterSection) { $State.Controls.spModelFilterSection.Visibility = 'Visible' }
|
||||||
|
if ($null -ne $State.Controls.lstDriverModels) { $State.Controls.lstDriverModels.Visibility = 'Visible' }
|
||||||
|
if ($null -ne $State.Controls.spDriverActionButtons) { $State.Controls.spDriverActionButtons.Visibility = 'Visible' }
|
||||||
|
try {
|
||||||
|
if ($State.Controls.cmbMake.SelectedIndex -lt 0 -and $State.Data.allDriverModels.Count -gt 0) {
|
||||||
|
$firstMake = ($State.Data.allDriverModels | Select-Object -First 1).Make
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($firstMake)) {
|
||||||
|
$makeItem = $State.Controls.cmbMake.Items | Where-Object { $_ -eq $firstMake } | Select-Object -First 1
|
||||||
|
if ($makeItem) { $State.Controls.cmbMake.SelectedItem = $makeItem }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "SupplementalImport: Non-fatal error selecting first Make: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$loadedDrivers = $true
|
||||||
|
WriteLog "SupplementalImport: Loaded $($State.Data.allDriverModels.Count) driver models."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: Drivers JSON empty or structure unexpected."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "SupplementalImport: Failed loading Drivers JSON ($driversJsonPath): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: Drivers JSON file missing: $driversJsonPath"
|
||||||
|
$missing.Add("Drivers (DriversJsonPath): $driversJsonPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "SupplementalImport: DriversJsonPath not defined in config."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($loadedWinget -or $loadedBYO) {
|
||||||
|
$State.Controls.chkInstallApps.IsChecked = $true
|
||||||
|
}
|
||||||
|
if ($loadedWinget) {
|
||||||
|
$State.Controls.chkInstallWingetApps.IsChecked = $true
|
||||||
|
}
|
||||||
|
if ($loadedBYO) {
|
||||||
|
$State.Controls.chkBringYourOwnApps.IsChecked = $true
|
||||||
|
}
|
||||||
|
if ($loadedDrivers) {
|
||||||
|
$State.Controls.chkDownloadDrivers.IsChecked = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command -Name Update-ApplicationPanelVisibility -ErrorAction SilentlyContinue) {
|
||||||
|
Update-ApplicationPanelVisibility -State $State -TriggeringControlName 'SupplementalImport'
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-DriverDownloadPanelVisibility -ErrorAction SilentlyContinue) {
|
||||||
|
Update-DriverDownloadPanelVisibility -State $State
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-DriverCheckboxStates -ErrorAction SilentlyContinue) {
|
||||||
|
Update-DriverCheckboxStates -State $State
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-OfficePanelVisibility -ErrorAction SilentlyContinue) {
|
||||||
|
Update-OfficePanelVisibility -State $State
|
||||||
|
}
|
||||||
|
if (Get-Command -Name Update-CopyButtonState -ErrorAction SilentlyContinue) {
|
||||||
|
Update-CopyButtonState -State $State
|
||||||
|
}
|
||||||
|
|
||||||
|
# Updated message to clarify successful load and that missing helper files are optional if not yet created.
|
||||||
|
if ($ShowWarnings -and $missing.Count -gt 0) {
|
||||||
|
$msg = "Configuration file loaded successfully.`n`n" +
|
||||||
|
"Optional helper file(s) referenced in the configuration were not found:`n" +
|
||||||
|
($missing | ForEach-Object { "- $_" } | Out-String) +
|
||||||
|
"`nThese files are optional. They won't exist until you create Winget (AppList.json), User (UserAppList.json), or Driver (Drivers.json) manifests. You can create them later or ignore this message."
|
||||||
|
[System.Windows.MessageBox]::Show($msg.TrimEnd(), "Configuration Loaded - Optional Files Missing", "OK", "Information") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog ("SupplementalImport: Complete. Winget={0} BYO={1} Drivers={2} Missing={3}" -f $loadedWinget, $loadedBYO, $loadedDrivers, $missing.Count)
|
||||||
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function *
|
Export-ModuleMember -Function *
|
||||||
@@ -167,10 +167,12 @@ function Save-DellDriversTask {
|
|||||||
[string]$WindowsArch,
|
[string]$WindowsArch,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[int]$WindowsRelease,
|
[int]$WindowsRelease,
|
||||||
[Parameter()] # Made optional
|
[Parameter()]
|
||||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[bool]$CompressToWim = $false # New parameter for compression
|
[bool]$CompressToWim = $false, # New parameter for compression
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$PreserveSourceOnCompress = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$modelName = $DriverItemData.Model
|
$modelName = $DriverItemData.Model
|
||||||
@@ -181,13 +183,15 @@ function Save-DellDriversTask {
|
|||||||
# Initial status update
|
# Initial status update
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Checking..." }
|
||||||
|
|
||||||
|
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||||
|
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
||||||
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||||||
$driverRelativePath = Join-Path -Path $make -ChildPath $modelName # Relative path for the driver folder
|
$driverRelativePath = Join-Path -Path $make -ChildPath $sanitizedModelName # Relative path for the driver folder
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Check for existing drivers
|
# Check for existing drivers
|
||||||
$existingDriver = Test-ExistingDriver -Make $make -Model $modelName -DriversFolder $DriversFolder -Identifier $modelName -ProgressQueue $ProgressQueue
|
$existingDriver = Test-ExistingDriver -Make $make -Model $sanitizedModelName -DriversFolder $DriversFolder -Identifier $modelName -ProgressQueue $ProgressQueue
|
||||||
if ($null -ne $existingDriver) {
|
if ($null -ne $existingDriver) {
|
||||||
# Add the 'Model' property to the return object for consistency if it's not there
|
# Add the 'Model' property to the return object for consistency if it's not there
|
||||||
if (-not $existingDriver.PSObject.Properties['Model']) {
|
if (-not $existingDriver.PSObject.Properties['Model']) {
|
||||||
@@ -196,14 +200,14 @@ function Save-DellDriversTask {
|
|||||||
|
|
||||||
# Special handling for existing folders that need compression
|
# Special handling for existing folders that need compression
|
||||||
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
|
if ($CompressToWim -and $existingDriver.Status -eq 'Already downloaded') {
|
||||||
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($modelName).wim"
|
$wimFilePath = Join-Path -Path $makeDriversPath -ChildPath "$($sanitizedModelName).wim"
|
||||||
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
$sourceFolderPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
|
||||||
try {
|
try {
|
||||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -ErrorAction Stop
|
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
$existingDriver.Status = "Already downloaded & Compressed"
|
$existingDriver.Status = "Already downloaded & Compressed"
|
||||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($modelName).wim"
|
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
|
||||||
$existingDriver.Success = $true
|
$existingDriver.Success = $true
|
||||||
WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath."
|
WriteLog "Successfully compressed existing drivers for $modelName to $wimFilePath."
|
||||||
}
|
}
|
||||||
@@ -664,7 +668,7 @@ function Save-DellDriversTask {
|
|||||||
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
|
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
|
||||||
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
||||||
try {
|
try {
|
||||||
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
if ($compressResult) {
|
if ($compressResult) {
|
||||||
WriteLog "Compression successful for '$modelName'."
|
WriteLog "Compression successful for '$modelName'."
|
||||||
$status = "Completed & Compressed"
|
$status = "Completed & Compressed"
|
||||||
|
|||||||
@@ -118,13 +118,16 @@ function Save-HPDriversTask {
|
|||||||
[Parameter()] # Made optional
|
[Parameter()] # Made optional
|
||||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[bool]$CompressToWim = $false # New parameter for compression
|
[bool]$CompressToWim = $false, # New parameter for compression
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$PreserveSourceOnCompress = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
$modelName = $DriverItemData.Model
|
$modelName = $DriverItemData.Model
|
||||||
$make = $DriverItemData.Make # Should be 'HP'
|
$make = $DriverItemData.Make # Should be 'HP'
|
||||||
$identifier = $modelName # Unique identifier for progress updates
|
$identifier = $modelName # Unique identifier for progress updates
|
||||||
$sanitizedModelName = $modelName -replace '[\\/:"*?<>|]', '_'
|
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||||
|
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||||
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
|
$hpDriversBaseFolder = Join-Path -Path $DriversFolder -ChildPath $make # Changed variable name for clarity
|
||||||
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
|
$platformListXml = Join-Path -Path $hpDriversBaseFolder -ChildPath "PlatformList.xml"
|
||||||
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName # Sanitize model name for folder path
|
$modelSpecificFolder = Join-Path -Path $hpDriversBaseFolder -ChildPath $sanitizedModelName # Sanitize model name for folder path
|
||||||
@@ -150,7 +153,7 @@ function Save-HPDriversTask {
|
|||||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
||||||
try {
|
try {
|
||||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
|
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
$existingDriver.Status = "Already downloaded & Compressed"
|
$existingDriver.Status = "Already downloaded & Compressed"
|
||||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
|
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedModelName).wim"
|
||||||
$existingDriver.Success = $true
|
$existingDriver.Success = $true
|
||||||
@@ -362,7 +365,7 @@ function Save-HPDriversTask {
|
|||||||
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
|
$wimFilePath = Join-Path -Path $hpDriversBaseFolder -ChildPath "$($identifier).wim"
|
||||||
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
|
WriteLog "Compressing '$modelSpecificFolder' to '$wimFilePath'..."
|
||||||
try {
|
try {
|
||||||
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
|
Compress-DriverFolderToWim -SourceFolderPath $modelSpecificFolder -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
WriteLog "Compression successful for '$identifier'."
|
WriteLog "Compression successful for '$identifier'."
|
||||||
$finalStatus = "Completed & Compressed"
|
$finalStatus = "Completed & Compressed"
|
||||||
$driverRelativePath = Join-Path -Path $make -ChildPath "$($identifier).wim" # Update relative path to the WIM
|
$driverRelativePath = Join-Path -Path $make -ChildPath "$($identifier).wim" # Update relative path to the WIM
|
||||||
|
|||||||
@@ -94,17 +94,20 @@ function Save-LenovoDriversTask {
|
|||||||
[hashtable]$Headers,
|
[hashtable]$Headers,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$UserAgent,
|
[string]$UserAgent,
|
||||||
[Parameter()] # Made optional
|
[Parameter()]
|
||||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null,
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[bool]$CompressToWim = $false
|
[bool]$CompressToWim = $false,
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$PreserveSourceOnCompress = $false
|
||||||
)
|
)
|
||||||
|
|
||||||
# The Model property from the UI already contains the combined "ProductName (MachineType)" string
|
# The Model property from the UI already contains the combined "ProductName (MachineType)" string
|
||||||
$identifier = $DriverItemData.Model
|
$identifier = $DriverItemData.Model
|
||||||
$machineType = $DriverItemData.MachineType
|
$machineType = $DriverItemData.MachineType
|
||||||
$make = "Lenovo"
|
$make = "Lenovo"
|
||||||
$sanitizedIdentifier = $identifier -replace '[\\/:"*?<>|]', '_'
|
$sanitizedIdentifier = ConvertTo-SafeName -Name $identifier
|
||||||
|
if ($sanitizedIdentifier -ne $identifier) { WriteLog "Sanitized model identifier: '$identifier' -> '$sanitizedIdentifier'" }
|
||||||
$status = "Starting..."
|
$status = "Starting..."
|
||||||
$success = $false
|
$success = $false
|
||||||
|
|
||||||
@@ -133,7 +136,7 @@ function Save-LenovoDriversTask {
|
|||||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $identifier -Status "Compressing existing..." }
|
||||||
try {
|
try {
|
||||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -ErrorAction Stop
|
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $identifier -WimDescription "Drivers for $identifier" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
$existingDriver.Status = "Already downloaded & Compressed"
|
$existingDriver.Status = "Already downloaded & Compressed"
|
||||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
|
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($sanitizedIdentifier).wim"
|
||||||
$existingDriver.Success = $true
|
$existingDriver.Success = $true
|
||||||
@@ -424,7 +427,7 @@ function Save-LenovoDriversTask {
|
|||||||
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
|
$driverRelativePath = Join-Path -Path $make -ChildPath $wimFileName # Update relative path to the WIM file
|
||||||
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
||||||
try {
|
try {
|
||||||
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -ErrorAction Stop
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $identifier -WimDescription $identifier -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
if ($compressResult) {
|
if ($compressResult) {
|
||||||
WriteLog "Compression successful for '$identifier'."
|
WriteLog "Compression successful for '$identifier'."
|
||||||
$status = "Completed & Compressed"
|
$status = "Completed & Compressed"
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ function Save-MicrosoftDriversTask {
|
|||||||
[Parameter()] # Made optional
|
[Parameter()] # Made optional
|
||||||
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue = $null, # Default to null
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[bool]$CompressToWim = $false # New parameter for compression
|
[bool]$CompressToWim = $false, # New parameter for compression
|
||||||
|
[Parameter()]
|
||||||
|
[bool]$PreserveSourceOnCompress = $false
|
||||||
# REMOVED: UI-related parameters
|
# REMOVED: UI-related parameters
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ function Save-MicrosoftDriversTask {
|
|||||||
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
WriteLog "Attempting compression of existing folder '$sourceFolderPath' to '$wimFilePath'."
|
||||||
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
|
if ($null -ne $ProgressQueue) { Invoke-ProgressUpdate -ProgressQueue $ProgressQueue -Identifier $modelName -Status "Compressing existing..." }
|
||||||
try {
|
try {
|
||||||
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -ErrorAction Stop
|
Compress-DriverFolderToWim -SourceFolderPath $sourceFolderPath -DestinationWimPath $wimFilePath -WimName $modelName -WimDescription "Drivers for $modelName" -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
$existingDriver.Status = "Already downloaded & Compressed"
|
$existingDriver.Status = "Already downloaded & Compressed"
|
||||||
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($modelName).wim"
|
$existingDriver.DriverPath = Join-Path -Path $make -ChildPath "$($modelName).wim"
|
||||||
$existingDriver.Success = $true
|
$existingDriver.Success = $true
|
||||||
@@ -232,8 +234,10 @@ function Save-MicrosoftDriversTask {
|
|||||||
WriteLog "Creating Drivers folder: $DriversFolder"
|
WriteLog "Creating Drivers folder: $DriversFolder"
|
||||||
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
|
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
$sanitizedModelName = ConvertTo-SafeName -Name $modelName
|
||||||
|
if ($sanitizedModelName -ne $modelName) { WriteLog "Sanitized model name: '$modelName' -> '$sanitizedModelName'" }
|
||||||
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
$makeDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
|
||||||
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $modelName
|
$modelPath = Join-Path -Path $makeDriversPath -ChildPath $sanitizedModelName
|
||||||
if (-Not (Test-Path -Path $modelPath)) {
|
if (-Not (Test-Path -Path $modelPath)) {
|
||||||
WriteLog "Creating model folder: $modelPath"
|
WriteLog "Creating model folder: $modelPath"
|
||||||
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
|
New-Item -Path $modelPath -ItemType Directory -Force | Out-Null
|
||||||
@@ -377,7 +381,7 @@ function Save-MicrosoftDriversTask {
|
|||||||
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
WriteLog "Compressing '$modelPath' to '$destinationWimPath'..."
|
||||||
try {
|
try {
|
||||||
# Use the function from the imported common module
|
# Use the function from the imported common module
|
||||||
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -ErrorAction Stop
|
$compressResult = Compress-DriverFolderToWim -SourceFolderPath $modelPath -DestinationWimPath $destinationWimPath -WimName $modelName -WimDescription $modelName -PreserveSource:$PreserveSourceOnCompress -ErrorAction Stop
|
||||||
if ($compressResult) {
|
if ($compressResult) {
|
||||||
WriteLog "Compression successful for '$modelName'."
|
WriteLog "Compression successful for '$modelName'."
|
||||||
$status = "Completed & Compressed"
|
$status = "Completed & Compressed"
|
||||||
|
|||||||
@@ -571,6 +571,8 @@ function Invoke-DownloadSelectedDrivers {
|
|||||||
$localHeaders = $coreStaticVars.Headers
|
$localHeaders = $coreStaticVars.Headers
|
||||||
$localUserAgent = $coreStaticVars.UserAgent
|
$localUserAgent = $coreStaticVars.UserAgent
|
||||||
$compressDrivers = $State.Controls.chkCompressDriversToWIM.IsChecked
|
$compressDrivers = $State.Controls.chkCompressDriversToWIM.IsChecked
|
||||||
|
# Determine if we must preserve source folders (used later for PE driver harvesting)
|
||||||
|
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
|
||||||
|
|
||||||
$State.Controls.txtStatus.Text = "Processing all selected drivers..."
|
$State.Controls.txtStatus.Text = "Processing all selected drivers..."
|
||||||
WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')"
|
WriteLog "Processing all selected drivers: $($selectedDrivers.Model -join ', ')"
|
||||||
@@ -620,15 +622,16 @@ function Invoke-DownloadSelectedDrivers {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$preserveSource = ($State.Controls.chkUseDriversAsPEDrivers.IsChecked -and $State.Controls.chkCompressDriversToWIM.IsChecked)
|
||||||
$taskArguments = @{
|
$taskArguments = @{
|
||||||
DriversFolder = $localDriversFolder
|
DriversFolder = $localDriversFolder
|
||||||
WindowsRelease = $localWindowsRelease
|
WindowsRelease = $localWindowsRelease
|
||||||
WindowsArch = $localWindowsArch
|
WindowsArch = $localWindowsArch
|
||||||
WindowsVersion = $localWindowsVersion
|
WindowsVersion = $localWindowsVersion
|
||||||
Headers = $localHeaders
|
Headers = $localHeaders
|
||||||
UserAgent = $localUserAgent
|
UserAgent = $localUserAgent
|
||||||
CompressToWim = $compressDrivers
|
CompressToWim = $compressDrivers
|
||||||
|
PreserveSourceOnCompress = $preserveSource
|
||||||
}
|
}
|
||||||
|
|
||||||
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers `
|
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers `
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -137,6 +152,50 @@ function Register-EventHandlers {
|
|||||||
$localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false
|
$localState.Controls.chkPromptExternalHardDiskMedia.IsChecked = $false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Additional FFU Files events
|
||||||
|
$State.Controls.chkCopyAdditionalFFUFiles.Add_Checked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.additionalFFUPanel.Visibility = 'Visible'
|
||||||
|
Update-AdditionalFFUList -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.chkCopyAdditionalFFUFiles.Add_Unchecked({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$localState.Controls.additionalFFUPanel.Visibility = 'Collapsed'
|
||||||
|
$localState.Controls.lstAdditionalFFUs.Items.Clear()
|
||||||
|
$headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.btnRefreshAdditionalFFUs.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Update-AdditionalFFUList -State $localState
|
||||||
|
})
|
||||||
|
$State.Controls.lstAdditionalFFUs.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 'chkSelectAllAdditionalFFUs'
|
||||||
|
$keyEvent.Handled = $true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$State.Controls.lstAdditionalFFUs.Add_SelectionChanged({
|
||||||
|
param($eventSource, $selChangeEvent)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
$headerChk = $localState.Controls.chkSelectAllAdditionalFFUs
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $localState.Controls.lstAdditionalFFUs -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
$State.Controls.btnCheckUSBDrives.Add_Click({
|
$State.Controls.btnCheckUSBDrives.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
@@ -192,19 +251,42 @@ 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'
|
||||||
if ($localState.Data.vmSwitchMap.ContainsKey($selectedItem)) {
|
if ($null -ne $selectedItem -and $localState.Data.vmSwitchMap.ContainsKey($selectedItem)) {
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem]
|
$localState.Controls.txtVMHostIPAddress.Text = $localState.Data.vmSwitchMap[$selectedItem]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found in map
|
$localState.Controls.txtVMHostIPAddress.Text = '' # Clear IP if not found or key null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 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 +438,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 +487,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 +511,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 +546,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)
|
||||||
@@ -688,6 +852,10 @@ function Register-EventHandlers {
|
|||||||
$State.Controls.chkCopyDrivers.Add_Unchecked($driverCheckboxHandler)
|
$State.Controls.chkCopyDrivers.Add_Unchecked($driverCheckboxHandler)
|
||||||
$State.Controls.chkCompressDriversToWIM.Add_Checked($driverCheckboxHandler)
|
$State.Controls.chkCompressDriversToWIM.Add_Checked($driverCheckboxHandler)
|
||||||
$State.Controls.chkCompressDriversToWIM.Add_Unchecked($driverCheckboxHandler)
|
$State.Controls.chkCompressDriversToWIM.Add_Unchecked($driverCheckboxHandler)
|
||||||
|
$State.Controls.chkCopyPEDrivers.Add_Checked($driverCheckboxHandler)
|
||||||
|
$State.Controls.chkCopyPEDrivers.Add_Unchecked($driverCheckboxHandler)
|
||||||
|
$State.Controls.chkUseDriversAsPEDrivers.Add_Checked($driverCheckboxHandler)
|
||||||
|
$State.Controls.chkUseDriversAsPEDrivers.Add_Unchecked($driverCheckboxHandler)
|
||||||
|
|
||||||
$State.Controls.btnBrowseDriversFolder.Add_Click({
|
$State.Controls.btnBrowseDriversFolder.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
@@ -830,6 +998,12 @@ function Register-EventHandlers {
|
|||||||
$localState = $window.Tag
|
$localState = $window.Tag
|
||||||
Invoke-LoadConfiguration -State $localState
|
Invoke-LoadConfiguration -State $localState
|
||||||
})
|
})
|
||||||
|
$State.Controls.btnRestoreDefaults.Add_Click({
|
||||||
|
param($eventSource, $routedEventArgs)
|
||||||
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
$localState = $window.Tag
|
||||||
|
Invoke-RestoreDefaults -State $localState
|
||||||
|
})
|
||||||
$State.Controls.btnBuildConfig.Add_Click({
|
$State.Controls.btnBuildConfig.Add_Click({
|
||||||
param($eventSource, $routedEventArgs)
|
param($eventSource, $routedEventArgs)
|
||||||
$window = [System.Windows.Window]::GetWindow($eventSource)
|
$window = [System.Windows.Window]::GetWindow($eventSource)
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -60,6 +59,10 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel')
|
$State.Controls.usbSelectionPanel = $window.FindName('usbDriveSelectionPanel')
|
||||||
$State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia')
|
$State.Controls.chkAllowExternalHardDiskMedia = $window.FindName('chkAllowExternalHardDiskMedia')
|
||||||
$State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia')
|
$State.Controls.chkPromptExternalHardDiskMedia = $window.FindName('chkPromptExternalHardDiskMedia')
|
||||||
|
$State.Controls.chkCopyAdditionalFFUFiles = $window.FindName('chkCopyAdditionalFFUFiles')
|
||||||
|
$State.Controls.additionalFFUPanel = $window.FindName('additionalFFUPanel')
|
||||||
|
$State.Controls.lstAdditionalFFUs = $window.FindName('lstAdditionalFFUs')
|
||||||
|
$State.Controls.btnRefreshAdditionalFFUs = $window.FindName('btnRefreshAdditionalFFUs')
|
||||||
$State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps')
|
$State.Controls.chkInstallWingetApps = $window.FindName('chkInstallWingetApps')
|
||||||
$State.Controls.wingetPanel = $window.FindName('wingetPanel')
|
$State.Controls.wingetPanel = $window.FindName('wingetPanel')
|
||||||
$State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule')
|
$State.Controls.btnCheckWingetModule = $window.FindName('btnCheckWingetModule')
|
||||||
@@ -89,10 +92,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 +118,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')
|
||||||
@@ -138,6 +147,7 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder')
|
$State.Controls.txtDriversFolder = $window.FindName('txtDriversFolder')
|
||||||
$State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder')
|
$State.Controls.txtPEDriversFolder = $window.FindName('txtPEDriversFolder')
|
||||||
$State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers')
|
$State.Controls.chkCopyPEDrivers = $window.FindName('chkCopyPEDrivers')
|
||||||
|
$State.Controls.chkUseDriversAsPEDrivers = $window.FindName('chkUseDriversAsPEDrivers')
|
||||||
$State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU')
|
$State.Controls.chkUpdateLatestCU = $window.FindName('chkUpdateLatestCU')
|
||||||
$State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet')
|
$State.Controls.chkUpdateLatestNet = $window.FindName('chkUpdateLatestNet')
|
||||||
$State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender')
|
$State.Controls.chkUpdateLatestDefender = $window.FindName('chkUpdateLatestDefender')
|
||||||
@@ -165,6 +175,7 @@ function Initialize-UIControls {
|
|||||||
$State.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath')
|
$State.Controls.btnBrowseDriversJsonPath = $window.FindName('btnBrowseDriversJsonPath')
|
||||||
$State.Controls.chkUpdateADK = $window.FindName('chkUpdateADK')
|
$State.Controls.chkUpdateADK = $window.FindName('chkUpdateADK')
|
||||||
$State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig')
|
$State.Controls.btnLoadConfig = $window.FindName('btnLoadConfig')
|
||||||
|
$State.Controls.btnRestoreDefaults = $window.FindName('btnRestoreDefaults')
|
||||||
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
|
$State.Controls.btnBuildConfig = $window.FindName('btnBuildConfig')
|
||||||
|
|
||||||
# Monitor Tab
|
# Monitor Tab
|
||||||
@@ -193,11 +204,11 @@ function Initialize-VMSwitchData {
|
|||||||
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
|
if ($State.Controls.cmbVMSwitchName.Items.Count -gt 1) {
|
||||||
$State.Controls.cmbVMSwitchName.SelectedIndex = 0
|
$State.Controls.cmbVMSwitchName.SelectedIndex = 0
|
||||||
$firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem
|
$firstSwitch = $State.Controls.cmbVMSwitchName.SelectedItem
|
||||||
if ($State.Data.vmSwitchMap.ContainsKey($firstSwitch)) {
|
if ($null -ne $firstSwitch -and $State.Data.vmSwitchMap.ContainsKey($firstSwitch)) {
|
||||||
$State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch]
|
$State.Controls.txtVMHostIPAddress.Text = $State.Data.vmSwitchMap[$firstSwitch]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found
|
$State.Controls.txtVMHostIPAddress.Text = $State.Defaults.generalDefaults.VMHostIPAddress # Use default if IP not found or key null
|
||||||
}
|
}
|
||||||
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
$State.Controls.txtCustomVMSwitchName.Visibility = 'Collapsed'
|
||||||
}
|
}
|
||||||
@@ -223,11 +234,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
|
||||||
@@ -248,6 +261,8 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
|
$State.Controls.usbSelectionPanel.Visibility = if ($State.Controls.chkSelectSpecificUSBDrives.IsChecked) { 'Visible' } else { 'Collapsed' }
|
||||||
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
$State.Controls.chkSelectSpecificUSBDrives.IsEnabled = $State.Controls.chkBuildUSBDriveEnable.IsChecked
|
||||||
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
|
$State.Controls.chkPromptExternalHardDiskMedia.IsEnabled = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
|
||||||
|
$State.Controls.chkCopyAdditionalFFUFiles.IsChecked = $State.Defaults.generalDefaults.CopyAdditionalFFUFiles
|
||||||
|
$State.Controls.additionalFFUPanel.Visibility = if ($State.Controls.chkCopyAdditionalFFUFiles.IsChecked) { 'Visible' } else { 'Collapsed' }
|
||||||
|
|
||||||
# Hyper-V Settings defaults from General Defaults
|
# Hyper-V Settings defaults from General Defaults
|
||||||
Initialize-VMSwitchData -State $State
|
Initialize-VMSwitchData -State $State
|
||||||
@@ -266,7 +281,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
|
||||||
@@ -303,6 +317,7 @@ function Initialize-UIDefaults {
|
|||||||
$State.Controls.chkInstallDrivers.IsChecked = $State.Defaults.generalDefaults.InstallDrivers
|
$State.Controls.chkInstallDrivers.IsChecked = $State.Defaults.generalDefaults.InstallDrivers
|
||||||
$State.Controls.chkCopyDrivers.IsChecked = $State.Defaults.generalDefaults.CopyDrivers
|
$State.Controls.chkCopyDrivers.IsChecked = $State.Defaults.generalDefaults.CopyDrivers
|
||||||
$State.Controls.chkCopyPEDrivers.IsChecked = $State.Defaults.generalDefaults.CopyPEDrivers
|
$State.Controls.chkCopyPEDrivers.IsChecked = $State.Defaults.generalDefaults.CopyPEDrivers
|
||||||
|
$State.Controls.chkUseDriversAsPEDrivers.IsChecked = $State.Defaults.generalDefaults.UseDriversAsPEDrivers
|
||||||
$State.Controls.chkCompressDriversToWIM.IsChecked = $State.Defaults.generalDefaults.CompressDownloadedDriversToWim
|
$State.Controls.chkCompressDriversToWIM.IsChecked = $State.Defaults.generalDefaults.CompressDownloadedDriversToWim
|
||||||
|
|
||||||
# Drivers tab UI logic
|
# Drivers tab UI logic
|
||||||
@@ -437,6 +452,75 @@ function Initialize-DynamicUIElements {
|
|||||||
$wingetGridView.Columns.Add($archColumn)
|
$wingetGridView.Columns.Add($archColumn)
|
||||||
# --- END: Add Architecture Column ---
|
# --- END: Add Architecture Column ---
|
||||||
|
|
||||||
|
# --- START: Add Additional Exit Codes Column ---
|
||||||
|
$exitCodesColumn = New-Object System.Windows.Controls.GridViewColumn
|
||||||
|
$exitCodesHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||||
|
$exitCodesHeader.Tag = "AdditionalExitCodes"
|
||||||
|
$exitCodesHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||||
|
|
||||||
|
$exitHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
|
||||||
|
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Additional Exit Codes")
|
||||||
|
$exitHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
|
||||||
|
$exitHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
||||||
|
|
||||||
|
$exitHeaderTemplate = New-Object System.Windows.DataTemplate
|
||||||
|
$exitHeaderTemplate.VisualTree = $exitHeaderTextFactory
|
||||||
|
$exitCodesHeader.ContentTemplate = $exitHeaderTemplate
|
||||||
|
|
||||||
|
$exitCodesColumn.Header = $exitCodesHeader
|
||||||
|
$exitCodesColumn.Width = 140
|
||||||
|
|
||||||
|
$exitCodesCellTemplate = New-Object System.Windows.DataTemplate
|
||||||
|
$exitCodesTextBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBox])
|
||||||
|
$exitBinding = New-Object System.Windows.Data.Binding("AdditionalExitCodes")
|
||||||
|
$exitBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
|
||||||
|
$exitCodesTextBoxFactory.SetBinding([System.Windows.Controls.TextBox]::TextProperty, $exitBinding)
|
||||||
|
$exitCodesCellTemplate.VisualTree = $exitCodesTextBoxFactory
|
||||||
|
$exitCodesColumn.CellTemplate = $exitCodesCellTemplate
|
||||||
|
$wingetGridView.Columns.Add($exitCodesColumn)
|
||||||
|
# --- END: Add Additional Exit Codes Column ---
|
||||||
|
|
||||||
|
# --- START: Add Ignore Non-Zero Exit Codes Column ---
|
||||||
|
$ignoreColumn = New-Object System.Windows.Controls.GridViewColumn
|
||||||
|
$ignoreHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||||
|
$ignoreHeader.Tag = "IgnoreNonZeroExitCodes"
|
||||||
|
$ignoreHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||||
|
|
||||||
|
$ignoreHeaderTextFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
|
||||||
|
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, "Ignore Exit Codes")
|
||||||
|
$ignoreHeaderTextFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, (New-Object System.Windows.Thickness(5, 2, 5, 2)))
|
||||||
|
$ignoreHeaderTextFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
||||||
|
|
||||||
|
$ignoreHeaderTemplate = New-Object System.Windows.DataTemplate
|
||||||
|
$ignoreHeaderTemplate.VisualTree = $ignoreHeaderTextFactory
|
||||||
|
$ignoreHeader.ContentTemplate = $ignoreHeaderTemplate
|
||||||
|
|
||||||
|
$ignoreColumn.Header = $ignoreHeader
|
||||||
|
$ignoreColumn.Width = 140
|
||||||
|
|
||||||
|
$ignoreCellTemplate = New-Object System.Windows.DataTemplate
|
||||||
|
|
||||||
|
# Center the checkbox in the cell
|
||||||
|
$ignoreCellGridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
|
||||||
|
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
|
||||||
|
$ignoreCellGridFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch)
|
||||||
|
|
||||||
|
$ignoreCheckFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
|
||||||
|
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
|
||||||
|
$ignoreCheckFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
||||||
|
|
||||||
|
$ignoreBinding = New-Object System.Windows.Data.Binding("IgnoreNonZeroExitCodes")
|
||||||
|
$ignoreBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
|
||||||
|
$ignoreCheckFactory.SetBinding([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty, $ignoreBinding)
|
||||||
|
|
||||||
|
# Build the visual tree: Grid -> CheckBox
|
||||||
|
$ignoreCellGridFactory.AppendChild($ignoreCheckFactory)
|
||||||
|
$ignoreCellTemplate.VisualTree = $ignoreCellGridFactory
|
||||||
|
|
||||||
|
$ignoreColumn.CellTemplate = $ignoreCellTemplate
|
||||||
|
$wingetGridView.Columns.Add($ignoreColumn)
|
||||||
|
# --- END: Add Ignore Non-Zero Exit Codes Column ---
|
||||||
|
|
||||||
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
|
Add-SortableColumn -gridView $wingetGridView -header "Download Status" -binding "DownloadStatus" -width 150 -headerHorizontalAlignment Left
|
||||||
$State.Controls.lstWingetResults.AddHandler(
|
$State.Controls.lstWingetResults.AddHandler(
|
||||||
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
|
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
|
||||||
@@ -454,6 +538,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()
|
||||||
@@ -582,6 +688,51 @@ function Initialize-DynamicUIElements {
|
|||||||
else {
|
else {
|
||||||
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
WriteLog "Warning: lstUSBDrives.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Additional FFUs ListView setup
|
||||||
|
$itemStyleAdditionalFFUs = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
|
||||||
|
$itemStyleAdditionalFFUs.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
|
||||||
|
$State.Controls.lstAdditionalFFUs.ItemContainerStyle = $itemStyleAdditionalFFUs
|
||||||
|
|
||||||
|
if ($State.Controls.lstAdditionalFFUs.View -is [System.Windows.Controls.GridView]) {
|
||||||
|
Add-SelectableGridViewColumn -ListView $State.Controls.lstAdditionalFFUs -State $State -HeaderCheckBoxKeyName "chkSelectAllAdditionalFFUs" -ColumnWidth 70
|
||||||
|
|
||||||
|
$additionalFFUsGridView = $State.Controls.lstAdditionalFFUs.View
|
||||||
|
|
||||||
|
if ($additionalFFUsGridView.Columns.Count -gt 1) {
|
||||||
|
$nameColumn = $additionalFFUsGridView.Columns[1]
|
||||||
|
$nameHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||||
|
$nameHeader.Content = "FFU Name"
|
||||||
|
$nameHeader.Tag = "Name"
|
||||||
|
$nameHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||||
|
$nameColumn.Header = $nameHeader
|
||||||
|
}
|
||||||
|
if ($additionalFFUsGridView.Columns.Count -gt 2) {
|
||||||
|
$lastModColumn = $additionalFFUsGridView.Columns[2]
|
||||||
|
$lastModHeader = New-Object System.Windows.Controls.GridViewColumnHeader
|
||||||
|
$lastModHeader.Content = "Last Modified"
|
||||||
|
$lastModHeader.Tag = "LastModified"
|
||||||
|
$lastModHeader.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Left
|
||||||
|
$lastModColumn.Header = $lastModHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
$State.Controls.lstAdditionalFFUs.AddHandler(
|
||||||
|
[System.Windows.Controls.GridViewColumnHeader]::ClickEvent,
|
||||||
|
[System.Windows.RoutedEventHandler] {
|
||||||
|
param($eventSource, $e)
|
||||||
|
$header = $e.OriginalSource
|
||||||
|
if ($header -is [System.Windows.Controls.GridViewColumnHeader] -and $header.Tag) {
|
||||||
|
$listViewControl = $eventSource
|
||||||
|
$window = [System.Windows.Window]::GetWindow($listViewControl)
|
||||||
|
$uiStateFromWindowTag = $window.Tag
|
||||||
|
Invoke-ListViewSort -listView $eventSource -property $header.Tag -State $uiStateFromWindowTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
WriteLog "Warning: lstAdditionalFFUs.View is not a GridView. Selectable column not added, and sorting cannot be enabled."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ $script:mctWindowsReleases = @(
|
|||||||
|
|
||||||
$script:windowsVersionMap = @{
|
$script:windowsVersionMap = @{
|
||||||
10 = @("22H2")
|
10 = @("22H2")
|
||||||
11 = @("22H2", "23H2", "24H2")
|
11 = @("22H2", "23H2", "24H2", "25H2")
|
||||||
2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016
|
2016 = @("1607") # Windows 10 LTSB 2016 & Server 2016
|
||||||
2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019
|
2019 = @("1809") # Windows 10 LTSC 2019 & Server 2019
|
||||||
# Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607"
|
# Note: Server 2016 and LTSB 2016 now share the key 2016, mapping to version "1607"
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -98,10 +98,12 @@ function Save-WingetList {
|
|||||||
$appList = @{
|
$appList = @{
|
||||||
apps = @($selectedApps | ForEach-Object {
|
apps = @($selectedApps | ForEach-Object {
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
name = $_.Name
|
name = (ConvertTo-SafeName -Name $_.Name)
|
||||||
id = $_.Id
|
id = $_.Id
|
||||||
source = $_.Source.ToLower()
|
source = $_.Source.ToLower()
|
||||||
architecture = $_.Architecture
|
architecture = $_.Architecture
|
||||||
|
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -148,13 +150,15 @@ function Import-WingetList {
|
|||||||
foreach ($appInfo in $importedAppsData.apps) {
|
foreach ($appInfo in $importedAppsData.apps) {
|
||||||
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
|
$arch = if ($appInfo.source -eq 'msstore') { 'NA' } else { if ($appInfo.PSObject.Properties['architecture']) { $appInfo.architecture } else { $defaultArch } }
|
||||||
$newAppListForItemsSource.Add([PSCustomObject]@{
|
$newAppListForItemsSource.Add([PSCustomObject]@{
|
||||||
IsSelected = $true # Imported apps are marked as selected
|
IsSelected = $true # Imported apps are marked as selected
|
||||||
Name = $appInfo.name
|
Name = $appInfo.name
|
||||||
Id = $appInfo.id
|
Id = $appInfo.id
|
||||||
Version = "" # Will be populated when searching or if data exists
|
Version = "" # Will be populated when searching or if data exists
|
||||||
Source = $appInfo.source
|
Source = $appInfo.source
|
||||||
Architecture = $arch
|
Architecture = $arch
|
||||||
DownloadStatus = ""
|
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
|
||||||
|
DownloadStatus = ""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,13 +195,15 @@ function Search-WingetPackagesPublic {
|
|||||||
$output = $results | ForEach-Object -Parallel {
|
$output = $results | ForEach-Object -Parallel {
|
||||||
$arch = if ($_.Source -eq 'msstore') { 'NA' } else { $using:DefaultArchitecture }
|
$arch = if ($_.Source -eq 'msstore') { 'NA' } else { $using:DefaultArchitecture }
|
||||||
[PSCustomObject]@{
|
[PSCustomObject]@{
|
||||||
IsSelected = [bool]$false
|
IsSelected = [bool]$false
|
||||||
Name = [string]$_.Name
|
Name = [string]$_.Name
|
||||||
Id = [string]$_.Id
|
Id = [string]$_.Id
|
||||||
Version = [string]$_.Version
|
Version = [string]$_.Version
|
||||||
Source = [string]$_.Source
|
Source = [string]$_.Source
|
||||||
Architecture = [string]$arch
|
Architecture = [string]$arch
|
||||||
DownloadStatus = [string]::Empty
|
AdditionalExitCodes = [string]::Empty
|
||||||
|
IgnoreNonZeroExitCodes = [bool]$false
|
||||||
|
DownloadStatus = [string]::Empty
|
||||||
}
|
}
|
||||||
} -ThrottleLimit 20
|
} -ThrottleLimit 20
|
||||||
WriteLog "Winget search completed. Created $($output.Count) output objects."
|
WriteLog "Winget search completed. Created $($output.Count) output objects."
|
||||||
@@ -385,7 +391,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
|
||||||
@@ -393,15 +400,12 @@ function Start-WingetAppDownloadTask {
|
|||||||
$source = $ApplicationItemData.Source
|
$source = $ApplicationItemData.Source
|
||||||
$status = "Checking..." # Initial local status
|
$status = "Checking..." # Initial local status
|
||||||
$resultCode = -1 # Default to error/unknown
|
$resultCode = -1 # Default to error/unknown
|
||||||
|
$sanitizedAppName = ConvertTo-SafeName -Name $appName
|
||||||
|
|
||||||
# Initial status update
|
# Initial status update
|
||||||
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
|
||||||
@@ -418,7 +422,7 @@ function Start-WingetAppDownloadTask {
|
|||||||
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
|
$userAppEntry = $userAppListContent | Where-Object { $_.Name -eq $appName }
|
||||||
|
|
||||||
if ($userAppEntry) {
|
if ($userAppEntry) {
|
||||||
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $appName
|
$appFolder = Join-Path -Path "$AppsPath\Win32" -ChildPath $sanitizedAppName
|
||||||
if (Test-Path -Path $appFolder -PathType Container) {
|
if (Test-Path -Path $appFolder -PathType Container) {
|
||||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||||
if ($folderSize -gt 1MB) {
|
if ($folderSize -gt 1MB) {
|
||||||
@@ -450,76 +454,41 @@ 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 $sanitizedAppName
|
||||||
$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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check MSStore folder
|
# Check MSStore folder
|
||||||
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
|
if (-not $appFound -and (Test-Path -Path "$AppsPath\MSStore" -PathType Container)) {
|
||||||
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $appName
|
$appFolder = Join-Path -Path "$AppsPath\MSStore" -ChildPath $sanitizedAppName
|
||||||
if (Test-Path -Path $appFolder -PathType Container) {
|
if (Test-Path -Path $appFolder -PathType Container) {
|
||||||
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
$folderSize = (Get-ChildItem -Path $appFolder -Recurse | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
|
||||||
if ($folderSize -gt 1MB) {
|
if ($folderSize -gt 1MB) {
|
||||||
@@ -567,7 +536,7 @@ function Start-WingetAppDownloadTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $appExistsInAppList) {
|
if (-not $appExistsInAppList) {
|
||||||
$newApp = @{ name = $appName; id = $appId; source = $source }
|
$newApp = @{ name = $sanitizedAppName; id = $appId; source = $source }
|
||||||
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
|
if (-not ($appListContent.apps -is [array])) { $appListContent.apps = @() }
|
||||||
$appListContent.apps += $newApp
|
$appListContent.apps += $newApp
|
||||||
try {
|
try {
|
||||||
@@ -634,7 +603,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,13 +722,50 @@ 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
|
||||||
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed
|
$itemsToProcess = $selectedApps | Select-Object Name, Id, Source, Version, Architecture # Include Version and Architecture if needed
|
||||||
|
|
||||||
|
# Before downloading, persist the selected apps to AppList.json including exit-code fields (parity with Save-WingetList)
|
||||||
|
try {
|
||||||
|
# Determine AppList.json path; default if empty
|
||||||
|
if ([string]::IsNullOrWhiteSpace($localAppListJsonPath)) {
|
||||||
|
$localAppListJsonPath = Join-Path -Path $localAppsPath -ChildPath "AppList.json"
|
||||||
|
$taskArguments.AppListJsonPath = $localAppListJsonPath
|
||||||
|
WriteLog "AppListJsonPath was empty. Defaulting to: $localAppListJsonPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build apps payload from current selection, preserving AdditionalExitCodes/IgnoreNonZeroExitCodes
|
||||||
|
$appListToSave = @{
|
||||||
|
apps = @($selectedApps | ForEach-Object {
|
||||||
|
[ordered]@{
|
||||||
|
name = (ConvertTo-SafeName -Name $_.Name)
|
||||||
|
id = $_.Id
|
||||||
|
source = $_.Source.ToLower()
|
||||||
|
architecture = $_.Architecture
|
||||||
|
AdditionalExitCodes = if ($_.PSObject.Properties['AdditionalExitCodes']) { $_.AdditionalExitCodes } else { "" }
|
||||||
|
IgnoreNonZeroExitCodes = if ($_.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$_.IgnoreNonZeroExitCodes } else { $false }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure destination directory exists and write AppList.json
|
||||||
|
$destDir = Split-Path -Parent $localAppListJsonPath
|
||||||
|
if (-not (Test-Path -LiteralPath $destDir)) {
|
||||||
|
[void][System.IO.Directory]::CreateDirectory($destDir)
|
||||||
|
}
|
||||||
|
$appListToSave | ConvertTo-Json -Depth 10 | Set-Content -Path $localAppListJsonPath -Encoding UTF8
|
||||||
|
WriteLog "Persisted AppList.json with selected apps and exit-code fields to: $localAppListJsonPath"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Warning: Failed to persist AppList.json prior to download. Error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
# Invoke the centralized parallel processing function
|
# Invoke the centralized parallel processing function
|
||||||
# Pass task type and task-specific arguments
|
# Pass task type and task-specific arguments
|
||||||
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
|
Invoke-ParallelProcessing -ItemsToProcess $itemsToProcess `
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -127,9 +128,11 @@ function Get-GeneralDefaults {
|
|||||||
AllowExternalHardDiskMedia = $false
|
AllowExternalHardDiskMedia = $false
|
||||||
PromptExternalHardDiskMedia = $true
|
PromptExternalHardDiskMedia = $true
|
||||||
SelectSpecificUSBDrives = $false
|
SelectSpecificUSBDrives = $false
|
||||||
|
CopyAdditionalFFUFiles = $false
|
||||||
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
|
||||||
@@ -139,7 +142,7 @@ function Get-GeneralDefaults {
|
|||||||
RemoveUpdates = $false
|
RemoveUpdates = $false
|
||||||
# Hyper-V Settings Defaults
|
# Hyper-V Settings Defaults
|
||||||
VMHostIPAddress = ""
|
VMHostIPAddress = ""
|
||||||
DiskSizeGB = 30
|
DiskSizeGB = 50
|
||||||
MemoryGB = 4
|
MemoryGB = 4
|
||||||
Processors = 4
|
Processors = 4
|
||||||
VMLocation = $vmLocationPath
|
VMLocation = $vmLocationPath
|
||||||
@@ -173,6 +176,7 @@ function Get-GeneralDefaults {
|
|||||||
InstallDrivers = $false
|
InstallDrivers = $false
|
||||||
CopyDrivers = $false
|
CopyDrivers = $false
|
||||||
CopyPEDrivers = $false
|
CopyPEDrivers = $false
|
||||||
|
UseDriversAsPEDrivers = $false
|
||||||
UpdateADK = $true
|
UpdateADK = $true
|
||||||
CompressDownloadedDriversToWim = $false
|
CompressDownloadedDriversToWim = $false
|
||||||
}
|
}
|
||||||
@@ -195,6 +199,65 @@ function Get-USBDrives {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Returns a list of FFU files from the provided folder with selection metadata
|
||||||
|
function Get-FFUFiles {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
if (-not (Test-Path -Path $Path)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
Get-ChildItem -Path $Path -Filter '*.ffu' -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
IsSelected = $false
|
||||||
|
Name = $_.Name
|
||||||
|
LastModified = $_.LastWriteTime
|
||||||
|
FullName = $_.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Populate Additional FFU List from the capture folder
|
||||||
|
function Update-AdditionalFFUList {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[PSCustomObject]$State
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
$ffuFolder = $State.Controls.txtFFUCaptureLocation.Text
|
||||||
|
$listView = $State.Controls.lstAdditionalFFUs
|
||||||
|
if ($null -eq $listView) { return }
|
||||||
|
$listView.Items.Clear()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ffuFolder) -or -not (Test-Path -Path $ffuFolder)) {
|
||||||
|
WriteLog "Additional FFUs: Capture folder not set or not found: $ffuFolder"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$items = Get-ChildItem -Path $ffuFolder -Filter '*.ffu' -File -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
ForEach-Object {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
IsSelected = $false
|
||||||
|
Name = $_.Name
|
||||||
|
LastModified = $_.LastWriteTime
|
||||||
|
FullName = $_.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($it in $items) { $listView.Items.Add($it) | Out-Null }
|
||||||
|
WriteLog "Additional FFUs: Found $($listView.Items.Count) FFU files in $ffuFolder."
|
||||||
|
}
|
||||||
|
$headerChk = $State.Controls.chkSelectAllAdditionalFFUs
|
||||||
|
if ($null -ne $headerChk) {
|
||||||
|
Update-SelectAllHeaderCheckBoxState -ListView $listView -HeaderCheckBox $headerChk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
WriteLog "Update-AdditionalFFUList error: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Function to manage the visibility of the application UI panels
|
# Function to manage the visibility of the application UI panels
|
||||||
function Update-ApplicationPanelVisibility {
|
function Update-ApplicationPanelVisibility {
|
||||||
param(
|
param(
|
||||||
@@ -202,6 +265,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 +336,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,11 +353,14 @@ function Update-DriverCheckboxStates {
|
|||||||
$installDriversChk = $State.Controls.chkInstallDrivers
|
$installDriversChk = $State.Controls.chkInstallDrivers
|
||||||
$copyDriversChk = $State.Controls.chkCopyDrivers
|
$copyDriversChk = $State.Controls.chkCopyDrivers
|
||||||
$compressWimChk = $State.Controls.chkCompressDriversToWIM
|
$compressWimChk = $State.Controls.chkCompressDriversToWIM
|
||||||
|
$copyPEDriversChk = $State.Controls.chkCopyPEDrivers
|
||||||
|
$useDriversAsPeChk = $State.Controls.chkUseDriversAsPEDrivers
|
||||||
|
|
||||||
# Default to enabled, then apply disabling rules
|
# Default to enabled, then apply disabling rules
|
||||||
$installDriversChk.IsEnabled = $true
|
$installDriversChk.IsEnabled = $true
|
||||||
$copyDriversChk.IsEnabled = $true
|
$copyDriversChk.IsEnabled = $true
|
||||||
$compressWimChk.IsEnabled = $true
|
$compressWimChk.IsEnabled = $true
|
||||||
|
$copyPEDriversChk.IsEnabled = $true
|
||||||
|
|
||||||
if ($installDriversChk.IsChecked) {
|
if ($installDriversChk.IsChecked) {
|
||||||
$copyDriversChk.IsEnabled = $false
|
$copyDriversChk.IsEnabled = $false
|
||||||
@@ -297,6 +374,16 @@ function Update-DriverCheckboxStates {
|
|||||||
if ($compressWimChk.IsChecked) {
|
if ($compressWimChk.IsChecked) {
|
||||||
$installDriversChk.IsEnabled = $false
|
$installDriversChk.IsEnabled = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Sub-option visibility logic: only show UseDriversAsPEDrivers when CopyPEDrivers is checked
|
||||||
|
if ($copyPEDriversChk.IsChecked) {
|
||||||
|
$useDriversAsPeChk.Visibility = 'Visible'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Parent unchecked: hide and clear sub-option
|
||||||
|
$useDriversAsPeChk.IsChecked = $false
|
||||||
|
$useDriversAsPeChk.Visibility = 'Collapsed'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to manage the visibility of Office UI panels
|
# Function to manage the visibility of Office UI panels
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function Write-ProgressLog {
|
|||||||
}
|
}
|
||||||
Function Get-RemovableDrive {
|
Function Get-RemovableDrive {
|
||||||
writelog "Get information for all removable drives"
|
writelog "Get information for all removable drives"
|
||||||
$USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media"}
|
$USBDrives = Get-WmiObject Win32_DiskDrive | Where-Object {$_.MediaType -eq "Removable media" -or $_.MediaType -eq "External hard disk media"}
|
||||||
If($USBDrives -and ($null -eq $USBDrives.count)) {
|
If($USBDrives -and ($null -eq $USBDrives.count)) {
|
||||||
$USBDrivesCount = 1
|
$USBDrivesCount = 1
|
||||||
} else {
|
} else {
|
||||||
@@ -62,6 +62,7 @@ Function Build-DeploymentUSB{
|
|||||||
$ScriptBlock = {
|
$ScriptBlock = {
|
||||||
param($DriveNumber)
|
param($DriveNumber)
|
||||||
Clear-Disk -Number $DriveNumber -RemoveData -RemoveOEM -Confirm:$false
|
Clear-Disk -Number $DriveNumber -RemoveData -RemoveOEM -Confirm:$false
|
||||||
|
Initialize-Disk -Number $DriveNumber
|
||||||
$Disk = Get-Disk -Number $DriveNumber
|
$Disk = Get-Disk -Number $DriveNumber
|
||||||
$PartitionStyle = $Disk.PartitionStyle
|
$PartitionStyle = $Disk.PartitionStyle
|
||||||
if($PartitionStyle -ne 'MBR'){
|
if($PartitionStyle -ne 'MBR'){
|
||||||
|
|||||||
@@ -203,12 +203,38 @@ 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 with HP-specific adjustments.
|
||||||
|
# Remove inch unit variants (23.8-in, 23.8 inch, 23inch, 23-in, etc.) keeping only the numeric size.
|
||||||
|
# Canonicalize All-in-One variants (All in One, All-in-One, All-in-One PC, AiO, AIO) to 'AIO'.
|
||||||
|
# Convert any non-alphanumeric sequence to a single space, collapse whitespace, and trim.
|
||||||
|
if ($null -eq $Text) { return '' }
|
||||||
|
$original = $Text
|
||||||
|
# Remove inch unit variants while preserving the numeric size
|
||||||
|
$Text = [regex]::Replace($Text, '(?i)(\d+(?:\.\d+)?)(?:\s*[-]?\s*)(?:in|inch)\b', '$1')
|
||||||
|
# Canonicalize All-in-One variants
|
||||||
|
$Text = [regex]::Replace($Text, '(?i)\bAll[\s-]*in[\s-]*One(?:\s*PC)?\b', 'AIO')
|
||||||
|
$Text = [regex]::Replace($Text, '(?i)\bAiO\b', 'AIO')
|
||||||
|
# Generic normalization
|
||||||
|
$normalized = ($Text -replace '[^A-Za-z0-9]+', ' ')
|
||||||
|
$normalized = ($normalized -replace '\s+', ' ').Trim()
|
||||||
|
if ($normalized -ne $original) {
|
||||||
|
WriteLog "Normalized model string: Original='$original' -> Normalized='$normalized'"
|
||||||
|
}
|
||||||
|
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 = '2509.1Preview'
|
||||||
WriteLog 'Begin Logging'
|
WriteLog 'Begin Logging'
|
||||||
WriteLog "Script version: $version"
|
WriteLog "Script version: $version"
|
||||||
|
|
||||||
@@ -337,8 +363,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 +407,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 +420,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 +571,19 @@ 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 = @()
|
||||||
|
# Normalize system model once outside the loop
|
||||||
|
$systemModelNorm = ConvertTo-ComparableModelName -Text $systemModel
|
||||||
foreach ($rule in $driverMappings) {
|
foreach ($rule in $driverMappings) {
|
||||||
# Use -like for wildcard matching.
|
# Use -like for wildcard matching.
|
||||||
|
# Prepare normalized rule model string
|
||||||
|
$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 +876,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 +924,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 {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ The Full-Flash update (FFU) process can automatically download the latest releas
|
|||||||
|
|
||||||
# Updates
|
# Updates
|
||||||
|
|
||||||
2507.1 has been released to preview! This is a major update that brings a new user interface to preview.
|
2509.1 has been released to preview! This is a major update that brings a new user interface to preview.
|
||||||
|
|
||||||
Docs are coming, but will take a bit to write them. The youtube video is a must watch for a complete demo on how to use the UI and the changes made to apps (InstallAppsAndSysprep.cmd is gone) and drivers. I'll be recording a more formalized deep dive with slides that go a bit deeper into how things work, but the UI walkthrough should get most people going.
|
Docs are coming, but will take a bit to write them. The youtube video is a must watch for a complete demo on how to use the UI and the changes made to apps (InstallAppsAndSysprep.cmd is gone) and drivers. I'll be recording a more formalized deep dive with slides that go a bit deeper into how things work, but the UI walkthrough should get most people going.
|
||||||
|
|
||||||
@@ -32,41 +32,74 @@ Docs are coming, but will take a bit to write them. The youtube video is a must
|
|||||||
|
|
||||||
## YouTube Detailed Walkthrough
|
## YouTube Detailed Walkthrough
|
||||||
|
|
||||||
Here's a detailed overview of the new UI process.
|
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