ConfigMgr: WIM Your Applications Like a Boss
12-12-2020 2:25 PM

BEFORE YOU BEGIN

Disclaimer: All information and content in this blog posts is provided without any warranty whatsoever. The entire risk of using this information or executing the provided content remains with you. Under no circumstances should Dell, Microsoft, its author, or anyone else involved in the creation of these blog posts be held liable for any damage or data loss.
Disclaimer: Do NOT create a WIM from files located on a dedup volume! Really bad things WILL happen! The ConfigMgr overlords (Mike & Gary) will punish those of us who did it anyway! - You have been warned!
Knowledge: I assume that people who read this blog post has an general understanding of application creation in Microsoft Endpoint Configuration Manager, so I won't be deep-diving into details about that in this post.

WIM YOUR APPLICATIONS

Introduction

For several years I've built huge applications of 10-20 GB in size (Hashtag Autodesk) with a long deployment time to follow for the end user. For all those years I been wanting to come up with a solution to reduce the size of the applications and deployment time.
LetΒ΄s WIM Your Applications Like a Boss!
Then earlier this year I was inspired by Martin Bengtsson and his blog post about capturing drivers into a WIM format and *BINGO* then came the idea to build a PowerShell script to handle WIM applications in our Microsoft Endpoint Configuration Manager environment with logic and log function.
After it has gone into production, I can now share it with you guys
πŸ€“
​

WHAT IS WIM?

Windows Imaging (WIM) format is a file-based disk image format. It was developed by Microsoft to help deploy Windows Vista and subsequent versions of the Windows operating systems.
With the Deployment Image Servicing and Management (DISM) tool, it is possible to create a data image for applications, files, and other resources.
Read more about creating a data image here​

WIM Creation

Okay, let's get started! As mentioned above, you need the Deployment Image Servicing and Management (DISM) tool in order to capture an application as WIM format and you must use this tool from either CMD or PowerShell.
Note: There is currently an ongoing discussion for and against using MAX compression when using deduplication in your environment. I've tested both FAST and MAX compression and I have not experienced any problems in our environment and we are using deduplication along with BranchCache.
But I tend to listen to clever people, so my recommendation at this point is to use "/Compress:fast" if you are using deduplication, just to be safe
πŸ€“
​
7-ZIP: Wait a second, why WIM and not 7-Zip you might ask? Well, with WIM you only need to mount and unmount the image, which leaves a much smaller footprint than 7-Zip because you don't have to extract the content and use extra storage space!
Hashing: Microsoft Endpoint Configuration Manager uses a hashing function to determine if the files are different. Every file are given a hash value, which means that there may be hundreds of files per application to hash check during distributing and downloading content.
By using WIM captured applications, only two files need to be hash checked.
Read more about hashing here​
Now, letΒ΄s try and WIM the latest Java 8 Update 271 application.
Step 1. Create a temp folder containing your source application e.g. C:\Temp\Java
Step 2. From an elevated CMD or PowerShell prompt, run the below DISM command.
1
# DISM command for capturing a data image
2
Dism.exe /Capture-Image /ImageFile:C:\Temp\Application.wim /CaptureDir:"C:\Temp\Java" /Name:"Oracle" /Description:"Java 8 Update 271" /Compress:fast
Copied!
Creating WIM with the Deployment Image Servicing and Management (DISM) Tool
This will create an application.wim file and if we compare the source folder to the application.wim, we can see that it didn't reduce the size due to the already small size of the Java application.
Comparison of the Source Folder and WIM Application
But what happens if we make the same comparison with an Autodesk application?
Creating WIM with the Deployment Image Servicing and Management (DISM) Tool
Comparison of the Source Folder and WIM Application
LetΒ΄s compare the Autodesk Revit 2021 source folder with the WIM file. Whoa..! We went from 12.039 files and 1610 folders to 1 single WIM file and reduced the size by 2,5 GB! Damn! That's awesome
😎
This was the easy part! We can now focus on my PowerShell script, which will be used to deploy the application as WIM format.

The Deployment Script

Download the deployment script from my GitHub repo.
The script supports the following deployment modes (Install, Uninstall, Repair) and you only need to change a few lines for each of these deployment modes to fit your needs - See more in the script details.
Installing Java 8 Update 271
Uninstalling Java 8 Update 271
Repairing Java 8 Update 271
The script does also support Get-Help and Verbose.
Get-Help -Examples
Get-Help -Detailed
-Verbose
Script (Invoke-AppDeploy.ps1)
1
<#
2
.SYNOPSIS
3
Mount WIM, Install, Uninstall or Repair Application and Unmount WIM.
4
​
5
.DESCRIPTION
6
The purpose of this script is to mount a WIM containing an application that will
7
be installed, uninstalled or repaired and then unmount the WIM again.
8
​
9
So before using this script, you must capture the application into a WIM file and
10
this can be done with the DISM example further down in this description.
11
​
12
Capturing applications as WIM and the use of this script reduces our application
13
deployment time through Configuration Manager by 30-60% depending on the hardware
14
configuration and the application size.
15
​
16
We are old school and we use CMD scripts for application deployment in our environment.
17
But it's pretty easy to modify the deployment mode variables found in the "Begin" script
18
block to support other formats like .msi, .exe or .ps1 file.
19
​
20
​
21
DISM Example.
22
Dism.exe /Capture-Image /ImageFile:C:\Temp\Application.wim /CaptureDir:"C:\Temp\Java" /Name:"Oracle" /Description:"Java" /Compress:fast
23
​
24
NOTE. I'll recommend using "/Compress:fast" if you are using deduplication. Some wise people state that
25
"/Compress:max" is no good if you are using deduplication. And I tend to listen to wise people ;)
26
​
27
​
28
CMD Example (Default).
29
​
30
$FilePath = Join-Path -Path "$MountDir" -ChildPath "Install.cmd"
31
$Process = "cmd.exe"
32
$Arguments = @(
33
"/c",
34
"""$FilePath""",
35
"> nul",
36
"&& exit"
37
)
38
​
39
​
40
MSI Example.
41
​
42
Change this -> $FilePath = Join-Path -Path "$MountDir" -ChildPath "Setup.msi"
43
Change this -> $Process = "msiexec.exe"
44
Change this -> $Arguments = @(
45
"/I",
46
"""$FilePath""",
47
"/qn",
48
"REBOOT=ReallySuppress"
49
)
50
​
51
​
52
EXE Example.
53
​
54
Change this -> $FilePath = Join-Path -Path "$MountDir" -ChildPath "Setup.exe"
55
$Process = "cmd.exe"
56
Change this -> $Arguments = @(
57
"/c",
58
"""$FilePath""",
59
"/s",
60
"&& Exit"
61
)
62
​
63
EXE Example (Microsoft Office Professional Plus 2019).
64
​
65
$FilePath = Join-Path -Path "$MountDir" -ChildPath "Setup.exe"
66
$Process = "cmd.exe"
67
$Arguments = @(
68
"/c",
69
"""$FilePath""",
70
"/Configure",
71
"$MountDir/$XML",
72
"&& Exit"
73
)
74
​
75
​
76
PS1 Example.
77
​
78
Change this -> $FilePath = Join-Path -Path "$MountDir" -ChildPath "Install.ps1"
79
Change this -> $Process = "powershell.exe"
80
Change this -> $Arguments = @(
81
"-NoProfile",
82
"-File",
83
"""$FilePath""",
84
"-Param_1 ""ParamValue""",
85
"-Param_2 ""ParamValue"""
86
)
87
​
88
​
89
.PARAMETER MountDir
90
Changes the default location from ".\Mount" to the location specified.
91
​
92
.PARAMETER SourceWIM
93
Changes the default location and filename from ".\Application.wim" to the location and filename specified.
94
​
95
.PARAMETER LogDir
96
Changes the default location from "$env:SystemRoot\Temp" (C:\WINDOWS\Temp) to the location specified.
97
​
98
.PARAMETER XML
99
Specify a XML configuration file for the application to be deployed, e.g. Office-Configuration.xml
100
​
101
.PARAMETER AppName
102
Specify a name of the application to be deployed, e.g. Microsoft Office Professional Plus 2019.
103
​
104
.PARAMETER DeploymentMode
105
Specify whether to Install, Uninstall or Repair the application, e.g. .\Invoke-AppDeploy.ps1 -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019"
106
​
107
.EXAMPLE
108
.
109
# Mount WIM to the default location, install the application, unmount WIM and cleanup the mount directory.
110
.\Invoke-AppDeploy.ps1 -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019"
111
​
112
# Mount WIM to the default location, repair the application, unmount WIM and cleanup the mount directory.
113
.\Invoke-AppDeploy.ps1 -DeploymentMode "Repair" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019"
114
​
115
# Mount WIM to the default location, uninstall the application, unmount WIM and cleanup the mount directory.
116
.\Invoke-AppDeploy.ps1 -DeploymentMode "Uninstall" -XML "Office-Uninstall.xml" -AppName "Microsoft Office Professional Plus 2019"
117
​
118
# Mount WIM to the default location, install the application, unmount WIM and cleanup the mount directory, with -Verbose added for troubleshooting purposes.
119
.\Invoke-AppDeploy.ps1 -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019" -Verbose
120
​
121
# Mount WIM to the default location, use a custom log location, install the application, unmount WIM and cleanup the mount directory.
122
.\Invoke-AppDeploy.ps1 -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019" -LogDir "C:\Temp\Log"
123
​
124
# Mount WIM to an custom location, install the application, unmount WIM and cleanup the mount directory.
125
.\Invoke-AppDeploy.ps1 -MountDir "C:\Temp\Mount" -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019"
126
​
127
# Mount WIM to an custom location, use a custom log location, install the application, unmount WIM and cleanup the mount directory.
128
.\Invoke-AppDeploy.ps1 -MountDir "C:\Temp\Mount" -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019" -LogDir "C:\Temp\Log"
129
​
130
.NOTES
131
Version: 1.0.5
132
Filename: Invoke-AppDeploy.ps1
133
Author: Sune Thomsen
134
Contact: @SuneThomsenDK
135
Created: 24-08-2020
136
Modified: 05-12-2020
137
​
138
Contributors: @MDaugaard_DK
139
​
140
Version History:
141
1.0.0 - (24-08-2020) Script created.
142
1.0.1 - (28-08-2020) Added correct exit code and changed the logic in the Invoke-ApplicationDeployment function.
143
1.0.2 - (31-08-2020) Added Invoke-SplitLog function to the script, which will split logs when it become larger than 250KB.
144
1.0.3 - (01-09-2020) Added VERBOSE to the script.
145
1.0.4 - (06-10-2020) Added check for already mounted images.
146
1.0.5 - (05-12-2020) Added XML parameter for XML configuration used in Microsoft Office Click-To-Run installation.
147
​
148
.LINK
149
https://github.com/SuneThomsenDK
150
#>
151
[CmdletBinding(SupportsShouldProcess = $true)]Param (
152
[Parameter(Mandatory = $false, HelpMessage = 'Changes the default location from ".\Mount" to the location specified.')]
153
[ValidateNotNullOrEmpty()]
154
[System.IO.FileInfo]
155
[String]$MountDir = ".\Mount",
156
​
157
[Parameter(Mandatory = $false, HelpMessage = 'Changes the default location and filename from ".\Application.wim" to the location and filename specified.')]
158
[ValidateNotNullOrEmpty()]
159
[System.IO.FileInfo]
160
[String]$SourceWIM = ".\Application.wim",
161
​
162
[Parameter(Mandatory = $false, HelpMessage = 'Changes the default location from "$env:SystemRoot\Temp" (C:\WINDOWS\Temp) to the location specified.')]
163
[ValidateNotNullOrEmpty()]
164
[System.IO.FileInfo]
165
[String]$LogDir = "$env:SystemRoot\Temp",
166
​
167
[Parameter(Mandatory = $false, HelpMessage = 'Specify a XML configuration file for the application to be deployed, e.g. Office-Configuration.xml')]
168
[ValidateNotNullOrEmpty()]
169
[string]$XML,
170
​
171
[Parameter(Mandatory = $true, HelpMessage = 'Specify a name of the application to be deployed, e.g. Microsoft Office Professional Plus 2019.')]
172
[ValidateNotNullOrEmpty()]
173
[string]$AppName,
174
​
175
[Parameter(Mandatory = $true, HelpMessage = 'Specify whether to Install, Uninstall or Repair the application, e.g. .\Invoke-AppDeploy.ps1 -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office Professional Plus 2019"')]
176
[ValidateNotNullOrEmpty()]
177
[ValidateSet("Install", "Uninstall", "Repair")]
178
[string]$DeploymentMode
179
)
180
Begin {
181
# Set the variables used throughout the script
182
$ScriptName = $MyInvocation.MyCommand.Name
183
Write-Verbose "Script name: $($ScriptName)"
184
​
185
$ReturnCode = 0
186
Write-Verbose "Return code: $($ReturnCode)"
187
​
188
Switch ($DeploymentMode) {
189
"Install" {
190
$FilePath = Join-Path -Path "$MountDir" -ChildPath "Install.cmd"
191
$Process = "cmd.exe"
192
$Arguments = @(
193
"/c",
194
"""$FilePath""",
195
"> nul",
196
"&& exit"
197
)
198
}
199
"Uninstall" {
200
$FilePath = Join-Path -Path "$MountDir" -ChildPath "Uninstall.cmd"
201
$Process = "cmd.exe"
202
$Arguments = @(
203
"/c",
204
"""$FilePath""",
205
"> nul",
206
"&& exit"
207
)
208
}
209
"Repair" {
210
$FilePath = Join-Path -Path "$MountDir" -ChildPath "Repair.cmd"
211
$Process = "cmd.exe"
212
$Arguments = @(
213
"/c",
214
"""$FilePath""",
215
"> nul",
216
"&& exit"
217
)
218
}
219
}
220
Write-Verbose "Deployment Mode: $($DeploymentMode)"
221
Write-Verbose "Deployment FilePath: $($FilePath)"
222
Write-Verbose "Deployment Process: $($Process)"
223
Write-Verbose "Deployment Arguments: $($Arguments)"
224
}
225
Process {
226
# Functions
227
Function Write-Log {
228
Param (
229
[Parameter(Mandatory=$true, HelpMessage = "Message added to the log file.")]
230
[ValidateNotNullOrEmpty()]
231
[String]$Message,
232
​
233
[Parameter(Mandatory=$false, HelpMessage = "Specify severity for the message. 1 = Information, 2 = Warning, 3 = Error.")]
234
[ValidateNotNullOrEmpty()]
235
[ValidateSet("1", "2", "3")]
236
[String]$Severity = "1"
237
)
238
​
239
#Set log file max size
240
If (($LogMaxSize -eq $Null)) {
241
$Script:LogMaxSize = 250KB
242
Write-Verbose "Write-Log - Log Max Size: $($LogMaxSize) Bytes"
243
}
244
​
245
# Trying to create log directory and filename if it does not exist
246
If (!(Test-Path -Path "$LogDir")) {
247
Try {
248
New-Item -Path $LogDir -ItemType Directory | Out-Null
249
$Script:LogDirFound = "True"
250
Write-Verbose "Write-Log - Log Directory: $($LogDir)"
251
}
252
Catch {
253
# Log directory creation failed. Write error on screen and stop the script.
254
Write-Error -Message "Log directory creation failed. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" -ErrorAction Stop
255
}
256
}
257
Else {
258
If (($LogDirFound -eq $Null)) {
259
$Script:LogDirFound = "True"
260
Write-Verbose "Write-Log - Log Directory: $($LogDir)"
261
}
262
}
263
​
264
If (($LogFile -eq $Null)) {
265
$Script:LogFile = "$($ScriptName).log"
266
Write-Verbose "Write-Log - Log Filename: $($LogFile)"
267
}
268
​
269
# Combine log directory with log file
270
If (($LogFilePath -eq $Null)) {
271
$Script:LogFilePath = Join-Path -Path "$LogDir" -ChildPath "$LogFile"
272
Write-Verbose "Write-Log - Log Path: $($LogFilePath)"
273
}
274
​
275
# Creating timestamp for the log entry
276
If (($Global:TimezoneBias -eq $Null)) {
277
[Int]$Global:TimezoneBias = [System.TimeZone]::CurrentTimeZone.GetUtcOffset([DateTime]::Now).TotalMinutes
278
Write-Verbose "Write-Log - Log Timezone Bias: $($TimezoneBias)"
279
}
280
​
281
If (!($LogTime -eq $Null)) {
282
$Script:LogTime = -Join @((Get-Date -Format "HH:mm:ss.fff"), $TimezoneBias)
283
}
284
Else {
285
$Script:LogTime = -Join @((Get-Date -Format "HH:mm:ss.fff"), $TimezoneBias)
286
Write-Verbose "Write-Log - Log Time: $($LogTime)"
287
}
288
​
289
If (!($LogDate -eq $Null)) {
290
$Script:LogDate = (Get-Date -Format "MM-dd-yyyy")
291
}
292
Else {
293
$Script:LogDate = (Get-Date -Format "MM-dd-yyyy")
294
Write-Verbose "Write-Log - Log Date: $($LogDate)"
295
}
296
​
297
# Creating context, component and log entry
298
If (($LogContext -eq $Null)) {
299
$Script:LogContext = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
300
Write-Verbose "Write-Log - Log Context: $($LogContext)"
301
}
302
​
303
If (($LogComponent -eq $Null)) {
304
$Script:LogComponent = "ApplicationDeployment"
305
Write-Verbose "Write-Log - Log Component: $($LogComponent)"
306
}
307
​
308
$LogEntry = "<![LOG[$($Message)]LOG]!><time=""$($LogTime)"" date=""$($LogDate)"" component=""$($LogComponent)"" context=""$($LogContext)"" type=""$($Severity)"" thread=""$($PID)"" file=""$($ScriptName)"">"
309
​
310
# Trying to write log entry to log file
311
If (!($LogFilePath -eq $Null)) {
312
Try {
313
Out-File -InputObject $LogEntry -Append -NoClobber -Encoding Default -FilePath $LogFilePath
314
Write-Verbose "Write-Log - Log Entry: $($LogEntry)"
315
}
316
Catch {
317
# Failed to append log entry. Write warning on screen but let the script continue.
318
Write-Warning -Message "Failed to append log entry to $($LogFile). Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
319
}
320
}
321
Else {
322
# Failed to append log entry. Write warning on screen but let the script continue.
323
Write-Warning -Message "Failed to append log entry. Error message: Log file not found."
324
}
325
​
326
# Check log size and split if it's greather than 250KB
327
If ((Test-Path -Path "$LogFilePath") -and (Get-ChildItem -Path $LogFilePath).Length -ge $LogMaxSize) {
328
Try {
329
Invoke-SplitLog
330
Write-Log -Message "The log file has been split, older log entries can be found here: $($SplitLogFilePath)" -Severity 2
331
}
332
Catch {
333
# Failed to split the log file. Write warning on screen but let the script continue.
334
Write-Warning -Message "Failed to split the log file."
335
}
336
} }
337
Function Invoke-SplitLog {
338
$SplitLogFileTime = (Get-Date).toString("yyyyMMdd-HHmmss")
339
$SplitLogFile = "$($ScriptName)_$($SplitLogFileTime).log"
340
$Script:SplitLogFilePath = Join-Path -Path "$LogDir" -ChildPath "$SplitLogFile"
341
Write-Verbose "Invoke-SplitLog - Log Split Timestamp: $($SplitLogFileTime)"
342
Write-Verbose "Invoke-SplitLog - Log Split Filename: $($SplitLogFile)"
343
Write-Verbose "Invoke-SplitLog - Log Split Path: $($SplitLogFilePath)"
344
​
345
$Reader = New-Object System.IO.StreamReader("$LogFilePath")
346
While(($Line = $Reader.ReadLine()) -ne $Null) {
347
Add-Content -Path $SplitLogFilePath -Value $Line
348
}
349
$Reader.Close()
350
​
351
# Remove old log file
352
Remove-Item -Path $LogFilePath -Force
353
Write-Verbose "Invoke-SplitLog - Log File Deleted: $($LogFilePath)"
354
​
355
# Compress the archived log file
356
Compact /C $SplitLogFilePath | Out-Null
357
Write-Verbose "Invoke-SplitLog - Archived Log File Compressed: $($SplitLogFilePath)"
358
}
359
Function Invoke-MountImage {
360
Write-Log -Message " - Directory mount verification . . ."
361
# Trying to create mount directory if it does not exist
362
If (!(Test-Path -Path "$MountDir")) {
363
Try {
364
Write-Log -Message " - A mount directory was not found. Trying to create mount directory: $($MountDir)" -Severity 2
365
New-Item -Path $MountDir -ItemType Directory | Out-Null
366
Write-Verbose "Invoke-MountImage - Mount Directory: $($MountDir)"
367
Write-Log -Message " - Creation of the mount directory was successful"
368
}
369
Catch {
370
# Mount directory creation failed. Set return code and write log entry.
371
$Script:ReturnCode = 1
372
Write-Verbose "Invoke-MountImage - Return code: $($ReturnCode)"
373
Write-Log -Message " - Mount directory creation failed. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" -Severity 3
374
}
375
}
376
Else {
377
Write-Verbose "Invoke-MountImage - Mount Directory: $($MountDir)"
378
Write-Log -Message " - A mount directory was found: $($MountDir)"
379
}
380
​
381
If (($ReturnCode -eq 0)) {
382
# Checking for already mounted images.
383
Write-Log -Message " - Checking for already mounted images . . ."
384
$CheckMount = Get-WindowsImage -Mounted
385
If (($CheckMount)) {
386
Write-Log -Message " - A mounted image was found." -Severity 2
387
Write-Log -Message " - Path: $($CheckMount.path)" -Severity 2
388
Write-Log -Message " - ImagePath: $($CheckMount.ImagePath)" -Severity 2
389
Write-Log -Message " - ImageIndex: $($CheckMount.ImageIndex)" -Severity 2
390
Write-Log -Message " - MountMode: $($CheckMount.MountMode)" -Severity 2
391
Write-Log -Message " - MountStatus: $($CheckMount.MountStatus)" -Severity 2
392
Write-Log -Message " - Trying to unmount image: $($CheckMount.ImagePath)"
393
​
394
Get-WindowsImage -Mounted | ForEach-Object {$_ | Dismount-WindowsImage -Discard -ErrorVariable wimerr | Out-Null; if ([bool]$wimerr) {$errflag = $true}}; If (-not $errflag) {Clear-WindowsCorruptMountPoint | Out-Null}
395
​
396
# Checking for already mounted images again.
397
$CheckMount = Get-WindowsImage -Mounted
398
If (!($CheckMount)) {
399
Write-Log -Message " - Image unmount was successful"
400
Write-Log -Message " - Trying to mount image: $($SourceWIM)"
401
}
402
Else {
403
Write-Log -Message " - Image unmount failed. But the script will try to mount image: $($SourceWIM)"
404
}
405
}
406
Else {
407
Write-Log -Message " - No mounted images was found. Trying to mount image: $($SourceWIM)"
408
}
409
Try {
410
# Trying to mount image
411
Mount-WindowsImage -ImagePath "$SourceWIM" -Index 1 -Path $MountDir | Out-Null
412
Write-Verbose "Invoke-MountImage - Mounted Image: $($SourceWIM)"
413
Write-Log -Message " - Image mount was successful"
414
} Catch {
415
# Image mount failed. Set return code and provide help for further investigation.
416
$Script:ReturnCode = 1
417
Write-Verbose "Invoke-MountImage - Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
418
Write-Verbose "Invoke-MountImage - Return code: $($ReturnCode)"
419
Write-Log -Message " - Image mount failed. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" -Severity 3
420
If (($_.Exception.Message -Match "wim is already mounted") -or ($_.Exception.Message -Match "wim er allerede tilsluttet") -or ($_.Exception.Message -Match "wim er allerede montert")) {
421
Write-Log -Message "For further information, please examine the DISM log: C:\WINDOWS\Logs\DISM\dism.log" -Severity 2
422
Write-Log -Message " " -Severity 2
423
Write-Log -Message "Check for mounted image with one of the below commands" -Severity 2
424
Write-Log -Message "-------------------------------------------------------------------" -Severity 2
425
Write-Log -Message "DISM command: Dism /Get-MountedImageInfo" -Severity 2
426
Write-Log -Message 'PowerShell command: Get-WindowsImage -Mounted' -Severity 2
427
Write-Log -Message " " -Severity 2
428
Write-Log -Message "Try unmounting the image with one of the below commands" -Severity 2
429
Write-Log -Message "-------------------------------------------------------------------" -Severity 2
430
Write-Log -Message "DISM command: Dism /Unmount-Image /MountDir:$($MountDir) /Discard" -Severity 2
431
Write-Log -Message 'PowerShell command: Get-WindowsImage -Mounted | ForEach-Object {$_ | Dismount-WindowsImage -Discard -ErrorVariable wimerr; if ([bool]$wimerr) {$errflag = $true}}; If (-not $errflag) {Clear-WindowsCorruptMountPoint}' -Severity 2
432
Write-Log -Message " " -Severity 2
433
Write-Log -Message "Please visit Microsoft Docs for further information about DISM or Get-WindowsImage." -Severity 2
434
Write-Log -Message "https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/dism-image-management-command-line-options-s14" -Severity 2
435
Write-Log -Message "https://docs.microsoft.com/en-us/powershell/module/dism/get-windowsimage" -Severity 2
436
}
437
Else {
438
Invoke-MountCleanup
439
}
440
}
441
}
442
} Function Invoke-UnmountImage { Try {
443
# Trying to unmount image
444
Write-Log -Message " - Trying to unmount image: $($SourceWIM)" Dismount-WindowsImage -Path $MountDir -Discard | Out-Null
445
Write-Verbose "Invoke-UnmountImage - Unmounting Image: $($SourceWIM)"
446
Write-Log -Message " - Image unmount was successful"
447
If ((Test-Path -Path "$MountDir")) {
448
Invoke-MountCleanup
449
} } Catch {
450
# Image mount failed. Set return code and provide help for further investigation.
451
$Script:ReturnCode = 1
452
Write-Verbose "Invoke-UnmountImage - Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
453
Write-Verbose "Invoke-UnmountImage - Return code: $($ReturnCode)"
454
Write-Log -Message " - Image unmount failed. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" -Severity 3
455
Write-Log -Message "For further information, please examine the DISM log: C:\WINDOWS\Logs\DISM\dism.log" -Severity 2
456
Write-Log -Message " " -Severity 2
457
Write-Log -Message "Check for mounted image with one of the below commands" -Severity 2
458
Write-Log -Message "-------------------------------------------------------------------" -Severity 2
459
Write-Log -Message "DISM command: Dism /Get-MountedImageInfo" -Severity 2
460
Write-Log -Message 'PowerShell command: Get-WindowsImage -Mounted' -Severity 2
461
Write-Log -Message " " -Severity 2
462
Write-Log -Message "Try unmounting the image with one of the below commands" -Severity 2
463
Write-Log -Message "-------------------------------------------------------------------" -Severity 2
464
Write-Log -Message "DISM command: Dism /Unmount-Image /MountDir:$($MountDir) /Discard" -Severity 2
465
Write-Log -Message 'PowerShell command: Get-WindowsImage -Mounted | ForEach-Object {$_ | Dismount-WindowsImage -Discard -ErrorVariable wimerr; if ([bool]$wimerr) {$errflag = $true}}; If (-not $errflag) {Clear-WindowsCorruptMountPoint}' -Severity 2
466
Write-Log -Message " " -Severity 2
467
Write-Log -Message "Please visit Microsoft Docs for further information about DISM or Get-WindowsImage." -Severity 2
468
Write-Log -Message "https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/dism-image-management-command-line-options-s14" -Severity 2
469
Write-Log -Message "https://docs.microsoft.com/en-us/powershell/module/dism/get-windowsimage" -Severity 2
470
}
471
}
472
Function Invoke-MountCleanup {
473
Try {
474
# Trying to cleanup the mount directory
475
Write-Log -Message " - Trying to cleanup the mount directory"
476
Remove-Item -Path $MountDir -Recurse -ErrorAction SilentlyContinue | Out-Null
477
Write-Verbose "Invoke-MountCleanup - Mount Cleanup: $($MountDir)"
478
Write-Log -Message " - Mount cleanup was successful"
479
}
480
Catch {
481
# Mount cleanup failed. Write log entry.
482
Write-Verbose "Invoke-MountCleanup - Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"
483
Write-Log -Message " - Mount cleanup was unsuccessful" -Severity 2
484
}
485
}
486
​
487
Function Invoke-ApplicationDeployment {
488
Param (
489
[Parameter(Mandatory=$true, HelpMessage = "Specify a process for the deployment process.")]
490
[ValidateNotNullOrEmpty()]
491
[String]$Process,
492
​
493
[Parameter(Mandatory=$true, HelpMessage = "Specify arguments for the deployment process.")]
494
[ValidateNotNullOrEmpty()]
495
[String[]]$Arguments
496
)
497
​
498
Try {
499
# Trying to execute the install process
500
Write-Log -Message " - Trying to $($DeploymentMode.ToLower()) $($AppName)"
501
If (($Host.Name -Match "ConsoleHost")) {
502
Write-Verbose "Invoke-ApplicationDeployment - Install Process: $($Process) $($Arguments)"
503
$Install = Start-Process -FilePath $Process -ArgumentList $Arguments -PassThru -NoNewWindow -Wait
504
}
505
Else {
506
Write-Verbose "Invoke-ApplicationDeployment - Install Process: $($Process) $($Arguments)"
507
$Install = Start-Process -FilePath $Process -ArgumentList $Arguments -PassThru -WindowStyle Hidden -Wait
508
}
509
$Install.WaitForExit()
510
$Script:ReturnCode = $Install.ExitCode
511
​
512
# Validate the install process
513
If (($ReturnCode -eq 0) -or ($ReturnCode -eq 3010)) {
514
Write-Verbose "Invoke-ApplicationDeployment - Return Code: $($ReturnCode)"
515
Write-Log -Message " - The $($DeploymentMode.ToLower()) was successful"
516
}
517
Else {
518
Write-Verbose "Invoke-ApplicationDeployment - Return Code: $($ReturnCode)"
519
Write-Log -Message " - The $($DeploymentMode.ToLower()) did not complete successfully. Return code $($ReturnCode)" -Severity 2
520
Invoke-UnmountImage
521
}
522
}
523
Catch {
524
# The deployment failed. Set return code and write log entry.
525
$Script:ReturnCode = 1
526
Write-Verbose "Invoke-ApplicationDeployment - Return Code: $($ReturnCode)"
527
Write-Log -Message " - The $($DeploymentMode.ToLower()) failed. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" -Severity 3
528
Invoke-UnmountImage
529
}
530
}
531
​
532
# Start Logging
533
Write-Log -Message "Successfully initialized logging for Application Deployment"
534
Write-Log -Message "[GatherInfo]: Starting gathering information . . ."
535
Write-Log -Message " - Username: $Env:USERDOMAIN\$Env:USERNAME"
536
Write-Log -Message " - Computername: $Env:USERDOMAIN\$env:COMPUTERNAMEquot;
537
Write-Log -Message " - Application: $($AppName)"
538
Write-Log -Message " - Deployment mode: $($DeploymentMode)"
539
Write-Log -Message " - Log location: $($LogFilePath)"
540
Write-Log -Message "[GatherInfo]: Completed gathering information"
541
​
542
# Trying to mount image
543
If (($ReturnCode -eq 0)) {
544
Write-Log -Message "[MountImage]: Starting mounting image . . ."
545
Invoke-MountImage
546
​
547
# Validate the image mount
548
If (($ReturnCode -eq 0)) {
549
Write-Log -Message "[MountImage]: Completed mounting image"
550
}
551
Else {
552
Write-Log -Message "[MountImage]: Mounting image was unsuccessful"
553
}
554
}
555
​
556
# Trying to deploy the application
557
If (($ReturnCode -eq 0)) {
558
Write-Log -Message "[ApplicationDeployment]: Starting application deployment in ''$($DeploymentMode)'' mode . . ."
559
Invoke-ApplicationDeployment -Process $Process -Arguments $Arguments
560
561
# Validate the application deployment
562
If (($ReturnCode -eq 0) -or ($ReturnCode -eq 3010)) {
563
Write-Log -Message "[ApplicationDeployment]: Completed application deployment in ''$($DeploymentMode)'' mode"
564
}
565
Else {
566
Write-Log -Message "[ApplicationDeployment]: Application deployment in ''$($DeploymentMode)'' mode was unsuccessful"
567
}
568
}
569
​
570
# Trying to unmount image
571
If (($ReturnCode -eq 0) -or ($ReturnCode -eq 3010)) {
572
Write-Log -Message "[UnmountImage]: Starting unmounting image . . ."
573
Invoke-UnmountImage
574
​
575
# Validate the image unmount
576
If (($ReturnCode -eq 0) -or ($ReturnCode -eq 3010)) {
577
Write-Log -Message "[UnmountImage]: Completed unmounting image"
578
}
579
Else {
580
Write-Log -Message "[UnmountImage]: Unmounting image was unsuccessful"
581
}
582
}
583
}
584
End {
585
# End Logging
586
Write-Log -Message "Application Deployment is exiting with return code $($ReturnCode)"
587
Write-Log -Message "Successfully finalized logging for Application Deployment"
588
​
589
# Set Exit Code
590
Write-Verbose "Exiting with return code $($ReturnCode)"
591
Exit $ReturnCode
592
}
Copied!

Create the Application

Okay, so letΒ΄s say that we want to deploy Microsoft Office Professional Plus 2019 in your environment and we have already created the application.wim file and modified the PowerShell script to fit your needs.
We will then have to created an application in Microsoft Endpoint Configuration Manager with an deployment type like we have done many times before, the only difference this time is the command lines used to install the application and that the source folder only contains the application.wim file, PowerShell script and maybe a folder with icons for Software Center (optional)
Source Folder with Application.wim and the PowerShell script
PowerShell Commands Used for Installation, Uninstall and Repair
1
# Install - Microsoft Office Professional Plus 2019
2
PowerShell.exe -ExecutionPolicy Bypass -NoProfile -File ".\Invoke-AppDeploy.ps1" -DeploymentMode "Install" -XML "Office-Configuration.xml" -AppName "Microsoft Office 2019"
Copied!
1
# Uninstall - Microsoft Office Professional Plus 2019
2
PowerShell.exe -ExecutionPolicy Bypass -NoProfile -File ".\Invoke-AppDeploy.ps1" -DeploymentMode "Uninstall" -XML "Office-Uninstall.xml" -AppName "Microsoft Office 2019"
Copied!
1
# Repair - Microsoft Office Professional Plus 2019
2
PowerShell.exe -ExecutionPolicy Bypass -NoProfile -File ".\Invoke-AppDeploy.ps1" -DeploymentMode "Repair" -XML "Office-Configuration.xml" -AppName "Microsoft Office 2019"
Copied!
Finish up the application creation, deploy it to your environment and enjoy
😎
​

What Does the Log Say?

Earlier in this blog post I showed you how to WIM the Java 8 Update 271 application, let's get back to that deployment and see what the log says.
So, the script comes with a user-friendly and ingenious log function (If I must say so myself
πŸ€“
). By default the script will create the log file in "C:\WINDOWS\Temp" and the log file will automatically split itself when it reaches 250 KB in size, the old log file will get time stamped and compressed.
As you can see in the below print screens, the Java 8 Update 271 is installed and uninstalled from different PS sessions (Hint - look at the different threads), the log is pretty detailed, but still user-friendly and it handles return codes as well, as you can see below.
Log - Installing Java 8 Update 271
Log - Uninstalling Java 8 Update 271
Log - Installing Java 8 Update 271 - Return Code 1603 = Already Installed
That's pretty awesome if you ask me
πŸ€“
​

CONCLUSION

Firstly, I must say that WIM your applications is pretty awesome! In most cases it can improve deployment time and reduce the size of the application and storage usage on the content share and distribution point.
In our BranchCache enabled production environment we have seen 30 - 60% improvements on deployment time for huge applications like Autodesk Revit and it has also reduced the application creation wait time in the Microsoft Endpoint Configuration Manager console.
But as you can also read from this blog post, you won't gain anything with small or already compressed applications, so the only thing you will gain here is to standardize your deployment methods.
With that said, I would definitely recommend "WIMMING" your applications, why? Because 1 file is better than 12,000 files and that is "WINNING" in the end
😎
​
WIMMING is WINNING...
If you have any questions regarding this topic, feel free to reach out to me. I am most active on Twitter!
Last modified 7mo ago