Compare commits

..

21 Commits

Author SHA1 Message Date
rbalsleyMSFT 08c9214976 Adds 2509.1 release notes
Documents new preview release and bumps displayed version. Notes broadened artifact cleanup, larger default disk size, auto load/restore of environment, centralized cleanup, dynamic PE driver sourcing, driver model normalization, deferred driver folder cleanup, safer name sanitization, improved config import & JSON determinism, per‑app exit code controls (incl. winget), multi‑FFU USB inclusion, removal of obsolete ESD workaround, and added Windows 11 25H2 support to improve UX, reliability, and maintainability.
2025-10-01 17:40:29 -07:00
rbalsleyMSFT c110dcd40e Updates preview version to 2509.1
Increments internal preview version for build and deployment scripts to reflect next preview cycle, ensuring logs and artifacts show the correct release identifier for traceability.
2025-10-01 17:19:39 -07:00
rbalsleyMSFT eaa3e1e6af Adds Windows 11 25H2 mapping
Extends supported Windows 11 releases to include 25H2 to keep version resolution current for upcoming media and configuration scenarios.
2025-10-01 13:23:46 -07:00
rbalsleyMSFT 6562d16ce5 Standardizes JSON output: depth, UTF-8, key order
- 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.
2025-09-20 13:30:41 -07:00
rbalsleyMSFT 15a5b16b39 Adds UI/CLI to copy additional FFUs to USB build
- 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.
2025-09-18 18:17:58 -07:00
rbalsleyMSFT d9c0c9c68e Adds exit-code overrides and UI for winget apps
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.
2025-09-18 15:15:00 -07:00
rbalsleyMSFT d1ca123104 Sanitizes app names for storage and paths
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.
2025-09-17 13:22:17 -07:00
rbalsleyMSFT f37647599a Includes exit code fields when using Copy Apps button
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.
2025-09-16 17:05:34 -07:00
rbalsleyMSFT cb14e84a26 Add robust sanitization for names used in paths
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.
2025-09-16 16:43:43 -07:00
rbalsleyMSFT 8d7e4d1066 Refactor config loading and improve error handling
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.
2025-09-15 18:10:39 -07:00
rbalsleyMSFT c30ed923b6 feat: Defer cleanup of compressed driver source folders
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.
2025-09-12 15:08:48 -07:00
rbalsleyMSFT 50713188bf Refactor: Improve model name normalization for driver mapping
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.
2025-09-11 18:15:51 -07:00
rbalsleyMSFT e2ccd11f07 feat: Add option to dynamically build PE drivers
Introduces a new feature, `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.
2025-09-11 12:13:06 -07:00
rbalsleyMSFT f3316a017b feat: Add restore defaults and centralize cleanup logic
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.
2025-09-10 11:31:53 -07:00
rbalsleyMSFT bdf1b63833 Improves UI state after environment autoload
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.
2025-09-09 18:00:52 -07:00
rbalsleyMSFT 3ef26f2918 Adds auto-loading of previous configuration on startup
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.
2025-09-09 17:49:36 -07:00
rbalsleyMSFT 372360d739 Update default disk size to 50GB in FFU scripts and UI
- 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.
2025-09-03 12:06:11 -07:00
rbalsleyMSFT dc5877f398 Removes the VM workaround for MCT ESD builds
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.
2025-08-28 13:56:54 -07:00
rbalsleyMSFT 49b2113fe1 Merge pull request #262 from jrollmann/UI_2508
Update USBImagingToolCreator.ps1
2025-08-27 15:38:12 -07:00
jrollmann 556cfa1ee3 Update USBImagingToolCreator.ps1 2025-08-27 17:07:52 -04:00
rbalsleyMSFT 1ab4093d54 Refactor: Enhance artifact cleanup for disabled features
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.
2025-08-27 12:40:11 -07:00
25 changed files with 1654 additions and 234 deletions
+120
View File
@@ -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 nonzero 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 :)
+354 -82
View File
@@ -52,7 +52,7 @@ When set to $true, will copy the $FFUDevelopmentPath\Autopilot folder to the Dep
When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false. When set to $true, will copy the drivers from the $FFUDevelopmentPath\Drivers folder to the Drivers folder on the deploy partition of the USB drive. Default is $false.
.PARAMETER CopyPEDrivers .PARAMETER CopyPEDrivers
When set to $true, will copy the drivers from the $FFUDevelopmentPath\PEDrivers folder to the WinPE deployment media. Default is $false. When set to $true, enables adding WinPE drivers. By default copies drivers from $FFUDevelopmentPath\PEDrivers to the WinPE deployment media unless -UseDriversAsPEDrivers is also $true.
.PARAMETER CopyPPKG .PARAMETER CopyPPKG
When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false. When set to $true, will copy the provisioning package from the $FFUDevelopmentPath\PPKG folder to the Deployment partition of the USB drive. Default is $false.
@@ -70,7 +70,7 @@ When set to $true, this will create WinPE deployment media for use when deployin
Sets a custom FFU output name with placeholders. Allowed placeholders are: {WindowsRelease}, {WindowsVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt}. Sets a custom FFU output name with placeholders. Allowed placeholders are: {WindowsRelease}, {WindowsVersion}, {SKU}, {BuildDate}, {yyyy}, {MM}, {dd}, {H}, {hh}, {mm}, {tt}.
.PARAMETER Disksize .PARAMETER Disksize
Size of the virtual hard disk for the virtual machine. Default is a 30GB dynamic disk. Size of the virtual hard disk for the virtual machine. Default is a 50GB dynamic disk.
.PARAMETER DriversFolder .PARAMETER DriversFolder
Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers. Path to the drivers folder. Default is $FFUDevelopmentPath\Drivers.
@@ -132,7 +132,7 @@ When set to $true, will optimize the FFU file. Default is $true.
.PARAMETER OptionalFeatures .PARAMETER OptionalFeatures
Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP). Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP).
.PARAMETER orchestrationPath .PARAMETER OrchestrationPath
Path to the orchestration folder containing scripts that run inside the VM. Default is $FFUDevelopmentPath\Apps\Orchestration. Path to the orchestration folder containing scripts that run inside the VM. Default is $FFUDevelopmentPath\Apps\Orchestration.
.PARAMETER PEDriversFolder .PARAMETER PEDriversFolder
@@ -186,6 +186,9 @@ When set to $true, will download and install the latest OneDrive and install it
.PARAMETER UpdatePreviewCU .PARAMETER UpdatePreviewCU
When set to $true, will download and install the latest Preview cumulative update. Default is $false. When set to $true, will download and install the latest Preview cumulative update. Default is $false.
.PARAMETER UseDriversAsPEDrivers
When set to $true (and -CopyPEDrivers is also $true), bypasses the contents of $FFUDevelopmentPath\PEDrivers and instead builds the WinPE driver set dynamically from the $DriversFolder path, copying only the required WinPE drivers. Has no effect if -CopyPEDrivers is not specified. Default is $false.
.PARAMETER UserAppListPath .PARAMETER UserAppListPath
Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json. Path to a JSON file containing a list of user-defined applications to install. Default is $FFUDevelopmentPath\Apps\UserAppList.json.
@@ -304,7 +307,7 @@ param(
[string]$Model, [string]$Model,
[bool]$InstallDrivers, [bool]$InstallDrivers,
[uint64]$Memory = 4GB, [uint64]$Memory = 4GB,
[uint64]$Disksize = 30GB, [uint64]$Disksize = 50GB,
[int]$Processors = 4, [int]$Processors = 4,
[string]$VMSwitchName, [string]$VMSwitchName,
[string]$VMLocation, [string]$VMLocation,
@@ -378,7 +381,10 @@ param(
[bool]$CompressDownloadedDriversToWim = $false, [bool]$CompressDownloadedDriversToWim = $false,
[bool]$CopyDrivers, [bool]$CopyDrivers,
[bool]$CopyPEDrivers, [bool]$CopyPEDrivers,
[bool]$UseDriversAsPEDrivers,
[bool]$RemoveFFU, [bool]$RemoveFFU,
[bool]$CopyAdditionalFFUFiles,
[string[]]$AdditionalFFUFiles,
[bool]$UpdateLatestCU, [bool]$UpdateLatestCU,
[bool]$UpdatePreviewCU, [bool]$UpdatePreviewCU,
[bool]$UpdateLatestMicrocode, [bool]$UpdateLatestMicrocode,
@@ -430,7 +436,7 @@ param(
[switch]$Cleanup [switch]$Cleanup
) )
$ProgressPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue'
$version = '2508.1Preview' $version = '2509.1Preview'
# Remove any existing modules to avoid conflicts # Remove any existing modules to avoid conflicts
if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) { if (Get-Module -Name 'FFU.Common.Core' -ErrorAction SilentlyContinue) {
@@ -553,6 +559,26 @@ class VhdxCacheItem {
[VhdxCacheUpdateItem[]]$IncludedUpdates = @() [VhdxCacheUpdateItem[]]$IncludedUpdates = @()
} }
#Support for ini reading
$definition = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
System.Text.StringBuilder lpReturnedString,
uint nSize,
string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern uint GetPrivateProfileSection(
string lpAppName,
byte[] lpReturnedString,
uint nSize,
string lpFileName);
'@
Add-Type -MemberDefinition $definition -Namespace Win32 -Name Kernel32 -PassThru
#Check if Hyper-V feature is installed (requires only checks the module) #Check if Hyper-V feature is installed (requires only checks the module)
$osInfo = Get-WmiObject -Class Win32_OperatingSystem $osInfo = Get-WmiObject -Class Win32_OperatingSystem
$isServer = $osInfo.Caption -match 'server' $isServer = $osInfo.Caption -match 'server'
@@ -579,10 +605,11 @@ if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" }
if (-not $UserAppListPath) { $UserAppListPath = "$AppsPath\UserAppList.json" } if (-not $UserAppListPath) { $UserAppListPath = "$AppsPath\UserAppList.json" }
if (-not $OrchestrationPath) { $OrchestrationPath = "$AppsPath\Orchestration" } if (-not $OrchestrationPath) { $OrchestrationPath = "$AppsPath\Orchestration" }
if (-not $wingetWin32jsonFile) { $wingetWin32jsonFile = "$OrchestrationPath\WinGetWin32Apps.json" } if (-not $wingetWin32jsonFile) { $wingetWin32jsonFile = "$OrchestrationPath\WinGetWin32Apps.json" }
if (-not $InstallDefenderPath) { $installDefenderPath = "$OrchestrationPath\Update-Defender.ps1" } if (-not $InstallOfficePath) { $InstallOfficePath = "$OrchestrationPath\Install-Office.ps1" }
if (-not $InstallMSRTPath) { $installMSRTPath = "$OrchestrationPath\Update-MSRT.ps1" } if (-not $InstallDefenderPath) { $InstallDefenderPath = "$OrchestrationPath\Update-Defender.ps1" }
if (-not $InstallODPath) { $installODPath = "$OrchestrationPath\Update-OneDrive.ps1" } if (-not $InstallMSRTPath) { $InstallMSRTPath = "$OrchestrationPath\Update-MSRT.ps1" }
if (-not $InstallEdgePath) { $installEdgePath = "$OrchestrationPath\Update-Edge.ps1" } if (-not $InstallODPath) { $InstallODPath = "$OrchestrationPath\Update-OneDrive.ps1" }
if (-not $InstallEdgePath) { $InstallEdgePath = "$OrchestrationPath\Update-Edge.ps1" }
if (-not $AppsScriptVarsJsonPath) { $AppsScriptVarsJsonPath = "$OrchestrationPath\AppsScriptVariables.json" } if (-not $AppsScriptVarsJsonPath) { $AppsScriptVarsJsonPath = "$OrchestrationPath\AppsScriptVariables.json" }
if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" } if (-not $DeployISO) { $DeployISO = "$FFUDevelopmentPath\WinPE_FFU_Deploy_$WindowsArch.iso" }
if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" } if (-not $CaptureISO) { $CaptureISO = "$FFUDevelopmentPath\WinPE_FFU_Capture_$WindowsArch.iso" }
@@ -879,8 +906,10 @@ function Get-MicrosoftDrivers {
New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null New-Item -Path $DriversFolder -ItemType Directory -Force | Out-Null
WriteLog "Drivers folder created" WriteLog "Drivers folder created"
} }
$sanitizedModel = ConvertTo-SafeName -Name $Model
if ($sanitizedModel -ne $Model) { WriteLog "Sanitized model name: '$Model' -> '$sanitizedModel'" }
$surfaceDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make $surfaceDriversPath = Join-Path -Path $DriversFolder -ChildPath $Make
$modelPath = Join-Path -Path $surfaceDriversPath -ChildPath $Model $modelPath = Join-Path -Path $surfaceDriversPath -ChildPath $sanitizedModel
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 | Out-Null New-Item -Path $modelPath -ItemType Directory | Out-Null
@@ -2338,7 +2367,7 @@ function New-ScratchVhdx {
param( param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$VhdxPath, [string]$VhdxPath,
[uint64]$SizeBytes = 30GB, [uint64]$SizeBytes = 50GB,
[uint32]$LogicalSectorSizeBytes, [uint32]$LogicalSectorSizeBytes,
[switch]$Dynamic, [switch]$Dynamic,
[Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]$PartitionStyle = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.Disk.PartitionStyle]::GPT
@@ -2624,6 +2653,110 @@ Function Set-CaptureFFU {
} }
} }
function Get-PrivateProfileString {
param (
[Parameter()]
[string]$FileName,
[Parameter()]
[string]$SectionName,
[Parameter()]
[string]$KeyName
)
$sbuilder = [System.Text.StringBuilder]::new(1024)
[void][Win32.Kernel32]::GetPrivateProfileString($SectionName, $KeyName, "", $sbuilder, $sbuilder.Capacity, $FileName)
return $sbuilder.ToString()
}
function Get-PrivateProfileSection {
param (
[Parameter()]
[string]$FileName,
[Parameter()]
[string]$SectionName
)
$buffer = [byte[]]::new(16384)
[void][Win32.Kernel32]::GetPrivateProfileSection($SectionName, $buffer, $buffer.Length, $FileName)
$keyValues = [System.Text.Encoding]::Unicode.GetString($buffer).TrimEnd("`0").Split("`0")
$hashTable = @{}
foreach ($keyValue in $keyValues) {
if (![string]::IsNullOrEmpty($keyValue)) {
$parts = $keyValue -split "="
$hashTable[$parts[0]] = $parts[1]
}
}
return $hashTable
}
function Copy-Drivers {
param (
[Parameter()]
[string]$Path,
[Parameter()]
[string]$Output
)
# Find more information about device classes here:
# https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
# For now, included are system devices, scsi and raid controllers, keyboards, mice and HID devices for touch support
# 4D36E97D-E325-11CE-BFC1-08002BE10318 = System devices
# 4D36E97B-E325-11CE-BFC1-08002BE10318 = SCSI, RAID, and NVMe Controllers
# 4d36e96b-e325-11ce-bfc1-08002be10318 = Keyboards
# 4d36e96f-e325-11ce-bfc1-08002be10318 = Mice and other pointing devices
# 745a17a0-74d3-11d0-b6fe-00a0c90f57da = Human Interface Devices
$filterGUIDs = @("{4D36E97D-E325-11CE-BFC1-08002BE10318}", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "{4d36e96b-e325-11ce-bfc1-08002be10318}", "{4d36e96f-e325-11ce-bfc1-08002be10318}", "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}")
$exclusionList = "wdmaudio.inf|Sound|Machine Learning|Camera|Firmware"
$pathLength = $Path.Length
$infFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.inf"
for ($i = 0; $i -lt $infFiles.Count; $i++) {
$infFullName = $infFiles[$i].FullName
$infPath = Split-Path -Path $infFullName
$childPath = $infPath.Substring($pathLength)
$targetPath = Join-Path -Path $Output -ChildPath $childPath
if ((Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "ClassGUID") -in $filterGUIDs) {
#Avoid drivers that reference keywords from the exclusion list to keep the total size small
if (((Get-Content -Path $infFullName) -match $exclusionList).Length -eq 0) {
$providerName = (Get-PrivateProfileString -FileName $infFullName -SectionName "Version" -KeyName "Provider").Trim("%")
WriteLog "Copying PE drivers for $providerName"
WriteLog "Driver inf is: $infFullName"
[void](New-Item -Path $targetPath -ItemType Directory -Force)
Copy-Item -Path $infFullName -Destination $targetPath -Force
$CatalogFileName = Get-PrivateProfileString -FileName $infFullName -SectionName "version" -KeyName "Catalogfile"
Copy-Item -Path "$infPath\$CatalogFileName" -Destination $targetPath -Force
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles"
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
}
else {
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
}
}
#Arch specific files override the files specified in the universal section
$sourceDiskFiles = Get-PrivateProfileSection -FileName $infFullName -SectionName "SourceDisksFiles.$WindowsArch"
foreach ($sourceDiskFile in $sourceDiskFiles.Keys) {
if (!$sourceDiskFiles[$sourceDiskFile].Contains(",")) {
Copy-Item -Path "$infPath\$sourceDiskFile" -Destination $targetPath -Force
}
else {
$subdir = ($sourceDiskFiles[$sourceDiskFile] -split ",")[1]
[void](New-Item -Path "$targetPath\$subdir" -ItemType Directory -Force)
Copy-Item -Path "$infPath\$subdir\$sourceDiskFile" -Destination "$targetPath\$subdir" -Force
}
}
}
}
}
}
function New-PEMedia { function New-PEMedia {
param ( param (
[Parameter()] [Parameter()]
@@ -2701,9 +2834,34 @@ function New-PEMedia {
WriteLog 'Copy complete' WriteLog 'Copy complete'
#If $CopyPEDrivers = $true, add drivers to WinPE media using dism #If $CopyPEDrivers = $true, add drivers to WinPE media using dism
if ($CopyPEDrivers) { if ($CopyPEDrivers) {
if ($UseDriversAsPEDrivers) {
WriteLog "UseDriversAsPEDrivers is set. Building WinPE driver set from Drivers folder (bypassing PEDrivers folder contents)."
if (Test-Path -Path $PEDriversFolder) {
try {
Remove-Item -Path (Join-Path $PEDriversFolder '*') -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
WriteLog "Warning: Failed clearing existing PEDriversFolder contents: $($_.Exception.Message)"
}
}
else {
try {
New-Item -Path $PEDriversFolder -ItemType Directory -Force | Out-Null
}
catch {
WriteLog "Error: Failed to create PEDriversFolder at $PEDriversFolder - continuing may fail when adding drivers."
}
}
WriteLog "Copying required WinPE drivers from Drivers folder"
Copy-Drivers -Path $DriversFolder -Output $PEDriversFolder
}
else {
WriteLog "Copying PE drivers from PEDrivers folder"
}
WriteLog "Adding drivers to WinPE media" WriteLog "Adding drivers to WinPE media"
try { try {
Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver "$PEDriversFolder" -Recurse -ErrorAction SilentlyContinue | Out-null Add-WindowsDriver -Path "$WinPEFFUPath\Mount" -Driver $PEDriversFolder -Recurse -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-null
} }
catch { catch {
WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.' WriteLog 'Some drivers failed to be added to the FFU. This can be expected. Continuing.'
@@ -2750,6 +2908,41 @@ function New-PEMedia {
WriteLog "Cleaning up $WinPEFFUPath" WriteLog "Cleaning up $WinPEFFUPath"
Remove-Item -Path "$WinPEFFUPath" -Recurse -Force Remove-Item -Path "$WinPEFFUPath" -Recurse -Force
WriteLog 'Cleanup complete' WriteLog 'Cleanup complete'
# Deferred cleanup of preserved driver model folders (only after WinPE Deploy media is created)
if ($UseDriversAsPEDrivers -and $CompressDownloadedDriversToWim -and $Deploy -and $CopyPEDrivers) {
WriteLog "Beginning deferred cleanup of preserved driver model folders (UseDriversAsPEDrivers + compression scenario)."
$removedCount = 0
$skippedCount = 0
if (Test-Path -Path $DriversFolder) {
Get-ChildItem -Path $DriversFolder -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$makeDir = $_.FullName
Get-ChildItem -Path $makeDir -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$modelDir = $_.FullName
$markerFile = Join-Path -Path $modelDir -ChildPath '__PreservedForPEDrivers.txt'
$leaf = Split-Path -Path $modelDir -Leaf
$wimPath = Join-Path -Path $makeDir -ChildPath ($leaf + '.wim')
if ((Test-Path -Path $markerFile -PathType Leaf) -and (Test-Path -Path $wimPath -PathType Leaf)) {
try {
WriteLog "Removing preserved driver folder: $modelDir (WIM located at $wimPath)"
Remove-Item -Path $modelDir -Recurse -Force -ErrorAction Stop
$removedCount++
}
catch {
WriteLog "Warning: Failed to remove preserved folder $modelDir : $($_.Exception.Message)"
$skippedCount++
}
}
else {
$skippedCount++
}
}
}
WriteLog "Deferred driver cleanup complete. Removed: $removedCount; Skipped: $skippedCount"
}
else {
WriteLog "Drivers folder $DriversFolder not found during deferred cleanup."
}
}
} }
function Optimize-FFUCaptureDrive { function Optimize-FFUCaptureDrive {
@@ -3271,7 +3464,8 @@ Function Get-USBDrive {
} }
Function New-DeploymentUSB { Function New-DeploymentUSB {
param( param(
[switch]$CopyFFU [switch]$CopyFFU,
[string[]]$FFUFilesToCopy
) )
WriteLog "CopyFFU is set to $CopyFFU" WriteLog "CopyFFU is set to $CopyFFU"
$BuildUSBPath = $PSScriptRoot $BuildUSBPath = $PSScriptRoot
@@ -3281,14 +3475,28 @@ Function New-DeploymentUSB {
# 1. Get FFU File(s) - This happens once before parallel processing # 1. Get FFU File(s) - This happens once before parallel processing
if ($CopyFFU.IsPresent) { if ($CopyFFU.IsPresent) {
if ($null -ne $FFUFilesToCopy -and $FFUFilesToCopy.Count -gt 0) {
$SelectedFFUFile = $FFUFilesToCopy
WriteLog "Using preselected FFU file list. Count: $($FFUFilesToCopy.Count)"
WriteLog "FFU files to copy:"
foreach ($f in $FFUFilesToCopy) {
WriteLog ("- {0}" -f (Split-Path $f -Leaf))
}
}
else {
$FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu" $FFUFiles = Get-ChildItem -Path "$BuildUSBPath\FFU" -Filter "*.ffu"
$FFUCount = $FFUFiles.count $FFUCount = $FFUFiles.count
if ($FFUCount -eq 1) { switch ($FFUCount) {
0 {
Write-Error "No FFU files found in $BuildUSBPath\FFU. Cannot copy FFU to USB drive."
return
}
1 {
$SelectedFFUFile = $FFUFiles.FullName $SelectedFFUFile = $FFUFiles.FullName
WriteLog "One FFU file found, will use: $SelectedFFUFile" WriteLog "One FFU file found, will use: $SelectedFFUFile"
} }
elseif ($FFUCount -gt 1) { default {
WriteLog "Found $FFUCount FFU files" WriteLog "Found $FFUCount FFU files"
if ($VerbosePreference -ne 'Continue') { if ($VerbosePreference -ne 'Continue') {
Write-Host "Found $FFUCount FFU files" Write-Host "Found $FFUCount FFU files"
@@ -3318,9 +3526,7 @@ Function New-DeploymentUSB {
} }
} while ($null -eq $SelectedFFUFile) } while ($null -eq $SelectedFFUFile)
} }
else { }
Write-Error "No FFU files found in $BuildUSBPath\FFU. Cannot copy FFU to USB drive."
Return
} }
} }
@@ -3597,7 +3803,26 @@ function Remove-FFU {
Remove-Item -Path $FFUCaptureLocation\*.ffu -Force Remove-Item -Path $FFUCaptureLocation\*.ffu -Force
WriteLog "Removal complete" WriteLog "Removal complete"
} }
Function Remove-DisabledUpdates { Function Remove-DisabledArtifacts {
# Remove Office artifacts if Install Office is disabled
if (-not $InstallOffice) {
$removed = $false
if (Test-Path -Path $installOfficePath) {
WriteLog "Install Office disabled - removing $installOfficePath"
Remove-Item -Path $installOfficePath -Force -ErrorAction SilentlyContinue
$removed = $true
}
if (Test-Path -Path $OfficePath) {
WriteLog 'Removing Office and ODT download'
$OfficeDownloadPath = "$OfficePath\Office"
Remove-Item -Path $OfficeDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$OfficePath\setup.exe" -Recurse -Force -ErrorAction SilentlyContinue
$removed = $true
}
if ($removed) { WriteLog 'Removal complete' }
}
# Remove Defender artifacts if Defender update is disabled # Remove Defender artifacts if Defender update is disabled
if (-not $UpdateLatestDefender) { if (-not $UpdateLatestDefender) {
$removed = $false $removed = $false
@@ -3779,7 +4004,7 @@ function Export-ConfigFile {
} }
# Convert to JSON and save # Convert to JSON and save
$orderedParams | ConvertTo-Json | Out-File $ExportConfigFile -Force $orderedParams | ConvertTo-Json -Depth 10 | Set-Content -Path $ExportConfigFile -Encoding UTF8
} }
function Get-PEArchitecture { function Get-PEArchitecture {
param( param(
@@ -4412,6 +4637,29 @@ if ($CopyPEDrivers) {
WriteLog "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again." WriteLog "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again."
throw "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again." throw "Driver folder path $PEDriversFolder contains spaces. Please remove spaces from the path and try again."
} }
if ($UseDriversAsPEDrivers) {
# When using Drivers as PE drivers, skip strict PEDrivers folder existence/content checks.
$driverSourceAvailable = $false
if ($DriversJsonPath -and (Test-Path -Path $DriversJsonPath)) {
$driverSourceAvailable = $true
WriteLog "Drivers JSON path is set to $DriversJsonPath; drivers will be downloaded for WinPE."
}
elseif ($Make -and $Model) {
$driverSourceAvailable = $true
WriteLog "Make/Model ($Make / $Model) specified; drivers will be downloaded for WinPE."
}
elseif ((Test-Path -Path $DriversFolder) -and ((Get-ChildItem -Path $DriversFolder -Recurse | Measure-Object -Property Length -Sum).Sum -ge 1MB)) {
$driverSourceAvailable = $true
WriteLog "Drivers folder contains existing content; will reuse for WinPE."
}
if (-not $driverSourceAvailable) {
WriteLog "-UseDriversAsPEDrivers is set, but no driver sources are available (Drivers folder missing/empty and no download instructions)."
throw "-UseDriversAsPEDrivers is set, but no driver sources are available (Drivers folder missing/empty and no download instructions)."
}
WriteLog "UseDriversAsPEDrivers is set. Skipping PEDrivers folder existence/content checks; drivers will be sourced from Drivers folder (or downloaded)."
WriteLog 'PEDriver validation complete'
}
else {
if (!(Test-Path -Path $PEDriversFolder)) { if (!(Test-Path -Path $PEDriversFolder)) {
WriteLog "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing" WriteLog "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing"
throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing" throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is missing"
@@ -4421,6 +4669,7 @@ if ($CopyPEDrivers) {
throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty" throw "-CopyPEDrivers is set to `$true, but the $PEDriversFolder folder is empty"
} }
WriteLog 'PEDriver validation complete' WriteLog 'PEDriver validation complete'
}
} }
#Validate PPKG folder #Validate PPKG folder
@@ -4479,10 +4728,10 @@ if ($InstallApps) {
#Override $InstallApps value if using ESD to build FFU. This is due to a strange issue where building the FFU #Override $InstallApps value if using ESD to build FFU. This is due to a strange issue where building the FFU
#from vhdx doesn't work (you get an older style OOBE screen and get stuck in an OOBE reboot loop when hitting next). #from vhdx doesn't work (you get an older style OOBE screen and get stuck in an OOBE reboot loop when hitting next).
#This behavior doesn't happen with WIM files. #This behavior doesn't happen with WIM files.
If (-not ($ISOPath) -and (-not ($InstallApps))) { # If (-not ($ISOPath) -and (-not ($InstallApps))) {
$InstallApps = $true # $InstallApps = $true
WriteLog "Script will download Windows media. Setting `$InstallApps to `$true to build VM to capture FFU. Must do this when using MCT ESD." # WriteLog "Script will download Windows media. Setting `$InstallApps to `$true to build VM to capture FFU. Must do this when using MCT ESD."
} # }
if (($InstallOffice -eq $true) -and ($InstallApps -eq $false)) { if (($InstallOffice -eq $true) -and ($InstallApps -eq $false)) {
throw "If variable InstallOffice is set to `$true, InstallApps must also be set to `$true." throw "If variable InstallOffice is set to `$true, InstallApps must also be set to `$true."
@@ -4583,6 +4832,49 @@ If (Test-Path -Path "$FFUDevelopmentPath\dirty.txt") {
WriteLog 'Creating dirty.txt file' WriteLog 'Creating dirty.txt file'
New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null New-Item -Path .\ -Name "dirty.txt" -ItemType "file" | Out-Null
# Early CLI prompt for additional FFUs (only if enabled and not provided)
if ($BuildUSBDrive -and $CopyAdditionalFFUFiles -and ((-not $AdditionalFFUFiles) -or ($AdditionalFFUFiles.Count -eq 0))) {
try {
$ffuFolder = Join-Path $FFUDevelopmentPath 'FFU'
if (Test-Path -Path $ffuFolder) {
$cand = Get-ChildItem -Path $ffuFolder -Filter '*.ffu' -File | Sort-Object LastWriteTime -Descending
if ($cand.Count -gt 0) {
Write-Host ""
Write-Host "Additional FFU files available in $($ffuFolder):"
$i = 1
foreach ($c in $cand) {
Write-Host ("{0,3}. {1} [{2}]" -f $i, $c.Name, $c.LastWriteTime)
$i++
}
Write-Host ""
$resp = Read-Host "Select additional FFUs to copy (e.g. 1,3,5) or 'A' for all, or press Enter to skip"
if ($resp -match '^[Aa]$') {
$AdditionalFFUFiles = @($cand.FullName)
}
elseif ($resp -match '^\s*\d+(\s*,\s*\d+)*\s*$') {
$indices = $resp.Split(',') | ForEach-Object { [int]($_.Trim()) }
$sel = @()
foreach ($idx in $indices) {
if ($idx -ge 1 -and $idx -le $cand.Count) {
$sel += $cand[$idx - 1].FullName
}
}
$AdditionalFFUFiles = @($sel | Select-Object -Unique)
}
else {
# Skip if blank or invalid
if (-not [string]::IsNullOrWhiteSpace($resp)) {
WriteLog "Invalid additional FFU selection input. Skipping."
}
}
}
}
}
catch {
WriteLog "Early additional FFU selection prompt failed: $($_.Exception.Message)"
}
}
#Get drivers first since user could be prompted for additional info #Get drivers first since user could be prompted for additional info
Set-Progress -Percentage 3 -Message "Processing drivers..." Set-Progress -Percentage 3 -Message "Processing drivers..."
if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or $CopyDrivers)) { if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or $CopyDrivers)) {
@@ -4617,6 +4909,7 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
else { else {
WriteLog "Found $($driversToProcess.Count) driver entries to process from $driversJsonPath." WriteLog "Found $($driversToProcess.Count) driver entries to process from $driversJsonPath."
$preserveSourceOnCompress = ($UseDriversAsPEDrivers -and $CompressDownloadedDriversToWim)
$taskArguments = @{ $taskArguments = @{
DriversFolder = $DriversFolder DriversFolder = $DriversFolder
WindowsRelease = $WindowsRelease WindowsRelease = $WindowsRelease
@@ -4625,6 +4918,7 @@ if ($driversJsonPath -and (Test-Path $driversJsonPath) -and ($InstallDrivers -or
Headers = $Headers Headers = $Headers
UserAgent = $UserAgent UserAgent = $UserAgent
CompressToWim = $CompressDownloadedDriversToWim CompressToWim = $CompressDownloadedDriversToWim
PreserveSourceOnCompress = $preserveSourceOnCompress
} }
WriteLog "Starting parallel driver processing using Invoke-ParallelProcessing..." WriteLog "Starting parallel driver processing using Invoke-ParallelProcessing..."
@@ -4926,6 +5220,9 @@ if ($InstallApps) {
} }
} }
# Remove residual update artifacts for any updates disabled via flags
Remove-DisabledArtifacts
#Install Office #Install Office
if ($InstallOffice) { if ($InstallOffice) {
#Check if Office has already been downloaded, if so, skip download #Check if Office has already been downloaded, if so, skip download
@@ -4952,10 +5249,6 @@ if ($InstallApps) {
} }
# Remove residual update artifacts for any updates disabled via flags
Remove-DisabledUpdates
#Update Latest Defender Platform and Definitions - these can't be serviced into the VHDX, will be saved to AppsPath #Update Latest Defender Platform and Definitions - these can't be serviced into the VHDX, will be saved to AppsPath
if ($UpdateLatestDefender) { if ($UpdateLatestDefender) {
# Check if Defender has already been downloaded, if so, skip download # Check if Defender has already been downloaded, if so, skip download
@@ -5849,7 +6142,35 @@ If ($BuildUSBDrive) {
Set-Progress -Percentage 95 -Message "Building USB drive..." Set-Progress -Percentage 95 -Message "Building USB drive..."
try { try {
If (Test-Path -Path $DeployISO) { If (Test-Path -Path $DeployISO) {
New-DeploymentUSB -CopyFFU $ffuFilesToCopy = @()
# Always include the FFU that was just built (fallback to most recent .ffu in capture folder)
$currentFFU = $null
if ($null -ne $FFUFile -and -not [string]::IsNullOrWhiteSpace($FFUFile) -and (Test-Path -LiteralPath $FFUFile)) {
$currentFFU = $FFUFile
}
else {
try {
$ffuDir = if (-not [string]::IsNullOrWhiteSpace($FFUCaptureLocation)) { $FFUCaptureLocation } else { Join-Path $FFUDevelopmentPath 'FFU' }
if (Test-Path -LiteralPath $ffuDir) {
$latest = Get-ChildItem -Path $ffuDir -Filter '*.ffu' -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($null -ne $latest) { $currentFFU = $latest.FullName }
}
}
catch {
WriteLog "Failed to resolve latest FFU file to copy: $($_.Exception.Message)"
}
}
if ($null -ne $currentFFU) {
$ffuFilesToCopy += $currentFFU
}
if ($CopyAdditionalFFUFiles -and ($null -ne $AdditionalFFUFiles) -and ($AdditionalFFUFiles.Count -gt 0)) {
$ffuFilesToCopy += $AdditionalFFUFiles
}
$ffuFilesToCopy = $ffuFilesToCopy | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
New-DeploymentUSB -CopyFFU -FFUFilesToCopy $ffuFilesToCopy
} }
else { else {
WriteLog "$BuildUSBDrive set to true, however unable to find $DeployISO. USB drive not built." WriteLog "$BuildUSBDrive set to true, however unable to find $DeployISO. USB drive not built."
@@ -5874,59 +6195,10 @@ If ($RemoveFFU) {
} }
Set-Progress -Percentage 99 -Message "Finalizing and cleaning up..." Set-Progress -Percentage 99 -Message "Finalizing and cleaning up..."
If ($CleanupCaptureISO) { # Delegated post-build cleanup to common module
try { Invoke-FFUPostBuildCleanup -RootPath $FFUDevelopmentPath -AppsPath $AppsPath -DriversPath $Driversfolder -FFUCapturePath $FFUCaptureLocation -CaptureISOPath $CaptureISO -DeployISOPath $DeployISO -AppsISOPath $AppsISO -RemoveCaptureISO:$CleanupCaptureISO -RemoveDeployISO:$CleanupDeployISO -RemoveAppsISO:$CleanupAppsISO -RemoveDrivers:$CleanupDrivers -RemoveFFU:$RemoveFFU -RemoveApps:$RemoveApps -RemoveUpdates:$RemoveUpdates
If (Test-Path -Path $CaptureISO) {
WriteLog "Removing $CaptureISO" # Remove KBPath for cached vhdx files
Remove-Item -Path $CaptureISO -Force
WriteLog "Removal complete"
}
}
catch {
Writelog "Removing $CaptureISO failed with error $_"
throw $_
}
}
If ($CleanupDeployISO) {
try {
If (Test-Path -Path $DeployISO) {
WriteLog "Removing $DeployISO"
Remove-Item -Path $DeployISO -Force
WriteLog "Removal complete"
}
}
catch {
Writelog "Removing $DeployISO failed with error $_"
throw $_
}
}
If ($CleanupAppsISO) {
try {
If (Test-Path -Path $AppsISO) {
WriteLog "Removing $AppsISO"
Remove-Item -Path $AppsISO -Force
WriteLog "Removal complete"
}
}
catch {
Writelog "Removing $AppsISO failed with error $_"
throw $_
}
}
If ($CleanupDrivers) {
try {
#Remove files in $Driversfolder, but keep $DriversFolder
If (Test-Path -Path $Driversfolder) {
WriteLog "Removing files in $Driversfolder"
Remove-Item -Path $Driversfolder\* -Force -Recurse
WriteLog "Removal complete"
}
}
catch {
Writelog "Removing $Driversfolder\* failed with error $_"
throw $_
}
}
if ($AllowVHDXCaching) { if ($AllowVHDXCaching) {
try { try {
If (Test-Path -Path $KBPath) { If (Test-Path -Path $KBPath) {
+21 -1
View File
@@ -126,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)"
}
}) })
@@ -392,8 +400,20 @@ $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 $script:uiState.Data.lastConfigFilePath = $configFilePath
if ($config.InstallOffice -and $config.OfficeConfigXMLFile) { if ($config.InstallOffice -and $config.OfficeConfigXMLFile) {
+30 -5
View File
@@ -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."/>
@@ -628,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>
@@ -755,6 +756,29 @@
<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 --> <!-- Max USB Drives -->
<StackPanel Orientation="Horizontal" Margin="5"> <StackPanel Orientation="Horizontal" Margin="5">
@@ -816,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,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,6 +69,20 @@ 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
if ($PreserveSource) {
WriteLog "Preserving source driver folder for deferred WinPE driver harvesting: $SourceFolderPath"
try {
$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)"
}
}
else {
WriteLog "Removing source driver folder: $SourceFolderPath" WriteLog "Removing source driver folder: $SourceFolderPath"
try { try {
Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop Remove-Item -Path $SourceFolderPath -Recurse -Force -ErrorAction Stop
@@ -75,6 +92,7 @@ function Compress-DriverFolderToWim {
WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)" WriteLog "Warning: Failed to remove source folder '$SourceFolderPath'. Error: $($_.Exception.Message)"
# Do not fail the whole operation, just log a warning. # Do not fail the whole operation, just log a warning.
} }
}
return $true # Indicate success return $true # Indicate success
} }
@@ -209,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 `
@@ -217,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 `
@@ -226,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 `
@@ -235,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."
@@ -108,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
@@ -426,10 +428,14 @@ function Get-Apps {
if ($app.source -in @('winget', 'msstore')) { if ($app.source -in @('winget', 'msstore')) {
$hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine)) $hasCmd = ($app.PSObject.Properties['CommandLine'] -and -not [string]::IsNullOrWhiteSpace($app.CommandLine))
$hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments)) $hasArgs = ($app.PSObject.Properties['Arguments'] -and -not [string]::IsNullOrWhiteSpace($app.Arguments))
if ($hasCmd -or $hasArgs) { $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] = @{ $overrideMap[$app.name] = @{
CommandLine = if ($hasCmd) { $app.CommandLine } else { $null } CommandLine = if ($hasCmd) { $app.CommandLine } else { $null }
Arguments = if ($hasArgs) { $app.Arguments } else { $null } Arguments = if ($hasArgs) { $app.Arguments } else { $null }
AdditionalExitCodes = if ($hasAdd) { $app.AdditionalExitCodes } else { $null }
IgnoreNonZeroExitCodes = if ($hasIgnore) { [bool]$app.IgnoreNonZeroExitCodes } else { $null }
} }
} }
} }
@@ -453,6 +459,16 @@ function Get-Apps {
$entry.Arguments = $ov.Arguments $entry.Arguments = $ov.Arguments
$changed = $true $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) { if ($changed) {
+2 -1
View File
@@ -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 = '*'
@@ -396,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,8 +34,10 @@ 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 InjectUnattend = $State.Controls.chkInjectUnattend.IsChecked
@@ -115,6 +117,16 @@ function Get-UIConfig {
$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
} }
@@ -242,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())"
@@ -339,6 +371,7 @@ 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 'chkInjectUnattend' -PropertyName 'IsChecked' -ConfigObject $ConfigContent -ConfigKey 'InjectUnattend' -State $State
@@ -460,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
@@ -632,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."
} }
# 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." WriteLog "LoadConfig: Configuration loading process finished."
} }
function Invoke-SaveConfiguration { function Invoke-SaveConfiguration {
param( param(
@@ -655,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")
} }
} }
@@ -664,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,7 +622,7 @@ 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
@@ -629,6 +631,7 @@ function Invoke-DownloadSelectedDrivers {
Headers = $localHeaders Headers = $localHeaders
UserAgent = $localUserAgent UserAgent = $localUserAgent
CompressToWim = $compressDrivers CompressToWim = $compressDrivers
PreserveSourceOnCompress = $preserveSource
} }
$parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers ` $parallelResults = Invoke-ParallelProcessing -ItemsToProcess $selectedDrivers `
@@ -152,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)
@@ -216,11 +260,11 @@ function Register-EventHandlers {
} }
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
} }
} }
}) })
@@ -808,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)
@@ -950,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)
@@ -59,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')
@@ -143,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')
@@ -170,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
@@ -198,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'
} }
@@ -255,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
@@ -309,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
@@ -443,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,
@@ -610,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"
@@ -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 }
} }
}) })
} }
@@ -154,6 +156,8 @@ function Import-WingetList {
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
AdditionalExitCodes = if ($appInfo.PSObject.Properties['AdditionalExitCodes']) { $appInfo.AdditionalExitCodes } else { "" }
IgnoreNonZeroExitCodes = if ($appInfo.PSObject.Properties['IgnoreNonZeroExitCodes']) { [bool]$appInfo.IgnoreNonZeroExitCodes } else { $false }
DownloadStatus = "" DownloadStatus = ""
}) })
} }
@@ -197,6 +201,8 @@ function Search-WingetPackagesPublic {
Version = [string]$_.Version Version = [string]$_.Version
Source = [string]$_.Source Source = [string]$_.Source
Architecture = [string]$arch Architecture = [string]$arch
AdditionalExitCodes = [string]::Empty
IgnoreNonZeroExitCodes = [bool]$false
DownloadStatus = [string]::Empty DownloadStatus = [string]::Empty
} }
} -ThrottleLimit 20 } -ThrottleLimit 20
@@ -394,6 +400,7 @@ 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
@@ -415,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) {
@@ -449,7 +456,7 @@ function Start-WingetAppDownloadTask {
# 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency) # 2. Check existing downloaded Win32 content (folder-based; no WinGetWin32Apps.json dependency)
if (-not $appFound -and $source -eq 'winget') { if (-not $appFound -and $source -eq 'winget') {
$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) {
$contentFound = $false $contentFound = $false
if ($ApplicationItemData.Architecture -eq 'x86 x64') { if ($ApplicationItemData.Architecture -eq 'x86 x64') {
@@ -481,7 +488,7 @@ function Start-WingetAppDownloadTask {
# 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) {
@@ -529,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 {
@@ -723,6 +730,42 @@ function Invoke-WingetDownload {
# 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 `
+75 -1
View File
@@ -128,6 +128,7 @@ 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
@@ -141,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
@@ -175,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
} }
@@ -197,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(
@@ -292,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
@@ -310,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
+2 -1
View File
@@ -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'){
@@ -209,10 +209,23 @@ function ConvertTo-ComparableModelName {
param( param(
[string]$Text [string]$Text
) )
# Normalize model strings by converting any non-alphanumeric sequence to a single space, collapsing whitespace, and trimming. # 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 '' } 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 = ($Text -replace '[^A-Za-z0-9]+', ' ')
$normalized = ($normalized -replace '\s+', ' ').Trim() $normalized = ($normalized -replace '\s+', ' ').Trim()
if ($normalized -ne $original) {
WriteLog "Normalized model string: Original='$original' -> Normalized='$normalized'"
}
return $normalized return $normalized
} }
@@ -221,7 +234,7 @@ $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 = '2508.1Preview' $version = '2509.1Preview'
WriteLog 'Begin Logging' WriteLog 'Begin Logging'
WriteLog "Script version: $version" WriteLog "Script version: $version"
@@ -562,10 +575,11 @@ if (Test-Path -Path $driverMappingPath -PathType Leaf) {
# 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 model strings (ignore special characters and collapse whitespace) # Prepare normalized rule model string
$systemModelNorm = ConvertTo-ComparableModelName -Text $systemModel
$ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model $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 ($systemModelNorm -like "$($ruleModelNorm)*" -or $ruleModelNorm -like "$systemModelNorm*")) { if ($systemManufacturer -like "$($rule.Manufacturer)*" -and ($systemModelNorm -like "$($ruleModelNorm)*" -or $ruleModelNorm -like "$systemModelNorm*")) {
+1 -1
View File
@@ -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.