Skip to content

Commit ff3ff29

Browse files
authored
Add script for generating a new Windows 11 VM for validation (microsoft#326455)
1 parent 52a760b commit ff3ff29

File tree

3 files changed

+293
-1
lines changed

3 files changed

+293
-1
lines changed

.github/workflows/scriptAnalyzer.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ on:
66
- master
77
paths:
88
- "**/*.ps1"
9+
- "**/*.psm1"
910
push:
1011
paths:
1112
- "**/*.ps1"
13+
- "**/*.psm1"
1214

1315
permissions:
1416
contents: read # Needed to check out the code
@@ -26,7 +28,8 @@ jobs:
2628
- name: Run PSScriptAnalyzer
2729
run: |
2830
# Run PSScriptAnalyzer on all PowerShell scripts
29-
$results = Get-ChildItem -Recurse -Filter *.ps1 | Invoke-ScriptAnalyzer
31+
$results = @(Get-ChildItem -Recurse -Filter *.ps1 | Invoke-ScriptAnalyzer)
32+
$results += @(Get-ChildItem -Recurse -Filter *.psm1 | Invoke-ScriptAnalyzer)
3033
if ($results) {
3134
Write-Output $results | Format-List -GroupBy ScriptName
3235
}

Tools/Build-MinWinVHD.ps1

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

doc/tools/Build-MinWinVHD.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Using Build-MinWin11.ps1
2+
3+
The [Build-MinWinVHD.ps1](Tools/Build-MinWinVHD.ps1) script creates a new `V`irtual `H`ard `D`isk (VHD) using a mounted Windows ISO for use with a virtual machine. This image will have as few dependencies and apps enabled as possible, to provide the best approximation of the images uses by the validation pipelines. Although this is not the same image, it should be close enough to use for testing manifests for those who are unable to use Windows Sandbox and [SandboxTest.ps1](doc/tools/SandboxTest.ps1).
4+
5+
## Summary
6+
7+
This script creates a dynamically sized VHDX, initializes GPT partitions (EFI + OS), applies a Windows image from a mounted ISO, configures basic UEFI boot, services the offline image (removes features / provisioned apps), and optionally creates a Hyper-V VM using the VHDX.
8+
9+
## Prerequisites
10+
11+
- **Run as Administrator:** The script must be executed from an elevated PowerShell session.
12+
- **PowerShell version:** Requires PowerShell 5.1 or newer due to use of built-in cmdlets and DISM commands.
13+
- **Windows ISO downloaded and mounted:** Mount the Windows 11 ISO (right-click -> Mount, or use `Mount-DiskImage`) and note the assigned drive letter (e.g., `D:`). ISO images can be downloaded from the official Microsoft page: https://www.microsoft.com/software-download/windows11
14+
- **DISM and bcdboot:** The script uses the built-in `dism` and `bcdboot` utilities. Ensure these commands are present on your path.
15+
- **Hyper-V (optional):** If the Hyper-V module is available and you want a VM created automatically, ensure Hyper-V is enabled and that you can manually create a new VM.
16+
17+
> [!IMPORTANT]
18+
> The script includes an "aggressive" list of features and provisioned app removals. Review the script before running if you need specific Windows features or provisioned packages preserved.
19+
20+
## Parameters
21+
22+
| Argument | Description | Required | Default |
23+
|------------------------------|-----------------------------------------------------------------------------|:--------:|:-------:|
24+
| **-IsoDrive** | Drive letter where the Windows ISO is mounted (string). Example: D:. | true ||
25+
| **-ImageIndex** | Index of the Windows image within `install.wim` or `install.esd` (int). Use DISM to list available indexes (for example `dism /Get-WimInfo /WimFile:D:\sources\install.wim`). | true ||
26+
| **-VhdPath** | Full path to create the VHDX file (string). Example: `C:\MinWin11.vhdx`. | true ||
27+
| **-VhdSizeGB** | Size of the VHDX in GB (int). | false | 25 |
28+
| **-VmName** | Name of the Hyper-V VM to create (string). | false | MinWin11 |
29+
30+
## How to determine image index
31+
32+
Use DISM to list images in the WIM/ESD on the mounted ISO. Replace `D:` with your ISO drive letter:
33+
34+
```powershell
35+
# For WIM
36+
dism /Get-WimInfo /WimFile:D:\sources\install.wim
37+
38+
# For ESD (DISM supports reading ESD similarly)
39+
dism /Get-ImageInfo /ImageFile:D:\sources\install.esd
40+
```
41+
42+
Look for the `Index :` value you want to apply (for example, 1 for the first edition).
43+
44+
## Basic usage examples
45+
46+
Open an elevated PowerShell prompt and run (example values shown):
47+
48+
```powershell
49+
# From the Tools folder
50+
cd <path-to-repo>\Tools
51+
52+
# Basic: create a 25GB dynamic VHDX from the image at D:, using index 1
53+
.\Build-MinWin11.ps1 -IsoDrive D: -ImageIndex 1 -VhdPath C:\MinWin11.vhdx
54+
55+
# With custom size and VM name
56+
.\Build-MinWin11.ps1 -IsoDrive D: -ImageIndex 2 -VhdPath C:\MinWin11.vhdx -VhdSizeGB 40 -VmName "MyMinWin11"
57+
```
58+
59+
## What the script does (high level)
60+
61+
- Locates `install.wim` or `install.esd` on the mounted ISO at `\sources`.
62+
- Creates a dynamic VHDX at the path provided (`-VhdPath`) and mounts it.
63+
- Initializes the disk as GPT and creates a 100MB EFI partition and the remaining OS partition.
64+
- Formats the partitions (EFI = FAT32, OS = NTFS).
65+
- Applies the Windows image to the OS partition using `dism /Apply-Image`.
66+
- Creates UEFI boot files with `bcdboot`.
67+
- Mounts an offline copy of the image for servicing and removes a predefined list of optional features and provisioned appx packages (see script for the removal list).
68+
- Runs component-store cleanup (`/StartComponentCleanup /ResetBase`) to reduce size.
69+
- Dismounts VHD and optionally creates a Hyper-V VM named `-VmName` if the Hyper-V cmdlets are present.
70+
71+
## Output
72+
73+
- A VHDX file at the path specified by `-VhdPath` (e.g., `C:\MinWin11.vhdx`).
74+
- If Hyper-V is available and `-VmName` does not exist, a new Gen 2 VM is created and attached to the VHDX.
75+
76+
## Warnings & tips
77+
78+
- The script will throw an error if it cannot locate `install.wim` or `install.esd` in the ISO `sources` folder.
79+
- Review and edit the features/provisioned-app removal lists in the script before running in production environments — the defaults are aggressive and intended for minimal builds.
80+
- If you prefer to inspect the VHD before dismounting, modify the script or run the VHD mounting steps manually.
81+
- Ensure sufficient free space on the volume where `-VhdPath` is located.
82+
83+
## Troubleshooting
84+
85+
- If the ISO is not mounted as a drive letter, use `Mount-DiskImage -ImagePath "C:\path\to\Win11.iso"` and then run `Get-Volume` or check Explorer to find the assigned drive letter.
86+
- If `dism /Apply-Image` fails, verify the `-ImageIndex` value, and confirm whether the image file is `install.wim` or `install.esd`.
87+
- If Hyper-V VM creation fails, verify the `Hyper-V` Windows feature is enabled and that you are running elevated PowerShell.
88+
89+
## Using with other VM Providers
90+
91+
- The produced VHDX (`C:\MinWin11.vhdx`) can be used with other hypervisors, but many providers require a different disk format; convert the VHDX to the format your provider expects before attaching.
92+
- QEMU/KVM: convert to `qcow2` with `qemu-img convert -O qcow2`.
93+
- VMware: convert to `vmdk` with `qemu-img convert -O vmdk` (or use VMware tools if available).
94+
- VirtualBox: use `VBoxManage clonemedium disk --format VDI` or convert with `qemu-img` to `vdi`/`vmdk` as needed.
95+
- Before converting, ensure the VHDX is dismounted and not in use. After conversion, attach the converted disk to a VM configured for UEFI/GPT (Gen2/EFI) and the appropriate disk controller.
96+
- Expect to install or enable guest additions/tools for the target hypervisor and verify drivers (network, storage) inside the guest; hardware differences may require small post-boot fixes.

0 commit comments

Comments
 (0)