mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08c9214976 | |||
| c110dcd40e | |||
| eaa3e1e6af | |||
| 6562d16ce5 | |||
| 15a5b16b39 | |||
| d9c0c9c68e | |||
| d1ca123104 | |||
| f37647599a | |||
| cb14e84a26 | |||
| 8d7e4d1066 | |||
| c30ed923b6 | |||
| 50713188bf | |||
| e2ccd11f07 | |||
| f3316a017b | |||
| bdf1b63833 | |||
| 3ef26f2918 | |||
| 372360d739 | |||
| dc5877f398 | |||
| 49b2113fe1 | |||
| 556cfa1ee3 | |||
| 1ab4093d54 |
+120
@@ -1,5 +1,125 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
# 2509.1 UI Preview
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### [Refactor: Enhance artifact cleanup for disabled features](https://github.com/rbalsleyMSFT/FFU/commit/1ab4093d54b7d9bda9f47d7819694e66ae8de357)
|
||||||
|
|
||||||
|
Renames `Remove-DisabledUpdates` to `Remove-DisabledArtifacts` to better reflect its expanded scope.
|
||||||
|
|
||||||
|
This function now also removes Office installation scripts and downloaded content if the Office installation is disabled via the `$InstallOffice` flag.
|
||||||
|
|
||||||
|
The function call is moved to run before app installations to ensure artifacts are removed prior to the installation phase.
|
||||||
|
|
||||||
|
### [Removes the VM workaround for MCT ESD builds](https://github.com/rbalsleyMSFT/FFU/commit/dc5877f398316969299ee03800f3d07c7d98a9ab)
|
||||||
|
|
||||||
|
Comments out the logic that forces app installation when building from a downloaded ESD file. This workaround was implemented to prevent an OOBE reboot loop but is no longer required. This should speed up scenarios where you want to download the ESD media, install the latest CU and .NET CU, and capture the FFU.
|
||||||
|
|
||||||
|
### [Update default disk size to 50GB in FFU scripts and UI](https://github.com/rbalsleyMSFT/FFU/commit/372360d7392ad945be0db889a68e1fff0ed3b5d6)
|
||||||
|
|
||||||
|
Changed the default disk size parameter from 30GB to 50GB in BuildFFUVM.ps1 and FFUUI.Core.psm1 to accommodate larger virtual machines.
|
||||||
|
Updated tooltip and default value in the UI XAML file to reflect the new disk size.
|
||||||
|
|
||||||
|
### [Adds auto-loading of previous configuration on startup](https://github.com/rbalsleyMSFT/FFU/commit/3ef26f2918977906ebe14e328f015ce4f1941dc3)
|
||||||
|
|
||||||
|
Implements a new feature to automatically load the previously saved environment when the UI is launched.
|
||||||
|
|
||||||
|
This improves user experience by restoring the last saved configuration, including selected applications and drivers, eliminating the need to manually reload them on each run.
|
||||||
|
|
||||||
|
The process loads the main `FFUConfig.json` and then proceeds to load associated Winget, BYO App, and Driver lists if they are defined. UI elements and checkboxes are updated accordingly to reflect the loaded state.
|
||||||
|
|
||||||
|
### [Improves UI state after environment autoload](https://github.com/rbalsleyMSFT/FFU/commit/bdf1b63833c83171aed63e8fc16702078ccd577b)
|
||||||
|
|
||||||
|
Updates the visibility of UI panels for Winget and drivers when a previous environment is automatically loaded.
|
||||||
|
|
||||||
|
This ensures that if Winget apps or driver models are present, their corresponding UI sections are made visible. Additionally, it updates the "select all" checkbox state for Winget results and attempts to pre-select the hardware make for loaded drivers.
|
||||||
|
|
||||||
|
### [Add restore defaults and centralize cleanup logic](https://github.com/rbalsleyMSFT/FFU/commit/f3316a017b73bf12cf1a66e3d03a63e29c437cb1)
|
||||||
|
|
||||||
|
Introduces a "Restore Defaults" feature in the UI to reset the environment. This action removes generated configuration files, ISOs, downloaded apps, updates, drivers, and FFUs.
|
||||||
|
|
||||||
|
The post-build cleanup logic is refactored from the main build script into a new common function. This new function is used by both the standard build process and the new restore defaults feature, promoting code reuse and simplifying maintenance.
|
||||||
|
|
||||||
|
### [Add option to dynamically build PE drivers](https://github.com/rbalsleyMSFT/FFU/commit/e2ccd11f07217b389f1622a69794224412e046e1)
|
||||||
|
|
||||||
|
Thanks to @JonasKloseBW for the original code for this in https://github.com/rbalsleyMSFT/FFU/pull/115
|
||||||
|
|
||||||
|
Introduces a new parameter, `UseDriversAsPEDrivers`, that allows WinPE drivers to be sourced directly from the main driver repository.
|
||||||
|
|
||||||
|
When enabled, the script scans all available drivers, parses their INF files, and copies only the essential driver types (e.g., storage, mouse, keyboard, touchpad, system devices) needed for WinPE. This eliminates the need to maintain a separate, manually curated `PEDrivers` folder.
|
||||||
|
|
||||||
|
The UI is updated with a new checkbox that becomes visible when "Copy PE Drivers" is selected, making this a sub-option. Parameter validation is also adjusted to support this new workflow.
|
||||||
|
|
||||||
|
### [Improve model name normalization for driver mapping](https://github.com/rbalsleyMSFT/FFU/commit/50713188bffcb64f1b0c1f9eb89e02a300e3de98)
|
||||||
|
|
||||||
|
Enhances the model name normalization function to better handle variations in hardware model strings. This change introduces specific rules to canonicalize "All-in-One" and screen size variants (e.g., "-in" or "inch") for more reliable matching against driver mapping rules.
|
||||||
|
|
||||||
|
Additionally, optimizes performance by normalizing the system model once before the comparison loop. Logging is also added to show the original and normalized model strings for easier debugging.
|
||||||
|
|
||||||
|
### [Defer cleanup of compressed driver source folders](https://github.com/rbalsleyMSFT/FFU/commit/c30ed923b68b933f719b9a2941043b813bf4fd3f)
|
||||||
|
|
||||||
|
Implements a deferred cleanup mechanism for driver source folders when they are compressed to a WIM and also used for WinPE.
|
||||||
|
|
||||||
|
When drivers are compressed, the original source folders are now preserved if they are also needed for WinPE driver injection. A marker file is created in these preserved folders.
|
||||||
|
|
||||||
|
A new cleanup step is added after the WinPE media creation to remove these preserved folders, ensuring they are available when needed but not left behind permanently.
|
||||||
|
|
||||||
|
### [Refactor config loading and improve error handling](https://github.com/rbalsleyMSFT/FFU/commit/8d7e4d106620761d0ae1a5133f6d6ba301131471)
|
||||||
|
|
||||||
|
Extracts the logic for importing supplemental assets (Winget, BYO, Drivers) into a new reusable function. This function is now called by both the manual and automatic configuration loaders, reducing code duplication.
|
||||||
|
|
||||||
|
Enhances the manual configuration loading process with more robust error handling. It now provides specific user-facing error messages for file read failures, empty files, and invalid JSON, improving the user experience when loading a malformed configuration.
|
||||||
|
|
||||||
|
When loading a configuration, if optional supplemental files like AppList.json are referenced but not found, an informational message is now displayed to the user instead of failing silently.
|
||||||
|
|
||||||
|
### [Add robust sanitization for names used in paths](https://github.com/rbalsleyMSFT/FFU/commit/cb14e84a26acaf5863aa3bb094dbf18424798875)
|
||||||
|
|
||||||
|
Introduces a new common function, `ConvertTo-SafeName`, to sanitize strings by removing characters that are invalid in Windows file paths.
|
||||||
|
|
||||||
|
This function is now used consistently when creating directory and file names for drivers (Dell, HP, Lenovo, Microsoft) and applications to prevent path-related errors. It replaces several ad-hoc sanitization methods with a single, more robust implementation.
|
||||||
|
|
||||||
|
### [Includes exit code fields when using Copy Apps button](https://github.com/rbalsleyMSFT/FFU/commit/f37647599a318da29b62154bebff8c8a857d3002)
|
||||||
|
|
||||||
|
Adds persistence of AdditionalExitCodes and IgnoreNonZeroExitCodes when exporting the UI list to prevent losing custom exit handling settings and maintain parity with the primary save routine.
|
||||||
|
|
||||||
|
### [Sanitizes app names for storage and paths](https://github.com/rbalsleyMSFT/FFU/commit/d1ca1231045e38316733495e1fdb8590a225be67)
|
||||||
|
|
||||||
|
Applies name sanitization when persisting the app list and when building/checking Win32 and Store download directories.
|
||||||
|
Prevents invalid characters in folder names, aligns persisted names with on-disk structure, and improves detection of existing content to avoid redundant downloads and errors.
|
||||||
|
|
||||||
|
### [Adds exit-code overrides and UI for winget apps](https://github.com/rbalsleyMSFT/FFU/commit/d9c0c9c68ee1769230c9789b5c7cb84bcff4d642)
|
||||||
|
|
||||||
|
Adds per-app control for additional accepted exit codes and ignoring non‑zero exit codes to improve handling of installers with nonstandard returns.
|
||||||
|
|
||||||
|
Exposes editable fields in the app list UI, persists them across search defaults, import/export, and pre-download save, and applies overrides during app resolution to honor configured behavior.
|
||||||
|
|
||||||
|
### [Adds UI/CLI to copy additional FFUs to USB build](https://github.com/rbalsleyMSFT/FFU/commit/15a5b16b39887b71ae545c638d57183c97bdf629)
|
||||||
|
|
||||||
|
- Enables selecting multiple existing FFU images to include on the deployment USB for easier distribution and testing.
|
||||||
|
- Adds a UI option with selectable, sortable list from the capture folder, refresh support, and persisted selections.
|
||||||
|
- Validates that selections exist when the option is enabled to prevent empty runs.
|
||||||
|
- Supports unattended/CLI flows by prompting early or accepting a preselected list for USB creation; deduplicates and logs chosen files.
|
||||||
|
- Always includes the just-built (or latest available) FFU as a base.
|
||||||
|
- Improves no-FFU handling and streamlines multi-FFU selection workflow.
|
||||||
|
|
||||||
|
### [Standardizes JSON output: depth, UTF-8, key order](https://github.com/rbalsleyMSFT/FFU/commit/6562d16ce500197b428b51915332c6649df302df)
|
||||||
|
|
||||||
|
- Sorts top-level config keys before serialization for deterministic files and cleaner diffs.
|
||||||
|
- Increases JSON depth to 10 to retain nested settings.
|
||||||
|
- Writes JSON as UTF-8 via Set-Content for consistent encoding.
|
||||||
|
- Applies across config export and UI save flows.
|
||||||
|
|
||||||
|
### [Adds Windows 11 25H2 mapping](https://github.com/rbalsleyMSFT/FFU/commit/eaa3e1e6af5c25e0f8b185f8107e017782b0f00f)
|
||||||
|
|
||||||
|
Extends supported Windows 11 releases to include 25H2. Default is still 24H2.
|
||||||
|
|
||||||
|
* Update USBImagingToolCreator.ps1 by @jrollmann in https://github.com/rbalsleyMSFT/FFU/pull/262
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
|
||||||
|
* @jrollmann made their first contribution in https://github.com/rbalsleyMSFT/FFU/pull/262
|
||||||
|
|
||||||
# 2507.1 UI Preview
|
# 2507.1 UI Preview
|
||||||
|
|
||||||
Waaay too many to list. Just watch the Youtube video in the Readme :)
|
Waaay too many to list. Just watch the Youtube video in the Readme :)
|
||||||
|
|||||||
+354
-82
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 `
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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*")) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user