Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

Build WinPE using Windows Powershell

4.75/5 (3 votes)
27 Apr 2014CPOL6 min read 21.6K  
Building WinPEs from AIK/ADK with Powershell

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 built
  • Environment: WinPE environment
  • DriverLocation: A directory of drivers or string[] of driver inf files
  • ExtrasPath: A directory containing additional files to be added or a string[] of files
  • OutPut: Type of output is PXE, USB, Disc, or None
  • DiskId: Disc ID of USB
  • startnet: The command to add to startnet such as an batch file to run
  • WinKitDirectory: AIK or ADK install directory
  • UseAik: Default is ADK, this is set to use AIK
  • useExisting: 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)