Wimsimple.ps1


<#PSScriptInfo
 
.VERSION 1.0
 
.GUID 0ace940e-e8f4-4dc3-91f0-7bf35a3d283e
 
.AUTHOR Liam Robinson
 
.COMPANYNAME
 
.COPYRIGHT All rights reserved. - Liam Robinson. This script should not be republished in any way, shape or form without written consent.
 
.TAGS Windows11 WindowsImage Windows OSD
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 WIMSimple for W11. Streamline the deployment of Windows 11 by removing unwanted apps, indexes and adding drivers to your Windows 11 Image file using one handy script.
 
#>
 

Param()


Function Check-RunAsAdministrator()
{
  #Get current user context
  $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
  
  #Check user is running the script is member of Administrator Group
  if($CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))
  {
       Write-Output "Script is running with Administrator privileges!"
  }
  else
    {
       Write-Output "Script is running without Administrator privileges, this is needed to launch DISM."
       Write-Output "We will now restart and launch as Admin.."
       
       Start-Sleep -Seconds 5
       #Create a new Elevated process to Start PowerShell
       $ElevatedProcess = New-Object System.Diagnostics.ProcessStartInfo "PowerShell";
 
       # Specify the current script path and name as a parameter
       $ElevatedProcess.Arguments = "& '" + $script:MyInvocation.MyCommand.Path + "'"
 
       #Set the Process to elevated
       $ElevatedProcess.Verb = "runas"
 
       #Start the new elevated process
       [System.Diagnostics.Process]::Start($ElevatedProcess)
 
       #Exit from the current, unelevated, process
       Exit
 
    }
}
 
#Check Script is running with Elevated Privileges
Check-RunAsAdministrator
 
#Begin Script

Write-Output @"
 
░██╗░░░░░░░██╗██╗███╗░░░███╗░██████╗██╗███╗░░░███╗██████╗░██╗░░░░░███████╗
░██║░░██╗░░██║██║████╗░████║██╔════╝██║████╗░████║██╔══██╗██║░░░░░██╔════╝
░╚██╗████╗██╔╝██║██╔████╔██║╚█████╗░██║██╔████╔██║██████╔╝██║░░░░░█████╗░░
░░████╔═████║░██║██║╚██╔╝██║░╚═══██╗██║██║╚██╔╝██║██╔═══╝░██║░░░░░██╔══╝░░
░░╚██╔╝░╚██╔╝░██║██║░╚═╝░██║██████╔╝██║██║░╚═╝░██║██║░░░░░███████╗███████╗
░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░░░░╚═╝╚═════╝░╚═╝╚═╝░░░░░╚═╝╚═╝░░░░░╚══════╝╚══════╝
 
█▄▄ █▄█   ▄▄   █░░ █ ▄▀█ █▀▄▀█   █▀█ █▀█ █▄▄ █ █▄░█ █▀ █▀█ █▄░█ 
█▄█ ░█░   ░░   █▄▄ █ █▀█ █░▀░█   █▀▄ █▄█ █▄█ █ █░▀█ ▄█ █▄█ █░▀█  
"@


###########################
###########################
# Create working directory
###########################
###########################
$WimDir = "C:\WIMSIMPLE\WIM"
$StageDir = "C:\WIMSIMPLE\STAGING"

Write-Output ""
Write-Output "We will now try to create an app directory in C:\WIMSIMPLE\"
Start-Sleep -Seconds 3

Write-Output ""

## Creat WIM Folder

if (!(Test-Path $WimDir)) {
    New-Item -ItemType Directory -Path $WimDir | Out-Null
    Write-Output "$WimDir created successfully."
} else {
    Write-Output "$Wimdir already exists."
}

Write-Output ""
## Create Staging Folder

if (!(Test-Path $StageDir)) {
    New-Item -ItemType Directory -Path $StageDir | Out-Null
    Write-Output "$StageDir created successfully."
} else {
    Write-Output "$StageDir already exists."
}

Write-Output ""
Write-Output "Importing DISM Module"
Import-Module -Name Dism

###########################
###########################
##### Choose ISO File #####
###########################
###########################

Write-Output ""
Write-Output "You now need to select your W11 ISO File"
Write-Output ""
pause
# Function to open file browser dialog
function Open-FileBrowserDialog {
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null

    $fileBrowserDialog = New-Object System.Windows.Forms.OpenFileDialog
    $fileBrowserDialog.Filter = "ISO files (*.iso)|*.iso"
    $fileBrowserDialog.Title = "Select ISO File"
    $fileBrowserDialog.ShowDialog() | Out-Null

    return $fileBrowserDialog.FileName
}

# Prompt until a file is selected
do {
    $selectedFile = Open-FileBrowserDialog
    if ([string]::IsNullOrWhiteSpace($selectedFile)) {
        Write-Output "No file selected. Please try again."
        pause
    }
} while ([string]::IsNullOrWhiteSpace($selectedFile))

Write-Output "Selected file: $selectedFile"

###########################
###########################
###########################
###########################

$iso = $selectedFile

# Extract just the filename without the directory path
$ISOName = Split-Path $iso -Leaf

Write-Output ""
Write-Output "Mounting ISO - $ISOName..."



##Mount Selected File
$Mount = Mount-DiskImage -ImagePath $iso

## Get Drive Letter

$driveLetter = ($Mount | Get-Volume).DriveLetter
Write-Output ""
Write-Output "ISO mounted to $driveletter"
Write-Output ""
Write-Output "Begining to extract WIM File from ISO"
Write-Output ""

######## Create directory based on current date and time (To avoid wim files clashing) ######
## Get current date and time ##

$currentDateTime = Get-Date

## Format the date and time as a timestamp ##

$timestamp = $currentDateTime.ToString("dd-MM-yyyy_HHmmss")

## Create Unique Directory based on time

New-Item -ItemType Directory -Force -Path C:\WIMSIMPLE\WIM\$timestamp | Out-Null

$dest = $WimDir + "\" + $timestamp 

##import copying tool and copy from ISO to WIM Folder ##

Import-Module BitsTransfer
Start-BitsTransfer -Source "${driveLetter}:\sources\install.wim" -Destination $dest -Description "Extracting WIM file to $dest" -DisplayName "Extracting WIM file from ISO"

# Remove Read-Only Attribute
Set-ItemProperty -Path $dest\install.wim -Name IsReadOnly -Value $false

##Dismount ISO File

Write-Output "Attempting to dismount ISO"
Write-Output ""

try {
    # Unmount the disk image
    Dismount-DiskImage -ImagePath $iso -ErrorAction Stop | Out-Null
    
    # If no error occurred, the unmounting was successful
    Write-Output "ISO unmounting was successful."
} catch {
    # If an error occurred, the unmounting failed
    Write-Output "Disk image unmounting failed. Error: $_"
}


Write-Output ""
Write-Output "Checking WIM File for Multiple Operating Systems"
Write-Output ""

## Get info from mounted image
Start-Sleep -Seconds 2

# Get the image indexes from the WIM file
$imageInfo = Get-WindowsImage -ImagePath $dest\install.wim

# Specify Wim Path
$OriginalWimPath = $dest + "\install.wim"
$NewWimPath = $dest + "\modified" + "\install.wim"

# Display available image indexes to the user
Write-Output "Available Image Indexes:"
Write-Output ""
for ($i = 0; $i -lt $imageInfo.Count; $i++) {
    $index = $i + 1  # Adjust index to start from 1
    Write-Output "$index. $($imageInfo[$i].ImageName)"
}


# Prompt the user to select an image index
do {
    Write-Output ""
    $selectedIndex = Read-Host "Enter the index number of the image you want to mount"
} while (-not ([int]::TryParse($selectedIndex, [ref]$null)))

$selectedIndex = [int]$selectedIndex

if ($selectedIndex -ge 0 -and $selectedIndex -lt $imageInfo.Count) {
    Write-Output ""
    Write-Output "Checking $StageDir for already mounted images, we will remove them if found"
    Write-Output ""
    # Get the contents of the mount directory
    $contents = Get-ChildItem -Path $StageDir

    # Check if the directory is empty
    if ($contents.Count -eq 0) {
        Write-Output ""
        Write-Output "No image mounted..continuing"
        Write-Output ""
    } else {
        Write-Output ""
        Write-Output "The staging directory already contains a mounted image."
        Write-Output ""
        Write-Output "Attempting to unmount image"
        Dismount-WindowsImage -Path $StageDir -Discard
        #dism /Unmount-Wim /Mountdir:$StageDir /discard
        Write-Output ""
    }

    # Prompt the user if they want to copy the selected index
    do {
        $copySelectedOnly = Read-Host "Do you want to reduce WIM filesize by removing all other indexes? (Y/N)"
    } while ($copySelectedOnly -notmatch "^[yn]$")

    if ($copySelectedOnly -eq "Y" -or $copySelectedOnly -eq "y") {
        # Copy selected index to a new WIM file
        $Moddest = $Dest + "\MODIFIED"
        $wimpath = $NewWimPath
        New-Item -ItemType Directory -Path $Moddest | Out-Null
        Write-Output ""
        Write-Output "Exporting index $selectedIndex to new WIM File - $NewWimPath"
        Write-Output ""
        Export-WindowsImage -SourceImagePath $OriginalWimPath -SourceIndex $selectedIndex -DestinationImagePath $NewWimPath

        Write-Output "Selected index copied to $NewWimPath"
        Write-Output ""
        Write-Output "Mounting WIM File - $NewWimPath"
        # Mount the new WIM file
        Mount-WindowsImage -ImagePath $NewWimPath -Index 1 -Path $StageDir

        # Display the image name of the selected index
        Write-Output "Mounted image: $($imageInfo[$selectedIndex - 1].ImageName)"
    } else {
        # Mount the original WIM file
        $wimpath = $OriginalWimPath
        Write-Output ""
        Write-Output "Mounting WIM File - $OriginalWimPath"
        Mount-WindowsImage -ImagePath $OriginalWimPath -Index $selectedIndex -Path $StageDir

        # Display the image name of the selected index
        Write-Output "Mounted image: $($imageInfo[$selectedIndex - 1].ImageName)"
    }

    if ($?) {
        Write-Output "Image mounted successfully at $StageDir"
    } else {
        Write-Output "There was an error mounting the image. Please check the staging folder and try again."
        Write-Output "You can use 'Get-WindowsImage -Mounted' CMDlet to check if an image is already mounted."
        Write-Output "Use 'dism /Unmount-Wim /Mountdir:C:\WIMSIMPLE\STAGING /discard' to manually unmount fully and try the script again."
    }
} else {
    Write-Output "Invalid index number. Please enter a valid index number."
}

###################-------------------########################
################### REMOVE APPS ########################
###################-------------------########################

# Function to remove provisioned app by name
function Remove-ProvisionedAppByName {
    param(
        [string[]]$Patterns,
        [string]$MountPath,
        [array]$ProvisionedApps
    )
    foreach ($Pattern in $Patterns) {
        foreach ($App in $ProvisionedApps) {
            if ($App.DisplayName -like "*$Pattern*") {
                Write-Output "Removing $($App.DisplayName)..."
                Remove-AppxProvisionedPackage -Path $MountPath -PackageName $App.PackageName | Out-Null
            }
        }
    }
}

# Function to list provisioned apps and ask the user if they want to remove them
function Remove-ProvisionedAppsInteractively {
    param(
        [string]$MountPath,
        [array]$ProvisionedApps
    )
    foreach ($App in $ProvisionedApps) {
        $Response = Read-Host "Remove $($App.DisplayName)? (Y/N)"
        if ($Response -eq 'Y' -or $Response -eq 'y') {
            Write-Output "Removing $($App.DisplayName)..."
            Remove-AppxProvisionedPackage -Path $MountPath -PackageName $App.PackageName | out-null
        }
    }
}

# Get a list of provisioned apps
$ProvisionedApps = Get-AppxProvisionedPackage -Path $StageDir

# Offer user options
$validOption = $false
while (-not $validOption) {
    Write-Output ""
    Write-Output "Remove unwanted apps"
    Write-Output ""
    Write-Output "1. Optimise for Education - will automatically remove a pre-set list of apps (see website for details)"
    Write-Output "2. Manually remove each app 1-by-1"
    Write-Output "3. Don't remove any apps"
    Write-Output ""
    $Option = Read-Host "Choose an Option."
    Write-Output ""

    switch ($Option) {
        1 {
            $validOption = $true
            $Patterns = @("Microsoft.WindowsMaps", "Microsoft.Xbox", "Microsoft.Bing", "MicrosoftCorporationII.QuickAssist" , "MicrosoftCorporationII.MicrosoftFamily", "Microsoft.ZuneVideo", "Microsoft.ZuneMusic", "Microsoft.YourPhone", "Microsoft.WindowsTerminal", "Microsoft.WindowsAlarms", "Microsoft.WindowsFeedbackHub", "Microsoft.Todos", "Microsoft.PowerAutomateDesktop", "Microsoft.People", "Microsoft.MicrosoftOfficeHub", "Microsoft.MicrosoftSolitaireCollection", "Microsoft.Getstarted", "Microsoft.GetHelp", "Microsoft.GamingApp")  # Add multiple patterns here
            Remove-ProvisionedAppByName -Patterns $Patterns -MountPath $StageDir -ProvisionedApps $ProvisionedApps
            break
        }
        2 {
            $validOption = $true
            Remove-ProvisionedAppsInteractively -MountPath $StageDir -ProvisionedApps $ProvisionedApps
            break
        }
        3 {
            $validOption = $true
            break
        }
        default {
            Write-Output "Invalid option selected. Please select either 1 or 2."
            break
        }
    }
}

Write-Output ""
Write-Output "The Following apps still remain on the WIM file"
Write-Output ""

# Get information about Appx packages from the Windows image
$ProvisionedApps = Get-AppxProvisionedPackage -Path $StageDir

# Display the information in a clear and readable format
$ProvisionedApps | Format-Table -AutoSize DisplayName, PackageName

Write-Output ""
do {
    $RemoveOption = Read-Host "Choose an option:`n1. Remove more apps`n2. Don't remove any more apps`n"

    if ($RemoveOption -ne "1" -and $RemoveOption -ne "2") {
        Write-Output "Invalid option selected. Please choose either '1' or '2'."
        Write-Output ""
    }
} until ($RemoveOption -eq "1" -or $RemoveOption -eq "2")

switch ($RemoveOption) {
    1 {
        Remove-ProvisionedAppsInteractively -MountPath $StageDir -ProvisionedApps $ProvisionedApps
        break
    }
    2 {
        # Continue without removing anymore apps
        break
    }
}

Write-Output ""
do {
    $DriverOption = Read-Host "Do You Want to inject drivers to the image?:`n1. Inject drivers`n2. Do not add drivers.`n"

    if ($DriverOption -ne "1" -and $DriverOption -ne "2") {
        Write-Output "Invalid option selected. Please choose either '1' or '2'."
        Write-Output ""
    }
} until ($DriverOption -eq "1" -or $DriverOption -eq "2")

switch ($DriverOption) {
    1 { 
    
        # Load Windows Forms assembly
        Add-Type -AssemblyName System.Windows.Forms

        # Function to validate if the directory contains driver files
        function Validate-DriversFolder {
            param (
                [string]$folderPath
            )

            # Check if the selected directory contains driver files
            if (Test-Path (Join-Path $folderPath "*") -PathType Leaf) {
                return $true
            }
            else {
                return $false
            }
        }

        # Create open file dialog
        $openFileDialog = New-Object System.Windows.Forms.FolderBrowserDialog
        $openFileDialog.Description = "Select the directory containing the drivers"
        $openFileDialog.RootFolder = [System.Environment+SpecialFolder]::MyComputer
        $openFileDialog.ShowNewFolderButton = $false

        # Prompt user to select a directory containing drivers
        $selectedFolder = $null
        while (-not $selectedFolder) {
            # Show the open file dialog and check if user selects a directory
            if ($openFileDialog.ShowDialog() -eq 'OK') {
                $selectedFolder = $openFileDialog.SelectedPath
            }
            else {
                # User cancelled the dialog
                $choice = [System.Windows.Forms.MessageBox]::Show("Do you want to cancel injecting drivers?", "Cancel?", "YesNo", "Question")
                if ($choice -eq "Yes") {
                    break
                }
            }
        }

        # Validate the selected folder for drivers
        if (Validate-DriversFolder -folderPath $selectedFolder) {

            # Add drivers to the mounted WIM
            Write-Output "Injecting Selected Driver Files. This could take a while."
            Write-Output ""
            Add-WindowsDriver -Path $StageDir -Driver $selectedFolder -Recurse -Verbose | Out-Null
            Write-Output ""
            Write-Output "Finished adding drivers to image.."
            Write-Output ""
            break

        } else {
            Write-Output "The selected directory does not contain any driver files."
        }
    }
    2 {
        Write-Output "Skipping importing drivers..."
        Write-Output ""
        break
    }
}


try {
    Write-Output ""
    Write-Output "Attempting to save & dismount image..This could take a while"
    Write-Output ""
    
    # Attempt to dismount the Windows image
    Dismount-WindowsImage -Path $StageDir -Save

    # If successful, provide feedback
    Write-Output ""
    Write-Output "Image successfully dismounted and saved."
    Write-Output "Your updated WIM file is located at $wimpath"
    Write-Output ""
} catch {
    # If an error occurs, provide more detailed error information
    $errorMessage = $_.Exception.Message
    Write-Output "Error occurred while dismounting the image: $errorMessage"
}

Write-Output "Thank you for using WIMSimple...Exiting" 
Pause
Exit