Introduction
I originally wrote this in late 2010 using AIK for Vista. This code builds WinPEs for PXE, Disc, and USB, allowing the addition of files, drivers, and (one) startnet command. More recently, I updated the code to also use ADK 8.1 for Windows 8. (I thought it would be nice to have .NET and Powershell in PE, but I have yet to use it... I still use a cmd file to feed diskpart a text file and run imagex.)
I use WinPE to deploy (and re-deploy) Windows images often. I also use it for XP images, but that requires some additional bootsect.exe command not covered here as XP is no longer supported. Generally, each WinPE targets a certain brand and model of computer. Anytime a new model is introduced that requires new drivers, I can have a PE image ready in minutes.
Background
Windows Automated Installation Kit or Windows Assessment and Deployment Kit is required to build and customize WinPE images with this script.
DISM only works on PE versions greater than 2.1.
For ADK, 8.1 and .Net 4.5 are required.
Using the Code
*Code descriptions follow after the code.
Caution: This script can be used to format a USB drive. The author assumes no responsibility for lost data. It has been tested using only one USB device present. (It will format any selected drive, so be careful to select the correct one.)
DISM requires elevation, so the script must be run as an administrator. I use a registry edit so I can right click scripts and select run as. This isn't a requirement.
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas]
[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas\command]
@="\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\" -file \"%1\""
Although the code (as posted) has command parameters, I recommend the no parameter option as it will display a form. The parameters originally applied to the AIK version, but still work but only for ADK--as ADK is now set as the default for building WinPE images. Right click the script, select run as administrator, and a form loads--that's how I prefer it.
The Parameters
param(
[string]$PeDirectory,
[string]$PeEnvironment = "amd64",
$PeDrivers,
$PeExtras,
[string]$PeOutPut = "NONE",
$PeDiskId)
.Synopsis
Create WinPE for ISO, bootable USB, or PXE.
.Parameter PeDirectory
Directory where WinPE will be copied. Omitting this will produce the GUI.
.Parameter PeEnvironment
WinPE environment: amd64 | x86 | i64. Default value is amd64
.Parameter PeDrivers
Directory or directories for drivers to add to WinPE.
.Parameter PeExtras
Additional files to add to the System32 directory--like any additional script files.
.Parameter PeOutPut
Format of final WinPE: ISO | USB | PXE. Default value is NONE.
.Parameter PeDiskId
When OutPut is USB, DiskId identifies the disk for diskpart preparation. Default USB is Disk 1.
.Description
Creates a WinPE image. NOTE: Administrator role is required--Powershell must be run as an Administrator.
A Few Requirements
$IsSTA = ([System.Threading.Thread]::CurrentThread.GetApartmentState() -eq 'STA')
$psVersionMajor = [int]($PSVersionTable.PSVersion).Major
if((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment")."Processor_Architecture".Contains("64")) {
$defDir = [String]::Format("{0}\\Windows Kits\8.1\Assessment and Deployment Kit", ${env:ProgramFiles(x86)})
}
else {
$defDir = [String]::Format("{0}\\Windows Kits\8.1\Assessment and Deployment Kit", $env:ProgramFiles)
}
#$defDir = "the actual install directory" # required for running with parameters.
A FolderBrowserDialog
is used; as a result, the current threading apartment must be known.
To use the Powershell imports for ADK, version 4 with .NET 4.5 is required.
The AIK/ADK directory must be defined; the default behavior is to set it to the expected location. (I do not have either installed in the default location.) This check only applies to the form version. Also note, this can be changed when using the form.
Note: If the install directory is not default, the script needs a default set to be run from a Powershell window using parameters. (I also have this set so I don't have to browse to the installed directory each time it is run.)
Copying WinPE to a Work Directory
function Copy-PE {
param([Parameter(Position=0)][string]$ProcessorArchitecture,
[Parameter(Position=1)][string]$Destination, $installDirectory, [switch]$Aik)
# Create Directories
if(Test-Path "$Destination") {
if((Get-Item "$Destination").GetDirectories().Length -gt 0) {
Write-Warning "$Destination already exists."
return
}
}
else {
Write-Verbose -Message "Creating $Destination"
New-Item -Path "$Destination" -Type directory | Out-Null
}
The original version did not perform updates to existing WinPE images. Using this code makes it all too simple for any need to update an image. Allowing image updates in this revision introduced a new hazard: cross image manipulation, which doesn't work too well. I have added no checking procedures to prevent this from occurring. Additionally, it is possible to add multiple commands to startnet when updating images (something else to watch for...).
# DISM issue
New-Item -Path "$Destination\scratch" -Type directory | Out-Null
# well known SID: S-1-1-0 (everyone)
$identity = New-Object System.Security.Principal.NTAccount("Everyone")
$fsr = [System.Security.AccessControl.FileSystemRights]::FullControl
$act = [System.Security.AccessControl.AccessControlType]::Allow
$inherit = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bxor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
$prop = [System.Security.AccessControl.PropagationFlags]::None
$acr = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fsr, $inherit, $prop, $act)
$acl = Get-Acl "$Destination\scratch"
$acl.AddAccessRule($acr)
(Get-Item "$Destination\scratch").SetAccessControl($acl)
I originally encountered issues using the ADK, and found multiple recommendations for this type of fix. Alas, it didn't work. It is here only if a similar issue is experienced by others (in which case the ADK commands will have to include the scratch parameter.) As seen later, my issue was DISM environment variables--I opted to change to the DISM directory during execution. This code can be omitted.
# Default ADK
$bootFiles = "$installDirectory\Windows Preinstallation Environment\$ProcessorArchitecture\Media"
$isoFiles = "$installDirectory\Deployment Tools\$ProcessorArchitecture\Oscdimg"
$winPeFile = "$installDirectory\Windows Preinstallation Environment\$ProcessorArchitecture\en-us"
if($Aik) {
$bootFiles = "$installDirectory\Tools\PETools\$ProcessorArchitecture"
$isoFiles = "$installDirectory\Tools\PETools\$ProcessorArchitecture\boot"
$winPeFile = "$installDirectory\Tools\PETools\$ProcessorArchitecture"
}
Just setting required directories.
Write-Verbose -Message "Copying efisys.bin"
Copy-Item -Path "$isoFiles\efisys.bin" -Destination "$Destination"
Write-Verbose -Message "Copying efisys_noprompt.bin"
Copy-Item -Path "$isoFiles\efisys_noprompt.bin" -Destination "$Destination"
Write-Verbose -Message "Copying etfsboot.com"
Copy-Item -Path "$isoFiles\etfsboot.com" -Destination "$Destination"
Write-Verbose -Message "Copying winpe.wim"
Copy-Item -Path "$winPeFile\winpe.wim" -Destination "$Destination"
Copying winpe.wim and other ISO support files.
# Mount
Write-Verbose -Message "Creating $Destination\mount"
New-Item -Path "$Destination\mount" -Type directory | Out-Null
# ISO
Write-Verbose -Message "Creating $Destination\ISO"
New-Item -Path "$Destination\ISO" -Type directory | Out-Null
# ISO\sources
Write-Verbose -Message "Creating $Destination\ISO\sources"
New-Item -Path "$Destination\ISO\sources" -Type directory | Out-Null
Write-Verbose -Message "Copying boot.wim"
Copy-Item -Path "$winPeFile\winpe.wim" -Destination "$Destination\ISO\sources"
Rename-Item -Path "$Destination\ISO\sources\winpe.wim" -NewName "boot.wim"
You may notice that I have retained the AIK directory styles--if it's not broke, don't fix it.
Write-Verbose -Message "Copying bootmgr"
Copy-Item -Path "$bootFiles\bootmgr" -Destination "$Destination\ISO"
Write-Verbose -Message "Copying bootmgr.efi"
Copy-Item -Path "$bootFiles\bootmgr.efi" -Destination "$Destination\ISO"
# Boot/EFI directories
Write-Verbose -Message "Copying $sourceDir\boot"
Copy-Item -Path "$bootFiles\boot" -Destination "$Destination\ISO" -Recurse
Write-Verbose -Message "Copying $sourceDir\efi"
Copy-Item -Path "$bootFiles\EFI" -Destination "$Destination\ISO" -Recurse
}
Last items required are the boot files.
Creating a Bootable ISO
function Create-ISO {
param(
[Parameter(Position=0)][string]$PeDir,
[Parameter(Position=1)][string]$FileName,
[Parameter(Position=2)][string]$OscdimgPath)
Write-OutPut "Creating $PeDir\$FileName"
[System.Diagnostics.ProcessStartInfo] $sInfo = New-Object System.Diagnostics.ProcessStartInfo -ArgumentList "$OscdimgPath\oscdimg.exe"
$sInfo.Arguments = [String]::Format("-n `-b`"{0}`" `"{1}`" `"{2}`"", "$PeDir\etfsboot.com", "$PeDir\ISO", "$PeDir\$FileName")
$sInfo.CreateNoWindow = $true
$sInfo.UseShellExecute = $false
[System.Diagnostics.Process] $proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $sInfo
[void]$proc.Start()
$proc.WaitForExit()
if(Test-Path "$PeDir\$FileName") {
Write-OutPut "ISO created successfully."
}
else {
Write-OutPut "ISO was not created."
}
}
It's important to note that the oscdimg.exe must be the same as the executing environment--x86 or amd64.
Creating a Bootable USB
function Create-USB
{
param(
[Parameter(Position=0)][string]$IsoDir,
[string]$UsbDisk = "1")
if((gwmi Win32_Volume -Filter 'DriveType = 2') -eq $null) {
Write-Error "No USB device detected."
return
}
$temp = ${env:TEMP}
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Select Disk $UsbDisk" -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "clean" -Append -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Create Partition Primary" -Append -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Select Part 1" -Append -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Active" -Append -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Format fs=ntfs quick" -Append -Encoding ASCII
$letter = "Z"
$assigned = "Assign letter=$letter"
[byte]$s = 68
while($s -lt 91) {
$letter = [System.Convert]::ToChar($s)
$s++
$filter = [String]::Format("DriveLetter = `"{0}:`"", $letter)
if((gwmi Win32_Volume -Filter $filter) -eq $null) {
$assigned = "Assign letter=$letter"
break
}
}
Out-File -FilePath "$temp\partdisk.txt" -InputObject $assigned -Append -Encoding ASCII
Out-File -FilePath "$temp\partdisk.txt" -InputObject "Exit" -Append -Encoding ASCII
Write-OutPut "Formating USB--all data will be lost."
$procOutput = diskpart /s "$temp\partdisk.txt" 2>&1
Remove-Item -Path "$temp\partdisk.txt"
Write-OutPut "Copying files."
# Copy Files
if(Test-Path "$letter`:\") {
Copy-Item -Path "$IsoDir\bootmgr" -Destination "$letter`:\"
Copy-Item -Path "$IsoDir\bootmgr.efi" -Destination "$letter`:\"
Copy-Item -Path "$IsoDir\boot" -Destination "$letter`:\" -Recurse
Copy-Item -Path "$IsoDir\EFI" -Destination "$letter`:\" -Recurse
Copy-Item -Path "$IsoDir\sources" -Destination "$letter`:\" -Recurse
Write-OutPut "USB ISO created successfully."
}
else {
Write-OutPut "USB was not created."
}
}
This section uses diskpart to format and partition a USB drive.
(It is written to only copy one WinPE image for the USB device. However, with modifications using bcdedit, it is possible to use multiple WinPE images on a single USB.)
Install AIK Packages
function Install-AikPackage {
param([Parameter(Position=0)][string]$pePackage,
[Parameter(Position=1)]$installedPackages,
[Parameter(Position=2)][string]$peDir,
[Parameter(Position=3)][string]$WinPE_FPs)
# check for package
[System.Text.RegularExpressions.Regex]$regx = New-Object System.Text.RegularExpressions.Regex -argumentlist "$pePackage", IgnoreCase
$matches = $regx.Matches($installedPackages)
if($matches -ne $null) {
if($matches.count -lt 2) {
# check for language pack
$b = Select-String -InputObject $packages -Pattern "$pePackage(.*?)en-us~"
if($b -eq $null) {
# install language pack
Write-OutPut "Installing $pePackage language package."
dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\en-us\$pePackage`_en-us.cab"
}
else {
# has language pack
Write-OutPut "Installing $pePackage package."
dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\$pePackage.cab"
}
}
else {
Write-OutPut "Skipping $pePackage Packages."
}
}
else {
Write-OutPut "Installing $pePackage Packages."
dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\$pePackage.cab"
dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\en-us\$pePackage`_en-us.cab"
}
}
Installing AIK specific packages.
Install ADK Packages
function Install-AdkPackage {
param([Parameter(Position=0)][string]$pePackage,
[Parameter(Position=1)]$installedPackages,
[Parameter(Position=2)][string]$peDir,
[Parameter(Position=3)][string]$WinPE_FPs)
$found = $false
foreach($p in $installedPackages) {
# doesn't check for language packs (install both regardless)
[System.Text.RegularExpressions.Regex]$regx = New-Object System.Text.RegularExpressions.Regex -argumentlist "$pePackage", IgnoreCase
$m = $regx.Matches($p.FeatureName)
if($m -ne $null -and $m.Count -gt 0) {
$found = $true
}
}
if($found) {
Write-Host "Skipping $pePackage"
return
}
Add-WindowsPackage -Path "$peDir\mount" -PackagePath "$WinPE_FPs\$pePackage.cab" | Out-Null
Add-WindowsPackage -Path "$peDir\mount" -PackagePath "$WinPE_FPs\en-us\$pePackage`_en-us.cab" | Out-Null
}
Installing ADK specific packages.
Checking for Administrator Privileges
function Has-Role {
param([Security.Principal.WindowsBuiltInRole]$Role = [Security.Principal.WindowsBuiltInRole]::Administrator)
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal $identity
return $principal.IsInRole($Role)
}
DISM requires elevation, without this, DISM will error.
Folder Selection
function Get-Directory {
param([Parameter(Position=0)][string] $WindowCaption, $StartDirectory, [switch] $NoNewFolderButton)
if($IsSTA) {
[System.Windows.Forms.FolderBrowserDialog] $browserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
$browserDialog.Description = $WindowCaption
if($StartDirectory -ne $null) {
$browserDialog.SelectedPath = $StartDirectory
}
if($NoNewFolderButton) {
$browserDialog.ShowNewFolderButton = $false
}
if ($browserDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
return $browserDialog.SelectedPath
}
}
else {
$shell = New-Object -COM "Shell.Application"
$f = $shell.BrowseForFolder(0, $WindowCaption, 0)
if($f -ne $null) {
return $f.Self.Path
}
}
# otherwise
return [String]::Empty
}
Selecting directories required the STA check.
Message Box
# MessageBox -- MUST Have Assemblies Loaded
function Show-Message
{
Param(
[Parameter(Position=0)][string]$message,
[Parameter(Position=1)][string]$caption)
if ($caption -eq $null -or $caption -eq [String]::Empty) {
return [System.Windows.Forms.MessageBox]::Show($message)
}
else {
return [System.Windows.Forms.MessageBox]::Show($message,$caption)
}
}
Making the WinPE Image
function Main
{
param(
[Parameter(Position=0, Mandatory=$true)][string]$WinPEDirectory,
[string]$Environment = "amd64",
$DriverLocation,
$ExtrasPath,
[string]$OutPut = "NONE",
$DiskId,
[string]$startnet,
$WinKitDirectory,
[switch]$UseAik,
[switch]$useExisting)
WinPEDirectory
: where the WinPE will be builtEnvironment
: WinPE environmentDriverLocation
: A directory of drivers or string[]
of driver inf filesExtrasPath
: A directory containing additional files to be added or a string[]
of filesOutPut
: Type of output is PXE, USB, Disc, or NoneDiskId
: Disc ID of USBstartnet
: The command to add to startnet such as an batch file to runWinKitDirectory
: AIK or ADK install directoryUseAik
: Default is ADK, this is set to use AIKuseExisting
: Update an existing WinPE image
# Begin Process................................................
Write-OutPut "------------------------------------------------"
$msg = [DateTime]::Now.ToShortTimeString()
Write-OutPut "Beginning Make-WinPE $msg"
Write-OutPut "------------------------------------------------"
$dismArchitecture = "amd64"
if(!([Environment]::Is64BitProcess)){
$dismArchitecture = "x86"
}
Most important here is getting the correct version of DISM.
# Copy Files
if($UseAik) {
# AIK
if(!($useExisting)) {
Copy-PE $Environment $WinPEDirectory -installDirectory $WinKitDirectory -Aik
}
}
else {
# import dism for ADK (Set-Location for dism dependencies)
Set-Location "$WinKitDirectory\Deployment Tools\$dismArchitecture\DISM"
import-module "$WinKitDirectory\Deployment Tools\$dismArchitecture\DISM"
if(!($useExisting)) {
Copy-PE $Environment $WinPEDirectory -installDirectory $WinKitDirectory
}
}
Copy files for new build. (Also sets the ADK environment requirement.)
if((Test-Path "$WinPEDirectory\ISO\sources\boot.wim") -ne $true) {
Write-Output "boot.wim is missing from $WinPEDirectory\ISO\sources"
Write-Output "Copying new boot.wim"
if(Test-Path "$WinPEDirectory\winpe.wim") {
Copy-Item -Path "$WinPEDirectory\winpe.wim" -Destination "$WinPEDirectory\ISO\sources"
Rename-Item -Path "$WinPEDirectory\ISO\sources\winpe.wim" -NewName "boot.wim"
}
else {
Write-Error "$WinPEDirectory\winpe.wim is missing.
The WinPE directory appears to be corrupted. Select an alternate location for WinPE output."
return
}
}
Additional check for boot.wim:
# Mount...
if($UseAik) {
dism /Mount-Wim /WimFile:"$WinPEDirectory\ISO\sources\boot.wim"
/index:1 /MountDir:"$WinPEDirectory\mount"
}
else {
Mount-WindowsImage -Path "$WinPEDirectory\mount"
-ImagePath "$WinPEDirectory\ISO\sources\boot.wim" -Index 1 | Out-Null
}
Mount the image.
if($OutPut.ToUpper() -eq "PXE") {
# extract PXE boot
if(Test-Path "$WinPEDirectory\mount\Windows\Boot\PXE") {
# Copy
if((Test-Path "$WinPEDirectory\PXE") -ne $true) {
Write-OutPut "Extracting PXE boot files to $WinPEDirectory\PXE"
Copy-Item -Path "$WinPEDirectory\mount\Windows\Boot\PXE"
-Destination "$WinPEDirectory" -Recurse
}
}
else {
# Possible Error Mounting
if($UseAik) {
$stdOut = dism /Unmount-Wim /MountDir:"$WinPEDirectory\mount" /discard 2>&1
}
else {
Dismount-WindowsImage -Path "$WinPEDirectory\mount" -Discard | Out-Null
}
Write-Error "Unable to find mounted files."
return
}
}
Copy PXE files if building for PXE.
$packages = New-Object System.Collections.ArrayList
# AIK & ADK
[void] $packages.Add("winpe-wmi")
[void] $packages.Add("winpe-scripting")
[void] $packages.Add("winpe-hta")
$imagexFile = ""
$packageFiles = ""
$osCd = ""
if($UseAik) {
$imagexFile = "$WinKitDirectory\Tools\$Environment\imagex.exe"
$packageFiles = "$WinKitDirectory\Tools\PETools\$Environment\WinPE_FPs"
$osCd = "$WinKitDirectory\Tools\$dismArchitecture"
}
else {
$imagexFile = "$WinKitDirectory\Deployment Tools\$Environment\DISM\imagex.exe"
$packageFiles = "$WinKitDirectory\Windows Preinstallation Environment\$Environment\WinPE_OCs"
$osCd = "$WinKitDirectory\Deployment Tools\$dismArchitecture\Oscdimg"
[void] $packages.Add("WinPE-NetFX")
[void] $packages.Add("WinPE-PowerShell")
[void] $packages.Add("WinPE-DismCmdlets")
[void] $packages.Add("WinPE-StorageWMI")
[void] $packages.Add("WinPE-EnhancedStorage")
}
Remove any unneeded packages or the image will be very large.
# imagex
if((Test-Path "$WinPEDirectory\mount\Windows\System32\imagex.exe") -ne $true) {
Write-OutPut "Adding imagex.exe"
Copy-Item -Path $imagexFile -Destination "$WinPEDirectory\mount\Windows\System32"
}
if($startnet -ne [String]::Empty) {
Out-File -FilePath "$WinPEDirectory\mount\Windows\System32\startnet.cmd"
-InputObject "$startnet" -Append -Encoding ASCII
}
# Currently Installed Packages
if($UseAik) {
$installedPackages = dism /image:"$WinPEDirectory\mount" /Get-Packages 2>&1
}
else {
# instead of Get-WindowsPackage
$installedPackages = Get-WindowsOptionalFeature -Path "$WinPEDirectory\mount"
}
# Packages
$packages | % {
if($UseAik) {
Install-AikPackage $_ $installedPackages "$WinPEDirectory" "$packageFiles"
}
else {
Install-AdkPackage $_ $installedPackages "$WinPEDirectory" "$packageFiles"
}
}
Installing packages, updating startnet, and copying imagex. I used Get-WindowsOptionalFeature
instead of Get-WindowsPackage
, according to the documentation, it returns broader results.
# Drivers
if($DriverLocation -ne $null)
{
Write-OutPut "Installing Drivers"
foreach($s in $DriverLocation) {
if($s -ne [String]::Empty) {
if((Get-Item "$s").PSIsContainer) {
$files = [System.IO.Directory]::GetFiles("$s")
if($files -ne $null) {
foreach($file in $files) {
if($file.EndsWith("inf")) {
if($UseAik) {
dism /image:"$WinPEDirectory\mount" /Add-Driver /driver:"$file"
}
else {
Add-WindowsDriver -Path "$WinPEDirectory\mount" -Driver "$file" | Out-Null
}
}
}
}
}
else {
# install
if($UseAik) {
dism /image:"$WinPEDirectory\mount" /Add-Driver /driver:"$s"
}
else {
Add-WindowsDriver -Path "$WinPEDirectory\mount" -Driver "$s" | Out-Null
}
}
}
}
}
Driver Install:
# Extras
if($ExtrasPath -ne $null) {
Write-OutPut "Copying additional files"
foreach($f in $ExtrasPath) {
if($f -ne [String]::Empty) {
if((Get-Item "$f").PSIsContainer) {
$files = [System.IO.Directory]::GetFiles("$f")
if($files -ne $null) {
foreach($file in $files) {
Copy-Item -Path "$file" -Destination "$WinPEDirectory\mount\Windows\System32"
}
}
}
else {
Copy-Item -Path "$f" -Destination "$WinPEDirectory\mount\Windows\System32"
}
}
}
}
Extra files:
# Save changes to wim
Write-OutPut "Applying Changes to WinPE"
# Unmount
if($UseAik) {
dism /Unmount-Wim /MountDir:"$WinPEDirectory\mount" /commit
}
else {
Dismount-WindowsImage -Path "$WinPEDirectory\mount" -Save | Out-Null
}
switch ($OutPut.ToUpper()) {
"ISO" {
if(Test-Path "$WinPEDirectory\WinPE_$Environment.iso") {
Remove-Item "$WinPEDirectory\WinPE_$Environment.iso"
}
Create-ISO "$WinPEDirectory" "WinPE_$Environment.iso" "$osCd"
break
}
"USB" {
Create-USB "$WinPEDirectory\ISO"
break
}
"PXE" {
if(Test-Path "$WinPEDirectory\PXE") {
Write-OutPut "Copying files for PXE"
Copy-Item -Path "$WinPEDirectory\ISO"
-Destination "$WinPEDirectory\PXE" -Recurse
[int]$bfSize = ((Get-Item "$WinPEDirectory\PXE\pxeboot.n12").Length / 512)
$h = [String]::Format("0x{0}", $bfSize.ToString("X"))
# Save dhcp pxe settings to file
Out-File -FilePath "$WinPEDirectory\PXE\DHCP_PXE_SETTINGS.txt"
-InputObject "Option 13 $h"
Out-File -FilePath "$WinPEDirectory\PXE\DHCP_PXE_SETTINGS.txt"
-InputObject "Option 67 pxeboot.n12" -Append
}
else {
Write-OutPut "PXE files could not be copied."
}
break
}
default { break }
}
Write-OutPut "------------------------------------------------"
$msg = [DateTime]::Now.ToShortTimeString()
Write-OutPut "Ended Make-WinPE $msg"
Write-OutPut "------------------------------------------------"
}
The finish: create the desired output. For PXE output, I also include an additional dhcp settings file as a reminder for setting PXE.
Using a Form
This section is longer than the rest, covering the form controls and events. This was created using VS and then converted to Powershell using Notepad and find-replace.
function Use-Form
{
Add-Type -AssemblyName 'System.Drawing'
Add-Type -AssemblyName 'System.Windows.Forms'
# Start Form
$form1 = New-Object System.Windows.Forms.Form
$label1 = New-Object System.Windows.Forms.Label
$label2 = New-Object System.Windows.Forms.Label
$label3 = New-Object System.Windows.Forms.Label
$label4 = New-Object System.Windows.Forms.Label
$label5 = New-Object System.Windows.Forms.Label
$label6 = New-Object System.Windows.Forms.Label
$label7 = New-Object System.Windows.Forms.Label
$btnOutput = New-Object System.Windows.Forms.Button
$btnDrivers = New-Object System.Windows.Forms.Button
$btnFiles = New-Object System.Windows.Forms.Button
$btnCancel = New-Object System.Windows.Forms.Button
$btnOK = New-Object System.Windows.Forms.Button
$btnRemoveDriver = New-Object System.Windows.Forms.Button
$btnRemoveFile = New-Object System.Windows.Forms.Button
$txtOutPut = New-Object System.Windows.Forms.TextBox
$txtStartnet = New-Object System.Windows.Forms.TextBox
$cmbOutPut = New-Object System.Windows.Forms.ComboBox
[System.Windows.Forms.ListBox]$lstDrivers = New-Object System.Windows.Forms.ListBox
$groupBox1 = New-Object System.Windows.Forms.GroupBox
$cmbDisk = New-Object System.Windows.Forms.ComboBox
[System.Windows.Forms.ListBox]$lstFiles = New-Object System.Windows.Forms.ListBox
$cmbEnv = New-Object System.Windows.Forms.ComboBox
# Added for ADK
$gb = New-Object System.Windows.Forms.GroupBox
$rdoAik = New-Object System.Windows.Forms.RadioButton
$rdoAdk = New-Object System.Windows.Forms.RadioButton
$lblInsRoot = New-Object System.Windows.Forms.Label
$txtInsRoot = New-Object System.Windows.Forms.TextBox
$btnInsRoot = New-Object System.Windows.Forms.Button
$chkPrevious = New-Object System.Windows.Forms.CheckBox
Most important here is Add-Type
to load the required assemblies for the form and dialogs:
# Update
$yOffset = 120
$label1.AutoSize = $true
$label1.Location = New-Object System.Drawing.Point(12, (9 + $yOffset))
$label1.Size = New-Object System.Drawing.Size(84, 13)
$label1.Text = "Output Directory"
$label2.AutoSize = $true
$label2.Location = New-Object System.Drawing.Point(12, (40 + $yOffset))
$label2.Size = New-Object System.Drawing.Size(66, 13)
$label2.Text = "Output Type"
$label3.AutoSize = $true
$label3.Location = New-Object System.Drawing.Point(12, (147 + $yOffset))
$label3.Size = New-Object System.Drawing.Size(40, 13)
$label3.Text = "Drivers"
$label4.AutoSize = $true
$label4.Location = New-Object System.Drawing.Point(7, 20) # (20 + $yOffset))
$label4.Size = New-Object System.Drawing.Size(61, 13)
$label4.Text = "Disk Select"
$label5.AutoSize = $true
$label5.Location = New-Object System.Drawing.Point(15, (258 + $yOffset))
$label5.Size = New-Object System.Drawing.Size(77, 13)
$label5.Text = "Additional Files"
$label6.AutoSize = $true
$label6.Location = New-Object System.Drawing.Point(307, (40 + $yOffset))
$label6.Size = New-Object System.Drawing.Size(66, 13)
$label6.Text = "Environment"
$label7.AutoSize = $true
$label7.Location = New-Object System.Drawing.Point(18, (360 + $yOffset))
$label7.Size = New-Object System.Drawing.Size(94, 13)
$label7.Text = "Startnet Command"
When the ADK use was introduced, some of the controls needed to move.
# Disks
$cmbDisk.FormattingEnabled = $true
$cmbDisk.Location = New-Object System.Drawing.Point(85, 20) #(20 + $yOffset))
$cmbDisk.Size = New-Object System.Drawing.Size(325, 21)
$cmbDisk.TabIndex = 1
# $cmbDisk.Text = "Disk 1"
$groupBox1.Controls.Add($cmbDisk)
$groupBox1.Controls.Add($label4)
$groupBox1.Location = New-Object System.Drawing.Point(103, (67 + $yOffset))
$groupBox1.Size = New-Object System.Drawing.Size(418, 59)
$groupBox1.TabStop = $false
$groupBox1.Text = "DISKPART"
$groupBox1.Visible = $false
$txtOutPut.Location = New-Object System.Drawing.Point(110, (9 + $yOffset))
$txtOutPut.Size = New-Object System.Drawing.Size(410, 20)
$txtOutPut.TabIndex = 1
$txtStartnet.Location = New-Object System.Drawing.Point(130, (360 + $yOffset))
$txtStartnet.Size = New-Object System.Drawing.Size(390, 20)
$txtStartnet.TabIndex = 19
$lstDrivers.FormattingEnabled = $true
$lstDrivers.Location = New-Object System.Drawing.Point(110, (147 + $yOffset))
$lstDrivers.Name = "lstDrivers"
$lstDrivers.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiSimple
$lstDrivers.Size = New-Object System.Drawing.Size(410, 95)
$lstDrivers.TabIndex = 6
$lstFiles.FormattingEnabled = $true
$lstFiles.Location = New-Object System.Drawing.Point(110, (258 + $yOffset))
$lstFiles.Name = "lstFiles"
$lstFiles.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiSimple
$lstFiles.Size = New-Object System.Drawing.Size(410, 95)
$lstFiles.TabIndex = 10
$btnOutput.Location = New-Object System.Drawing.Point(528, (9 + $yOffset))
$btnOutput.Name = "btnOutput"
$btnOutput.Size = New-Object System.Drawing.Size(85, 23)
$btnOutput.TabIndex = 2
$btnOutput.Text = "Browse"
$btnOutput.UseVisualStyleBackColor = $true
$btnOutput.Add_Click(
{
$txtOutPut.Text = Get-Directory "WinPE Output Directory"
})
$btnDrivers.Location = New-Object System.Drawing.Point(528, (147 + $yOffset))
$btnDrivers.Name = "btnDrivers"
$btnDrivers.Size = New-Object System.Drawing.Size(85, 23)
$btnDrivers.TabIndex = 7
$btnDrivers.Text = "Add Drivers"
$btnDrivers.UseVisualStyleBackColor = $true
$btnDrivers.Add_Click(
{
[System.Windows.Forms.OpenFileDialog] $ofd = New-Object System.Windows.Forms.OpenFileDialog
if ($IsSTA -ne $true) {
$ofd.AutoUpgradeEnabled = $true
$ofd.ShowHelp = $true # Needed if not using ISE
}
$ofd.Filter = "Setup File (*.inf)|*.inf"
$ofd.Multiselect = $true
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
foreach ($s in $ofd.FileNames) {
$lstDrivers.Items.Add($s)
}
}
})
Using dialogs from Powershell--notably, the FolderBrowserDialog
.
$btnFiles.Location = New-Object System.Drawing.Point(528, (258 + $yOffset))
$btnFiles.Name = "btnFiles"
$btnFiles.Size = New-Object System.Drawing.Size(85, 23)
$btnFiles.TabIndex = 11
$btnFiles.Text = "Add Files"
$btnFiles.UseVisualStyleBackColor = $true
$btnFiles.Add_Click(
{
[System.Windows.Forms.OpenFileDialog] $ofd = New-Object System.Windows.Forms.OpenFileDialog
if ($IsSTA -ne $true) {
$ofd.AutoUpgradeEnabled = $true
$ofd.ShowHelp = $true # Needed if not using ISE
}
$ofd.Filter = "All Files (*.*)|*.*"
$ofd.Multiselect = $true
if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
foreach ($s in $ofd.FileNames) {
$lstFiles.Items.Add($s);
}
}
})
The second (and last) FolderBrowserDialog
.
$btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$btnCancel.Location = New-Object System.Drawing.Point(527, (395 + $yOffset))
$btnCancel.Name = "btnCancel"
$btnCancel.Size = New-Object System.Drawing.Size(85, 23)
$btnCancel.TabIndex = 12
$btnCancel.Text = "Cancel"
$btnCancel.UseVisualStyleBackColor = $true
$btnOK.Location = New-Object System.Drawing.Point(440, (395 + $yOffset))
$btnOK.Name = "btnOK"
$btnOK.Size = New-Object System.Drawing.Size(85, 23)
$btnOK.TabIndex = 13
$btnOK.Text = "OK"
$btnOK.UseVisualStyleBackColor = $true
$btnOK.Add_Click(
{
$form1.DialogResult = [System.Windows.Forms.DialogResult]::OK
})
$btnRemoveDriver.Location = New-Object System.Drawing.Point(528, (177 + $yOffset))
$btnRemoveDriver.Name = "btnRemoveDriver"
$btnRemoveDriver.Size = New-Object System.Drawing.Size(85, 23)
$btnRemoveDriver.TabIndex = 14
$btnRemoveDriver.Text = "Remove"
$btnRemoveDriver.UseVisualStyleBackColor = $true
$btnRemoveDriver.Add_Click(
{
$items = $lstDrivers.SelectedItems
while($items -ne $null) {
$lstDrivers.Items.Remove($items[0])
$items = $lstDrivers.SelectedItems
}
})
$btnRemoveFile.Location = New-Object System.Drawing.Point(527, (287 + $yOffset))
$btnRemoveFile.Name = "btnRemoveFile"
$btnRemoveFile.Size = New-Object System.Drawing.Size(85, 23)
$btnRemoveFile.TabIndex = 15
$btnRemoveFile.Text = "Remove"
$btnRemoveFile.UseVisualStyleBackColor = $true
$btnRemoveFile.Add_Click(
{
$items = $lstFiles.SelectedItems
while($items -ne $null) {
$lstFiles.Items.Remove($items[0])
$items = $lstFiles.SelectedItems
}
})
$cmbOutPut.FormattingEnabled = $true
$cmbOutPut.Items.AddRange(("ISO","USB","PXE"))
$cmbOutPut.Location = New-Object System.Drawing.Point(110, (40 + $yOffset))
$cmbOutPut.Size = New-Object System.Drawing.Size(121, 21)
$cmbOutPut.TabIndex = 4
$cmbOutPut.Text = "NONE"
$cmbOutPut.Add_SelectedIndexChanged(
{
if ($cmbOutPut.Text.ToUpper() -eq "USB") {
if($groupBox1.Visible -eq $false) {
# First Show: Get Disks
Show-Message "Please wait while the disks are listed. This may take a while." | Out-Null
$w32dd = gwmi -List | Where-Object -FilterScript {$_.Name -eq "Win32_DiskDrive"}
$disks = $w32dd.GetInstances()
$disks | % {
$size = [Math]::Round(($_.Size / [Math]::Pow(2, 20)))
if($size -gt 1024) {
$size = ([Math]::Round(($_.Size / [Math]::Pow(2, 30)))).ToString() + " GB"
}
else {
$size = $size.ToString() + " MB"
}
$cmbDisk.Items.Add([String]::Format("{0}-{1} - {2}", $_.Index, $size, $_.Caption))
}
if($cmbDisk.Items.Count -lt 2)
{
Show-Message "No USB drive detected." | Out-Null
}
else
{
$cmbDisk.SelectedIndex = 1
}
}
$groupBox1.Visible = $true
}
else {
$groupBox1.Visible = $false
}
})
This may warrant some explanation. Sizes are just exponents of 2: KB = 2^10 (1024), MB = 2^20 (1024 * 1024), and so on. Using Pow is just a shortcut to determining the MB/GB value.
$cmbEnv.FormattingEnabled = $true
$cmbEnv.Items.AddRange(("x86","i64"))
$cmbEnv.Location = New-Object System.Drawing.Point(400, (40 + $yOffset))
$cmbEnv.Size = New-Object System.Drawing.Size(121, 21)
$cmbEnv.TabIndex = 4
$cmbEnv.Text = "amd64"
$toolTip = New-Object System.Windows.Forms.ToolTip
$toolTip.SetToolTip($txtStartnet, "Command executed after wpeinit. Example: wscript script.vbs")
$toolTip.SetToolTip($cmbEnv, "Processor architecture")
$toolTip.SetToolTip($cmbOutPut, "Output format")
$toolTip.SetToolTip($lstFiles, "Scripts and executables to be added to WinPE. (Imagex is added by default)")
$toolTip.SetToolTip($lstDrivers, "Drivers to add to WinPE. Example: network drivers")
$toolTip.SetToolTip($txtOutPut, "WinPE output directory")
$toolTip.SetToolTip($cmbDisk, "USB disk")
# Finish Form
$form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13) #6, 13
$form1.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
$form1.CancelButton = $btnCancel
$form1.ClientSize = New-Object System.Drawing.Size(626, 550) #430
#update
$gb.Size = New-Object System.Drawing.Size(606, 102)
$gb.Location = New-Object System.Drawing.Point(13, 13)
$gb.Text = "WinPE Source"
$rdoAik.AutoSize = $true
$rdoAdk.AutoSize = $true
$lblInsRoot.AutoSize = $true
$chkPrevious.AutoSize = $true
if($psVersionMajor -lt 4) {
$rdoAik.Checked = $true
$rdoAdk.Enabled = $false
}
else {
# Must have powershell 4 to use dism import (otherwise default is wrong dism version)
$rdoAdk.Checked = $true
}
$rdoAdk.Text = "ADK"
$rdoAik.Text = "AIK"
$lblInsRoot.Text = "Root (Assessment and Deployment Kit for ADK or Windows AIK for AIK)"
$btnInsRoot.Text = "Browse..."
$chkPrevious.Text = "Update an existing image"
$txtInsRoot.Text = $defDir
$rdoAdk.UseVisualStyleBackColor = $true
$rdoAik.UseVisualStyleBackColor = $true
$chkPrevious.UseVisualStyleBackColor = $true
$btnInsRoot.UseVisualStyleBackColor = $true
$rdoAik.Location = New-Object System.Drawing.Point(6, 69)
$rdoAdk.Location = New-Object System.Drawing.Point(62, 69)
$lblInsRoot.Location = New-Object System.Drawing.Point(6, 21)
$txtInsRoot.Location = New-Object System.Drawing.Point(6, 41)
$btnInsRoot.Location = New-Object System.Drawing.Point(513, 41)
$chkPrevious.Location = New-Object System.Drawing.Point(141, 69)
$rdoAdk.Size = New-Object System.Drawing.Size(57, 21)
$rdoAik.Size = New-Object System.Drawing.Size(50, 21)
$lblInsRoot.Size = New-Object System.Drawing.Size(259, 17)
$txtInsRoot.Size = New-Object System.Drawing.Size(500, 22)
$btnInsRoot.Size = New-Object System.Drawing.Size(75, 30)
$chkPrevious.Size = New-Object System.Drawing.Size(189, 21)
$btnInsRoot.Add_Click(
{
$insPath = Get-Directory -WindowCaption "Kit Installation Root" # -StartDirectory {$env:ProgramFiles}
$txtInsRoot.Text = $insPath
})
$gb.Controls.Add($rdoAik)
$gb.Controls.Add($rdoAdk)
$gb.Controls.Add($lblInsRoot)
$gb.Controls.Add($txtInsRoot)
$gb.Controls.Add($btnInsRoot)
$gb.Controls.Add($chkPrevious)
$form1.Controls.Add($gb)
$form1.Controls.Add($btnRemoveFile)
$form1.Controls.Add($btnRemoveDriver)
$form1.Controls.Add($btnOK)
$form1.Controls.Add($btnCancel)
$form1.Controls.Add($btnOutput)
$form1.Controls.Add($btnFiles)
$form1.Controls.Add($btnDrivers)
$form1.Controls.Add($lstFiles)
$form1.Controls.Add($groupBox1)
$form1.Controls.Add($lstDrivers)
$form1.Controls.Add($cmbOutPut)
$form1.Controls.Add($txtOutPut)
$form1.Controls.Add($txtStartnet)
$form1.Controls.Add($label1)
$form1.Controls.Add($label2)
$form1.Controls.Add($label3)
$form1.Controls.Add($label5)
$form1.Controls.Add($label6)
$form1.Controls.Add($label7)
$form1.Controls.Add($cmbEnv)
$form1.Name = "Form1"
$form1.Text = "WinPE"
# Display
$form1.Add_Shown({$form1.Activate})
$result = $form1.ShowDialog()
if($result -eq [System.Windows.Forms.DialogResult]::OK) {
if($cmbDisk.Text -ne [String]::Empty) {
$selDisk = $cmbDisk.Text.Substring(0, 1).Trim()
if($selDisk -eq "0") {
Show-Message "Invalid Disk Selection." | Out-Null
return
}
$cmbDisk.Text = $selDisk
}
if($rdoAik.Checked) {
if($chkPrevious.Checked) {
Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text
-DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items
-OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text -startnet $txtStartnet.Text
-WinKitDirectory $txtInsRoot.Text -UseAik -useExisting
}
else {
Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text
-DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items
-OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text -startnet $txtStartnet.Text
-WinKitDirectory $txtInsRoot.Text -UseAik
}
}
else {
if($chkPrevious.Checked) {
Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text
-DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items
-OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text
-startnet $txtStartnet.Text -WinKitDirectory $txtInsRoot.Text -useExisting
}
else {
Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text
-DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items
-OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text
-startnet $txtStartnet.Text -WinKitDirectory $txtInsRoot.Text
}
}
}
}
Showing the form and executing the Main
function.
if((Has-Role) -ne $true) {
Write-OutPut "Elevated permissions are required to run DISM."
Write-OutPut "Use an elevated command to complete these tasks."
return
}
if($PeDirectory -ne $null -and $PeDirectory -ne [String]::Empty)
{
Main -WinPEDirectory $PeDirectory -Environment $PeEnvironment
-DriverLocation $PeDrivers -ExtrasPath $PeExtras -OutPut $PeOutPut
-DiskId $PeDiskId -WinKitDirectory $defDir
}
else
{
# Display Form
Use-Form
}
Script execution: If PeDirectory
is blank, then the form is displayed; otherwise it assumes parameters were passed. Note that defDir
is used for WinKitDirectory
, I will reiterate the importance of setting this when running this from command line.
The code can be copied from this article to produce a fully functioning script. (The attached script is not exactly the same, the code posted here was edited for content.)
History
- April 27, 2014 - Original posting