Lib/WindowsImage.ps1

function ExpandWindowsImage {
<#
    .SYNOPSIS
        Writes a .wim image to a mounted VHD/(X) file.
#>

    [CmdletBinding(DefaultParameterSetName = 'Index')]
    param (
        ## File path to WIM file or ISO file containing the WIM image
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $MediaPath,
        
        ## WIM image index to apply
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Index')]
        [System.Int32] $WimImageIndex,
        
        ## WIM image name to apply
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Name')] [ValidateNotNullOrEmpty()]
        [System.String] $WimImageName,
        
        ## Mounted VHD(X) Operating System disk image
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()]
        [System.Object] $Vhd, # Microsoft.Vhd.PowerShell.VirtualHardDisk
        
        ## Disk image partition scheme
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateSet('MBR','GPT')]
        [System.String] $PartitionStyle,
        
        ## Optional Windows features to add to the image after expansion (ISO only)
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()]
        [System.String[]] $WindowsOptionalFeature,
        
        ## Optional Windows features source path
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $SourcePath = '\sources\sxs',

        ## Relative source WIM file path (only used for ISOs)
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $WimPath = '\sources\install.wim',

        ## Optional Windows packages to add to the image after expansion (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNull()]
        [System.String[]] $Package,
        
        ## Relative packages (.cab) file path (primarily used for Nano Server)
        [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $PackagePath = '\packages'
    )
    process {
        ## Assume the media path is a literal path to a WIM file
        $windowsImagePath = $MediaPath;
        $mediaFileInfo = Get-Item -Path $MediaPath;

        if ($mediaFileInfo.Extension -eq '.ISO') {
            ## Mount ISO
            WriteVerbose ($localized.MountingDiskImage -f $MediaPath);
            $iso = Mount-DiskImage -ImagePath $MediaPath -StorageType ISO -Access ReadOnly -PassThru -Verbose:$false;
            $iso = Get-DiskImage -ImagePath $iso.ImagePath;
            $isoDriveLetter = $iso | Get-Volume | Select-Object -ExpandProperty DriveLetter;
            ## Update the media path to point to the mounted ISO
            $windowsImagePath = '{0}:{1}' -f $isoDriveLetter, $WimPath;
        }

        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            ## Locate the image index
            $WimImageIndex = GetWindowsImageIndex -ImagePath $windowsImagePath -ImageName $WimImageName;
        }
        
        if ($PartitionStyle -eq 'MBR') { $partitionType = 'IFS'; }
        elseif ($PartitionStyle -eq 'GPT') { $partitionType = 'Basic'; }
        $vhdDriveLetter = GetDiskImageDriveLetter -DiskImage $Vhd -PartitionType $partitionType;
        
        $logName = '{0}.log' -f [System.IO.Path]::GetFileNameWithoutExtension($Vhd.Path);
        $logPath = Join-Path -Path $env:TEMP -ChildPath $logName;
        WriteVerbose ($localized.ApplyingWindowsImage -f $WimImageIndex, $Vhd.Path);
        $expandWindowsImage = @{
            ImagePath = $windowsImagePath;
            ApplyPath = '{0}:\' -f $vhdDriveLetter;
            LogPath = $logPath;
            Index = $WimImageIndex;
        }
        $dismOutput = Expand-WindowsImage @expandWindowsImage -Verbose:$false;

        [ref] $null = Get-PSDrive;

        ## Add additional packages (.cab) files
        if ($Package) {
            ## Default to relative package folder path
            $addWindowsPackageParams = @{
                PackagePath = '{0}:{1}' -f $isoDriveLetter, $PackagePath;
                DestinationPath = '{0}:\' -f $vhdDriveLetter;
                LogPath = $logPath;
                Package = $Package;
            }
            if (-not $PackagePath.StartsWith('\')) {
                ## Use the specified/literal path
                $addWindowsPackageParams['PackagePath'] = $PackagePath;
            }
            $dismOutput = AddWindowsPackage @addWindowsPackageParams;
        } #end if Package
        
        ## Add additional features if required
        if ($WindowsOptionalFeature) {
            ## Default to ISO relative source folder path
            $addWindowsOptionalFeatureParams = @{
                ImagePath = '{0}:{1}' -f $isoDriveLetter, $SourcePath;
                DestinationPath = '{0}:\' -f $vhdDriveLetter;
                LogPath = $logPath;
                WindowsOptionalFeature = $WindowsOptionalFeature;
            }
            if ($mediaFileInfo.Extension -eq '.WIM') {
                ## The Windows optional feature source path for .WIM files is a literal path
                $addWindowsOptionalFeatureParams['ImagePath'] = $SourcePath;
            }
            $dismOutput = AddWindowsOptionalFeature @addWindowsOptionalFeatureParams;
        } #end if WindowsOptionalFeature

        if ($mediaFileInfo.Extension -eq '.ISO') {
            ## Dismount ISO
            WriteVerbose ($localized.DismountingDiskImage -f $MediaPath);
            Dismount-DiskImage -ImagePath $MediaPath;
        }
    } #end process
} #end function ExpandWindowsImage

function AddWindowsOptionalFeature {
<#
    .SYMOPSIS
        Enables Windows optional features to an image.
#>

    [CmdletBinding()]
    param (
        ## Source package file path
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,
        
        ## Mounted VHD(X) Operating System disk drive
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,
        
        ## Windows packages to add to the image after expansion
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()]
        [System.String[]] $WindowsOptionalFeature,
        
        ## DISM log path
        [Parameter()] [ValidateNotNullOrEmpty()]
        [System.String] $LogPath = $DestinationPath
    )
    process {
        WriteVerbose ($localized.AddingWindowsFeature -f ($WindowsOptionalFeature -join ','), $DestinationPath);
        $enableWindowsOptionalFeatureParams = @{
            Source = $ImagePath;
            Path = $DestinationPath;
            LogPath = $LogPath;
            FeatureName = $WindowsOptionalFeature;
            LimitAccess = $true;
            All = $true;
        }
        $dismOutput = Enable-WindowsOptionalFeature @enableWindowsOptionalFeatureParams -Verbose:$false;
    } #end process
} #end function AddDiskImageOptionalFeature

function AddWindowsPackage {
<#
    .SYNOPSIS
        Adds a Windows package to an image.
#>

    ## Source ISO install.wim
    [CmdletBinding()]
    param (
        ## Windows packages (.cab) files to add to the image after expansion
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNull()]
        [System.String[]] $Package,

        ## Path to the .cab files
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $PackagePath,
        
        ## Mounted VHD(X) Operating System disk drive
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,
        
        ## DISM log path
        [Parameter()] [ValidateNotNullOrEmpty()]
        [System.String] $LogPath = $DestinationPath
    )
    process {
        foreach ($packageName in $Package) {
            WriteVerbose ($localized.AddingWindowsPackage -f $packagename, $DestinationPath);
            $packageFilename = '{0}.cab' -f $packageName;
            $packageFilePath = Join-Path -Path $PackagePath -ChildPath $packageFilename;
            AddDiskImagePackage -Name $packageName -Path $packageFilePath -DestinationPath $DestinationPath;
        } #end foreach package
    } #end process
} #end function AddWindowsPackage

function GetWindowsImageIndex {
<#
    .SYNOPSIS
        Locates the specified WIM image index by its name, i.e. SERVERSTANDARD or SERVERDATACENTERSTANDARD
    .OUTPUTS
        The WIM image index.
#>

    [CmdletBinding()]
    [OutputType([System.Int32])]
    param (
        # WIM image path
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,
        
        # Windows image name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.String] $ImageName
    )
    process {
        WriteVerbose ($localized.LocatingWimImageIndex -f $ImageName);
        Get-WindowsImage -ImagePath $ImagePath -Verbose:$false |
            Where-Object ImageName -eq $ImageName |
                Select-Object -ExpandProperty ImageIndex;
    } #end process
} #end function GetWindowsImageIndex

function GetWindowsImageName {
<#
    .SYNOPSIS
        Locates the specified WIM image name by its index.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        # WIM image path
        [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()]
        [System.String] $ImagePath,
        
        # Windows image index
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()]
        [System.Int32] $ImageIndex
    )
    process {
        WriteVerbose ($localized.LocatingWimImageName -f $ImageIndex);
        Get-WindowsImage -ImagePath $ImagePath -Verbose:$false |
            Where-Object ImageIndex -eq $ImageIndex |
                Select-Object -ExpandProperty ImageName;
    } #end process
} #end function GetWindowsImageName