2017-11-13

Add/remove features and patches/language packs to offline Windows 10 WIM from ISO file with Powershell

I could not find a script that mounts an official MS Windows 10 ISO, adds .NET 3.5 and patches automatically. So.. I made one myself.

Please share any improvements and run at your own risk! 

#=== SCRIPT BEGINS ===
<#
.SYNOPSIS
Mount an ISO, enable and or disable features and patches to WIM and save.
.DESCRIPTION
Mount ISO, copy install.wim, mount the file and enable/disable Windows features and patches, and save the changes.
.NOTES
David Djerf, 2017-10-11 - First version that only adds .NET 3.5
David Djerf, 2017-10-13 - Added automatic mounted drive letter detection, made it possible to add and remove custom Windows features, and patches.
David Djerf, 2017-10-16 - Added support for patchfolders
David Djerf, 2017-12-01 - Added export index to new file
David Djerf, 2017-12-08 - Fixed Verbose message for patches not writing name of patches
    David Djerf, 2018-04-03 - Added detection for WIM or ESD install-file.

.PARAMETER ISOfile
Full path to Windows 10 ISO file
.PARAMETER WIMIndex
Index number to modify, eg. 1
You can list the index using the DISM command: dism /get-wiminfo /wimfile:<PATH_TO_WIM>
.PARAMETER NewFileName
Your_new_file.wim
Will default to install_modified.wim if not specified.
.PARAMETER WorkFolder
Folder where the magic happens...
If not specified C:\WorkFolder will be used
.PARAMETER Patches
Full path to windows patches in cab or msu format, multiple files accepted if comma separated.
eg. "c:\patches\KB12345.msu","c:\patches\KB654321.msu"
.PARAMETER Patchfolder
Path to a folder containing windows patches in cab or msu format. All patches will be applied.
eg. c:\patches
.PARAMETER WIMAddFeatures
Add Windows features based on their proper "Featurenames"
.PARAMETER WIMRemoveFeatures
Remove Windows features based on their proper "Featurenames" (get features with Dism /Image:C:\workfolder /Get-Features)
.PARAMETER ExportIndexFile
Will export the define index to a new file, provide the full file path.
.PARAMETER Verbose
Add -Verbose to get information about what step the script is running.
.EXAMPLE
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patches "C:\Patches\KB123456.msu","C:\Patches\KB654321.msu" -Verbose
modify-windows_wim.ps1 -ISOfile C:\Users\admin\Downloads\SW_DVD5_Win_Pro_Ent_Edu_N_10_1709_64BIT_English_MLF_X21-50143.ISO -WIMIndex 1 -Workfolder C:\mountfolder -NewFileName my_new_install.wim -WIMAddFeatures "NetFx3" -WIMRemoveFeatures "SMB1Protocol" -Patchfolder "C:\Patches" -Verbose

#>
#Requires -Version 4.0
#Requires -RunAsAdministrator

[CmdletBinding(SupportsShouldProcess=$True)]
Param(
        [Parameter(Mandatory=$True,Position=0)]$ISOfile,
        [Parameter(Mandatory=$True,Position=1)]$WIMIndex,
        [Parameter(Mandatory=$False,Position=3)]$NewFileName,
        [Parameter(Mandatory=$False,Position=4)]$Workfolder,
        [string[]]$WIMAddFeatures,
        [string[]]$WIMRemoveFeatures,
        [string[]]$Patches,
        [string]$Patchfolder,
[string]$ExportIndexFile
    )

Begin{
IF (!$WorkFolder) { 
$WorkFolder = "C:\WorkFolder"
} # End of IF
IF (!$NewFileName) { 
$NewFileName = "install_modified.wim"
} # End of IF

# Make sure patchfolder is readable
IF ($Patchfolder) {
IF (Test-Path -path "$Patchfolder") {
Write-Verbose "'$Patchfolder' could be read"
} else { Write-Verbose "'$Patchfolder' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF
}

# Mount ISO if exist
IF (Test-Path -path "$ISOfile") {
Write-Verbose "'$ISOfile' could be read, Mounting ISO to $MountDrive"
$MountDrive=(Mount-Diskimage $ISOfile -PassThru | Get-Volume).DriveLetter+":"
} else { Write-Verbose "'$ISOfile' could not be read, aborting!"
Write-Verbose "Make sure you the path is correct and can be read."
Break
} # End of IF

# Failsafe, make sure wim or esd exists
IF (Test-Path -path "$MountDrive\Sources\install.wim") {
Write-Verbose "'$MountDrive\Sources\install.wim' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.wim"
} elseif (Test-Path -path "$MountDrive\Sources\install.esd") {
Write-Verbose "'$MountDrive\Sources\install.esd' exists, engage!"
        $InstallFile="$MountDrive\Sources\install.esd"
} else { 
Write-Verbose "'$MountDrive\Sources\install.wim' or '$MountDrive\Sources\install.esd' does not exist, aborting!"
Write-Verbose "Make sure you specified the correct drive letter for the new virtual drive."
Dismount-Diskimage $ISOfile
Break
} # End of IF

    # Copy install-file to newfile in temp folder (to make sure the file is not on a network share that may be disconnected)
IF (Test-Path -path "$ENV:TEMP\$NewFileName") {
Write-Verbose "$NewFileName exists, make it writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} else {
Write-Verbose "Copy $InstallFile to %TEMP%\$NewFileName"
Copy-Item $InstallFile -Destination "$ENV:TEMP\$NewFileName" -force
Write-Verbose "Make $NewFileName writable"
Set-ItemProperty "$ENV:TEMP\$NewFileName" -name IsReadOnly -value $false
} # End of IF

# Make sure $Workfolder exists and is empty
IF (Test-Path -path "$WorkFolder") {
Write-Verbose "$WorkFolder exists, discarding content"
Dismount-WindowsImage -Path $WorkFolder -Discard
Write-Verbose "$WorkFolder exists, removing folder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} else {
Write-Verbose "Create $WorkFolder"
New-Item -ItemType directory -Path $WorkFolder -force | Out-Null
} # End of IF
Write-Verbose "Mount $NewFileName to $WorkFolder"
Mount-WindowsImage -ImagePath "$ENV:TEMP\$NewFileName" -Index $WIMIndex -Path $WorkFolder -Optimize
} # End of BEGIN

Process{
# Add Features if specified
IF ($WIMAddFeatures) { 
ForEach ($WIMAddFeature in $WIMAddFeatures) {
Write-Verbose "Adding $WIMAddFeature to $NewFileName"
dism /image:$WorkFolder /enable-feature /featurename:$WIMAddFeature /all /source:$MountDrive\sources\sxs
} # End of ForEach
} else {
Write-Verbose "No feature to add..."
}# End of IF

# Remove Features if specified
IF ($WIMRemoveFeatures) { 
ForEach ($WIMRemoveFeature in $WIMRemoveFeatures) {
Write-Verbose "Removing $WIMRemoveFeature to $NewFileName"
dism /image:$WorkFolder /disable-feature /featurename:$WIMRemoveFeature
} # End of ForEach
} else {
Write-Verbose "No feature to remove..."
}# End of IF

# Apply Windows patches if specified
IF ($Patches) {
$Patch=""
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF

IF ($Patchfolder) {
$Patch=""
$Patches=Get-ChildItem $Patchfolder -Filter *.msu | % { $_.FullName }
ForEach ($Patch in $Patches) {
Write-Verbose "Applying $Patch to $NewFileName"
dism /Image:$WorkFolder /add-package /packagepath:$Patch
} # End of ForEach
} else {
Write-Verbose "No patch to apply..."
}# End of IF
} #End of Process

End{
Write-Verbose "Dismount ISO"
Dismount-Diskimage $ISOfile
Write-Verbose "Saving changes to $ENV:TEMP\$NewFileName"
Dismount-WindowsImage -Path $WorkFolder -Save -CheckIntegrity
IF ($ExportIndexFile) {
Write-Verbose "Exporting Index $WIMIndex to $ExportIndexFile"
Export-WindowsImage -SourceImagePath "$ENV:TEMP\$NewFileName" -SourceIndex $WIMIndex -DestinationImagePath $ExportIndexFile
} # End of if
Write-Verbose "Removing $WorkFolder"
Remove-Item -Recurse $WorkFolder -force
Write-Verbose "Finished saving $ENV:TEMP\$NewFileName"
Write-Verbose "You can now test your new file!"
}# End of End
#=== SCRIPT ENDS ===