-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Expand file tree
/
Copy pathBuild-MinWinVHD.ps1
More file actions
193 lines (155 loc) · 7.13 KB
/
Build-MinWinVHD.ps1
File metadata and controls
193 lines (155 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# ================================
# Minimal Windows 11 VHDX Builder
# ================================
# Run as Administrator
#Requires -Version 5.1
#Requires -RunAsAdministrator
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'This script is not intended to have any outputs piped')]
param
(
[Parameter(
Mandatory = $true,
HelpMessage = 'Drive letter where Windows 11 ISO is mounted (e.g., D:)'
)] [string] $IsoDrive = 'D:',
[Parameter(
Mandatory = $true,
HelpMessage = 'Index of the Windows 11 edition to install from the image (use /Get-ImageInfo to check)'
)] [ValidateRange(1, 10)] [int] $ImageIndex = 1,
[Parameter(
Mandatory = $true,
HelpMessage = 'Path to create the VHDX file (e.g., C:\MinWin11.vhdx)'
)] [string] $VhdPath = 'C:\MinWin11.vhdx',
[Parameter(
Mandatory = $false,
HelpMessage = 'Size of the VHDX in GB'
)] [ValidateRange(20, [int]::MaxValue)] [int] $VhdSizeGB = 25,
[Parameter(
Mandatory = $false,
HelpMessage = 'Name of the Hyper-V VM to create (optional)'
)] [string] $VmName = 'MinWin11'
)
Write-Host '=== Step 0: Prepare paths and image info ===' -ForegroundColor Cyan
# Determine install.wim or install.esd path
$InstallWim = Join-Path $IsoDrive 'sources\install.wim'
if (-not (Test-Path $InstallWim)) {
$InstallWim = Join-Path $IsoDrive 'sources\install.esd'
}
# Verify image file exists
if (-not (Test-Path $InstallWim)) {
throw "Cannot find install.wim or install.esd on $IsoDrive. Mount a Windows 11 ISO and update `\$IsoDrive`."
}
Write-Host "Using image file: $InstallWim" -ForegroundColor Yellow
Write-Host '=== Step 1: Create and initialize VHDX ===' -ForegroundColor Cyan
# Create VHDX
New-VHD -Path $VhdPath -SizeBytes ("${VhdSizeGB}GB") -Dynamic | Out-Null
# Mount and initialize
$disk = Mount-VHD -Path $VhdPath -Passthru
Initialize-Disk -Number $disk.Number -PartitionStyle GPT | Out-Null
# Create EFI + OS partitions
$efiPartition = New-Partition -DiskNumber $disk.Number -Size 100MB -GptType '{C12A7328-F81F-11D2-BA4B-00A0C93EC93B}' -AssignDriveLetter
$osPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter
# Format partitions
Format-Volume -Partition $efiPartition -FileSystem FAT32 -NewFileSystemLabel 'System' -Confirm:$false | Out-Null
Format-Volume -Partition $osPartition -FileSystem NTFS -NewFileSystemLabel 'Windows' -Confirm:$false | Out-Null
$EfiDrive = ($efiPartition | Get-Volume).DriveLetter + ':'
$OsDrive = ($osPartition | Get-Volume).DriveLetter + ':'
Write-Host "EFI drive: $EfiDrive OS drive: $OsDrive" -ForegroundColor Yellow
Write-Host '=== Step 2: Apply Windows image to OS partition ===' -ForegroundColor Cyan
# If using ESD, DISM can still apply directly
dism /Apply-Image /ImageFile:$InstallWim /Index:$ImageIndex /ApplyDir:$OsDrive | Out-Null
Write-Host '=== Step 3: Basic boot configuration ===' -ForegroundColor Cyan
# Create boot files on EFI partition
bcdboot "$OsDrive\Windows" /s $EfiDrive /f UEFI | Out-Null
Write-Host '=== Step 4: Mount offline image for servicing ===' -ForegroundColor Cyan
# Mount the OS volume as an offline image for DISM servicing
$MountDir = 'D:\Mount_MinWin11'
if (-not (Test-Path $MountDir)) {
New-Item -ItemType Directory -Path $MountDir | Out-Null
}
# Use DISM to mount the offline image
dism /Mount-Image /ImageFile:$InstallWim /Index:$ImageIndex /MountDir:$MountDir /ReadOnly | Out-Null
# NOTE:
# We will service the *applied* OS at $OsDrive, not the ISO image.
# For feature removal, we target the offline OS with /Image:$OsDrive not /Image:$MountDir.
Write-Host '=== Step 5: Remove optional features (offline) ===' -ForegroundColor Cyan
# You can see available features with:
# dism /Image:$OsDrive /Get-Features
# Aggressive feature removal list (adjust to taste)
$featuresToDisable = @(
'FaxServicesClientPackage',
'Printing-Foundation-Features',
'Printing-PrintToPDFServices-Features',
'Printing-XPSServices-Features',
'MSRDC-Infrastructure',
'Microsoft-Windows-Subsystem-Linux',
'MediaPlayback' ,
'WindowsMediaPlayer',
'WorkFolders-Client',
'SMB1Protocol',
'WCF-Services45',
'WCF-TCP-PortSharing45',
'IIS-WebServerRole',
'IIS-WebServer',
'IIS-DefaultDocument',
'IIS-DirectoryBrowsing',
'IIS-HttpErrors',
'IIS-StaticContent',
'IIS-HttpRedirect',
'IIS-ApplicationDevelopment',
'IIS-ISAPIExtensions',
'IIS-ISAPIFilter',
# "IIS-NetFxExtensibility45",
'IIS-ASPNET45',
'IIS-HealthAndDiagnostics',
'IIS-HttpLogging',
'IIS-LoggingLibraries',
'IIS-RequestMonitor',
'IIS-HttpTracing',
'IIS-Security',
'IIS-RequestFiltering',
'IIS-IPSecurity',
'IIS-Performance',
'IIS-HttpCompressionStatic',
'IIS-WebServerManagementTools',
'IIS-IIS6ManagementCompatibility',
'IIS-Metabase',
'IIS-HostableWebCore'
)
foreach ($feature in $featuresToDisable) {
Write-Host "Disabling feature: $feature" -ForegroundColor DarkYellow
dism /Image:$OsDrive /Disable-Feature /FeatureName:$feature /Remove | Out-Null
}
Write-Host '=== Step 6: Remove provisioned apps (offline) ===' -ForegroundColor Cyan
# Remove all provisioned appx packages except Store and framework (adjust as needed)
$ProvisionedApps = dism /Image:$OsDrive /Get-ProvisionedAppxPackages | Select-String 'PackageName'
foreach ($line in $ProvisionedApps) {
$pkg = $line.ToString().Split(':')[1].Trim()
if ($pkg -notlike '*Store*' -and $pkg -notlike '*NET*' -and $pkg -notlike '*AppInstaller*') {
Write-Host "Removing provisioned app: $pkg" -ForegroundColor DarkYellow
dism /Image:$OsDrive /Remove-ProvisionedAppxPackage /PackageName:$pkg | Out-Null
}
}
Write-Host '=== Step 7: WinSxS cleanup and image optimization ===' -ForegroundColor Cyan
# Component store cleanup to reduce size
dism /Image:$OsDrive /Cleanup-Image /StartComponentCleanup /ResetBase | Out-Null
Write-Host '=== Step 8: Unmount temporary mount and clean up ===' -ForegroundColor Cyan
# Unmount DISM image
dism /Unmount-Image /MountDir:$MountDir /Discard | Out-Null
# Remove mount directory
Remove-Item $MountDir -Recurse -Force | Out-Null
# Dismount VHD (you can leave it mounted if you want to inspect it)
Dismount-VHD -Path $VhdPath
Write-Host '=== Step 9: (Optional) Create a Hyper-V VM using this VHDX ===' -ForegroundColor Cyan
# Create a Hyper-V VM if Hyper-V module is available
if (Get-Command New-VM -ErrorAction SilentlyContinue) {
if (-not (Get-VM -Name $VmName -ErrorAction SilentlyContinue)) {
New-VM -Name $VmName -MemoryStartupBytes 2GB -Generation 2 -VHDPath $VhdPath | Out-Null
Set-VMFirmware -VMName $VmName -FirstBootDevice (Get-VMFirmware -VMName $VmName).BootOrder[0]
Write-Host "Created Hyper-V VM '$VmName' using $VhdPath" -ForegroundColor Green
} else {
Write-Host "Hyper-V VM '$VmName' already exists. Attach $VhdPath manually if needed." -ForegroundColor Yellow
}
} else {
Write-Host "Hyper-V module not available. Create a VM manually and attach $VhdPath." -ForegroundColor Yellow
}
Write-Host "=== DONE. Minimal Windows 11 VHDX created at $VhdPath ===" -ForegroundColor Green