Q: How can I add updates to an offline VHD or WIM file downloaded from the Microsoft Catalog?

A: Various solutions help in the automated updating of a virtual hard disk (VHD) file from Windows Update Services or Configuration Manager. However, you can also manually inject updates into a VHD using the DISM utility, and it's actually pretty simple.

After you download all the updates to a folder, you will have one folder with lots of subfolders (one for each downloaded update from the catalog). The Windows PowerShell script below mounts your VHD and assumes it has been mounted as drive letter I, then searches for all updates in subfolders of the current folder and applies them all to the VHD file, performs a cleanup of the image (if a Service Pack was installed there are many files that have been replaced) and then dismounts.

I save this as a script but actually run it manually by selecting the various blocks of the code within the Integrated Scripting Editor, so I can check each stage of the progress.

I use this process to keep my master Windows Server 2012 VHD patched with the latest update each month, so when it's deployed as part of a new OS deployment, it's already fully patched.

  1. #block 1
  2. mount-vhd -path G:\Temp\Win2012DCRTM.vhdx

  1. #block 2
  2. $updates = get-childitem -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname foreach($update in $updates) { write-debug $update.fullname $command = "dism /image:i:\ /add-package /packagepath:'" + $update.fullname + "'" write-debug $command Invoke-Expression $command }

  1. #block 3
  2. $command = "dism /image:i:\ /Cleanup-Image /spsuperseded" Invoke-Expression $command

  1. #block 4
  2. dismount-vhd -path G:\Temp\Win2012DCRTM.vhdx -confirm:$false

Below is an example execution in action:

  1. PS D:\software\Windows 2012 Updates> mount-vhd -path G:\Temp\Win2012DCRTM.vhdx
  2. PS D:\software\Windows 2012 Updates> $updates = get-childitem -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname
  3. foreach($update in $updates)
  4. {
  5. write-debug $update.fullname
  6. $command = "dism /image:i:\ /add-package /packagepath:'" + $update.fullname + "'"
  7. write-debug $command
  8. Invoke-Expression $command
  9. }


Here is the output from the example:

  1. Deployment Image Servicing and Management tool
  2. Version: 6.2.9200.16384
  3. Image Version: 6.2.9200.16384
  4. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Cumulative Security Update for Internet Explorer 10 for
  5. Windows Server 2012 (KB2761465)\AMD64-all-windows8-rt-kb2761465-x64_71efb0756cf746951571a72c83ddb55775362418.msu
  6. The operation completed successfully.
  7. Deployment Image Servicing and Management tool
  8. Version: 6.2.9200.16384
  9. Image Version: 6.2.9200.16384
  10. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Microsoft .NET Framework 4.5 on Wind
  11. ows 8 and Windows Server 2012 for x64-based Systems (KB2737084)\AMD64_X86_ARM-all-windows8-rt-kb2737084-x64_1a1b73c30d7bd20b
  12. 61fc522890a8fd61370ae1bb.msu
  13. The operation completed successfully.
  14. ....


Here is the next command, with output:

  1. PS D:\software\Windows 2012 Updates> $command = "dism /image:i:\ /Cleanup-Image /spsuperseded"
  2. Invoke-Expression $command
  3. Deployment Image Servicing and Management tool
  4. Version: 6.2.9200.16384
  5. Image Version: 6.2.9200.16384
  6. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  7. The operation completed successfully.


And the next command:

  1. PS D:\software\Windows 2012 Updates> dismount-vhd -path G:\Temp\Win2012DCRTM.vhdx -confirm:$false

Notice there was no Service Pack applied, so the step to clean up the image didn't perform any action.

The same set of commands can be used to patch a master WIM file that is used for new deployments or for deployed OSs to get updated or have corrupt features removed.

I got carried away creating this and created a full PowerShell function that basically does everything for you for a WIM or VHD building on those manually executed commands I showed above. Just pass the file to update and the patch folder location.

Note the script assumes it can use c:\wimmount as its mount and will create this folder if it does not exist. It also assumes if using a VHD that no other VHD file is attached on the system.

Click the link to my website here to download the psm1 file and save to the \Documents\WindowsPowerShell\Modules\Install-Patch folder as Install-Patch.psm1, which will allow the module to be available automatically and usable. Below is the actual code from the PSM1 file.

  1. function Install-Patch
  2. {
  3. <#
  4. .SYNOPSIS
  5. Patches a WIM or VHD file
  6. .DESCRIPTION
  7. Applies downloaded patches to a VHD or WIM file
  8. .NOTES
  9. File Name: Install-Patch.psm1
  10. Author: John Savill
  11. Requires: Tests on PowerShell 3 on Windows Server 2012
  12. Copyright (c) 2013 John Savill
  13. .LINK
  14. http://www.savilltech.com/
  15. .PARAMETER updateTargetPassed
  16. File (WIM, VHD or VHDX) to be patched
  17. .PARAMETER patchpath
  18. Path containing the updates
  19. .EXAMPLE
  20. Install-Patch d:\files\test.vhd d:\updates\win2012\
  21. Install-Patch d:\files\install.wim:4 d:\updates\win2012\
  22. #>
  23. [cmdletbinding()]
  24. Param(
  25. [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$updateTargetPassed,
  26. [Parameter(ValuefromPipeline=$false,Mandatory=$true)][string]$patchpath)
  27. #$updateTargetPassed = "G:\Temp\Win2012DatacenterRTM.vhdx"
  28. #or
  29. #$updateTargetPassed = "d:\sources\install.wim:4"
  30. #$patchpath = "D:\software\Windows 2012 Updates\"
  31. if(($updateTargetPassed.ToLower().Contains(".vhd")) -eq $true) # if its VHD or VHDX. Contains is case sensitive so have to convert to lower when comparing
  32. {
  33. $isVHD = $true
  34. }
  35. else
  36. {
  37. $isVHD = $false
  38. }
  39. if($isVHD)
  40. {
  41. $updateTarget=$updateTargetPassed
  42. if ((Test-Path $updateTarget) -eq $false) #if not found
  43. {
  44. write-output "Source not found ($updateTarget)"
  45. break
  46. }
  47. else
  48. {
  49. mount-vhd -path $updateTarget
  50. $disks = Get-CimInstance -ClassName Win32_DiskDrive | where Caption -eq "Microsoft Virtual Disk"
  51. foreach ($disk in $disks)
  52. {
  53. $vols = Get-CimAssociatedInstance -CimInstance $disk -ResultClassName Win32_DiskPartition
  54. foreach ($vol in $vols)
  55. {
  56. $updatedrive = Get-CimAssociatedInstance -CimInstance $vol -ResultClassName Win32_LogicalDisk |
  57. where VolumeName -ne 'System Reserved'
  58. }
  59. }
  60. $updatepath = $updatedrive.DeviceID + "\"
  61. }
  62. }
  63. if(!$isVHD) #its a WIM file
  64. {
  65. #Need to extract the WIM part and the index
  66. #extract file name and the index number
  67. $updateTargetPassedSplit = $updateTargetPassed.Split(":")
  68. if($updateTargetPassedSplit.Count -eq 3) #one for drive letter, one for folder and one for image number so would have been two colons in it c:\temp\install.wim:4
  69. {
  70. $updateTarget = $updateTargetPassedSplit[0] + ":" + $updateTargetPassedSplit[1] #There are two colons. The first is drive letter then the folder!
  71. $updateTargetIndex = $updateTargetPassedSplit[2]
  72. $updatepath = "c:\wimmount\"
  73. #check if exists and if not create it
  74. if ((Test-Path $updatepath) -eq $false) #if not found
  75. {
  76. Write-Host "Creating folder " + $updatepath
  77. New-Item -Path $updatepath -ItemType directory
  78. #could have also used [system.io.directory]::CreateDirectory($updatepath)
  79. }
  80. # Mount it as folder
  81. #dism /get-wiminfo /wimfile:install.wim
  82. dism /Mount-Wim /wimfile:$updateTarget /index:$updateTargetIndex /mountdir:$updatepath
  83. }
  84. else
  85. {
  86. write-output "Missing index number for WIM file. Example: c:\temp\install.wim:4"
  87. break
  88. }
  89. }
  90. # For WIM or VHD
  91. $updates = get-childitem -path $patchpath -Recurse | where {($_.extension -eq ".msu") -or ($_.extension -eq ".cab")} | select fullname
  92. foreach($update in $updates)
  93. {
  94. write-debug $update.fullname
  95. $command = "dism /image:" + $updatepath + " /add-package /packagepath:'" + $update.fullname + "'"
  96. write-debug $command
  97. Invoke-Expression $command
  98. }
  99. $command = "dism /image:" + $updatepath + " /Cleanup-Image /spsuperseded"
  100. Invoke-Expression $command
  101. if($isVHD)
  102. {
  103. dismount-vhd -path $updateTarget -confirm:$false
  104. }
  105. else
  106. {
  107. dism /Unmount-Wim /mountdir:$updatepath /commit
  108. #dism /Unmount-Wim /mountdir:$updatepath /discard
  109. }
  110. }

Below are some example usages of the module in action for both a VHDX and a WIM file.

  1. PS C:\> Install-Patch G:\temp\Win2012DatacenterRTM.vhdx 'D:\software\Windows 2012 Updates'
  2. Deployment Image Servicing and Management tool
  3. Version: 6.2.9200.16384
  4. Image Version: 6.2.9200.16384
  5. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
  6. 4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
  7. The operation completed successfully.
  8. Deployment Image Servicing and Management tool
  9. Version: 6.2.9200.16384
  10. Image Version: 6.2.9200.16384
  11. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
  12. 4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
  13. The operation completed successfully.
  14. Deployment Image Servicing and Management tool
  15. Version: 6.2.9200.16384
  16. Image Version: 6.2.9200.16384
  17. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
  18. 4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
  19. The operation completed successfully.
  20. Deployment Image Servicing and Management tool
  21. Version: 6.2.9200.16384
  22. Image Version: 6.2.9200.16384
  23. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  24. The operation completed successfully.
  25. PS C:\> Install-Patch d:\sources\install.wim:4 'D:\software\Windows 2012 Updates'
  26. Deployment Image Servicing and Management tool
  27. Version: 6.2.9200.16384
  28. Mounting image
  29. The operation completed successfully.
  30. Deployment Image Servicing and Management tool
  31. Version: 6.2.9200.16384
  32. Image Version: 6.2.9200.16384
  33. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2753842)\AMD6
  34. 4-all-windows8-rt-kb2753842-v2-x64_1287425c7410d86b10874e8d666dbb32deb45e42.msu
  35. The operation completed successfully.
  36. Deployment Image Servicing and Management tool
  37. Version: 6.2.9200.16384
  38. Image Version: 6.2.9200.16384
  39. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2765809)\AMD6
  40. 4-all-windows8-rt-kb2765809-x64_2b2b24ddf3b884815275a9103b84a5e38ba4ad2b.msu
  41. The operation completed successfully.
  42. Deployment Image Servicing and Management tool
  43. Version: 6.2.9200.16384
  44. Image Version: 6.2.9200.16384
  45. Processing 1 of 1 - Adding package D:\software\Windows 2012 Updates\Security Update for Windows Server 2012 (KB2770660)\AMD6
  46. 4-all-windows8-rt-kb2770660-x64_6a0d84e5053592949f2ca469a056568d45b5ec9c.msu
  47. The operation completed successfully.
  48. Deployment Image Servicing and Management tool
  49. Version: 6.2.9200.16384
  50. Image Version: 6.2.9200.16384
  51. Service Pack Cleanup cannot proceed: No Service Pack backup files were found.
  52. The operation completed successfully.
  53. Deployment Image Servicing and Management tool
  54. Version: 6.2.9200.16384
  55. Image File : d:\sources\install.wim
  56. Image Index : 4
  57. Saving image
  58. Unmounting image
  59. The operation completed successfully.

I created a small video that walks through the script and its usage to help, which is at my website or you can find it on YouTube.