Injecting Drivers in MECM Task Sequences

This post demonstrates how to do an MDT style folder based driver injection during OSD.

While there are many ways to accomplish driver injection during MECM task sequences, my university has always used a folder based system, originally modeled off the DriverPaths1 model outlined in this post by Michael Petersen.

I particularly like this configuration in my environment as we deal with hundreds of different models of devices and have hundreds of technicians, most of which don’t have permissions within the MECM console. By using a network share to store drivers, it is very easy for any technician to download and prepare drivers for a machine without ever touching the MECM console.

Note: This requires PowerShell in your boot image. See this post by Alexandre VIOT for instructions on how to add this.

Network Share and Drivers

First and foremost, you need a network share that can be mounted during OSD, including service account credentials for said network share that have read permissions on all subfolders. For this post, we will use “S:\” as the mount point for the store.

Once your share is setup, you can start uploading any drivers for models you use. The structure of the folder is typically like so:

  • Network Share (S:\)
    • Model Folder
      • Windows Version
        • Architecture Folder
          • Drivers

The model name will be the ComputerSystem Model string which can be obtained by running wmi computersystem get model on a target computer.

The Windows Version folder will be in the format “winXX” where X is the version of Windows, such as “win11” or “win7”.

Most enterprise driver packs will already come in the format of Windows Version -> Architecture -> Drivers, though some may need formatting upon upload. Here are some links to common enterprise drivers:

OSD Driver Injection

To integrate driver injection into your task sequence, start by adding a “Connect to Network Folder” step that mounts your network share to a local drive letter, such as “S:\”. This can be placed anywhere in the task sequence before the execution of the PowerShell script.

Next, add a “Run PowerShell Script” step with the following content:

View Code Block
PowerShell
<#
.SYNOPSIS
This script automates the process of installing drivers specific to the computer model and the Windows version. 
It matches the computer model to a driver folder, checks for the appropriate Windows version, and installs the drivers from the matched folder.

.DESCRIPTION
The script retrieves the computer model and a specified Windows version. It then searches a given directory for a matching model and Windows version folder. 
If a direct match is found, it copies the drivers to the system drive under C:\Drivers and initiates the installation. 
If no direct match is found, it looks for the closest lower version available. If still no match is found, it defaults to the base model directory. 
The drivers are installed silently without requiring user interaction.

.PARAMETERS
- BaseDir: The base directory where driver folders are located. It's expected to contain subfolders named after computer models.
- ComputerModel: Automatically retrieved model of the current computer.
- SpecVer: The specific Windows version to match against driver folders, passed as an argument to the script.
- ShowDialogs: A switch parameter to enable or disable the display of popup dialogs.

.EXAMPLE
.\DriverInjection.ps1 -BaseDir "I:" -SpecVer 11 -ShowDialogs

This example searches the "I:\" for a folder matching the current computer's model and Windows version 11, copies the matching drivers to the system drive, and installs them.

.NOTES
- Author: Carter Roeser
- Last Updated: 2024-03-08
- Source: https://cjroeser.com/2024/03/08/injecting-drivers-in-mecm-task-sequences/
- Please ensure PowerShell Execution Policy allows script execution and that all paths and dependencies are correctly configured in your environment.
- Designed to be used in an MECM task sequence, where automation of driver installation is crucial. Will fail if the Task Sequence Environment COM object is not available.
- Make sure to adjust the script paths and environment variables according to your specific deployment environment.

#>

param(
  [string]$BaseDir,
  [double]$SpecVer = 11,
  [switch]$ShowDialogs
)

Write-Host "Beginning Execution of Driver Installation Script"

# Retrieve the computer model and set up the task sequence environment
try {
  $TSEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment
  $TargetSystemDrive = $TSEnv.Value('OSDTargetSystemDrive')
  $TSEnv.Value('DriversInstalled') = "False"
  $TSProgressUI = New-Object -ComObject Microsoft.SMS.TSProgressUI
  $ComputerModel = (Get-CimInstance -ClassName Win32_ComputerSystem).Model
  Add-Type -AssemblyName PresentationFramework
}
catch {
  Write-Error "Task Sequence Environment COM object not available. Ensure the script is running in an MECM task sequence."
  exit
}

# Function: Show-PopupDialog
# Purpose: Displays a customizable popup dialog to the user. Used for error messages and confirmation prompts.
# Parameters:
#   - message: The text message to display in the popup.
#   - title: The title of the popup dialog window.
# Notes: If the ShowDialogs switch is not set, the function returns early. If the user clicks 'Cancel', the script exits.
function Show-PopupDialog {

  param(
    [string]$Message,
    [string]$Title
  )

  # If ShowDialogs switch is not set, return early
  if ($ShowDialogs -eq $false) {
    Write-Host "Skipping popup dialog"  
    return
  }

  # Close the TS progress dialog if it's open
  if ($TSProgressUI) {
    Write-Host "Closing TS progress dialog"
    $TSProgressUI.CloseProgressDialog()
  }

  Write-Host "Displaying popup dialog"

  # Display the popup dialog and capture the user's response
  $popup = [System.Windows.MessageBox]::Show($Message, $Title, "OKCancel", "Error")
  
  Write-Host "Button clicked: $popup"
  if ($popup -eq 2) {
    Write-Host "User clicked 'Cancel'. Exiting script."
    exit
  }
}

# Function: Install-Drivers
# Purpose: Installs drivers from a specified directory into the system drive.
# Notes: Uses Add-WindowsDriver cmdlet to add drivers to the offline Windows image on the target system drive. Sets a task sequence variable upon successful completion.
function Install-Drivers {
  Write-Host "Installing drivers from $TargetSystemDrive\Drivers"

  # Attempt to install drivers using Add-WindowsDriver cmdlet
  try {
    Add-WindowsDriver -Path $TargetSystemDrive -Driver "$TargetSystemDrive\Drivers" -Recurse -ForceUnsigned
    $TSEnv.Value('DriversInstalled') = "True"
  }
  catch {
    Write-Error "An error occurred during driver installation: $_"
  }
}

# Function: Copy-Drivers
# Purpose: Copies driver files from the source directory to the target system drive's Drivers directory.
# Parameters:
#   - SourceDir: The directory containing the driver files to be copied.
# Notes: Creates the target directory if it does not exist and then copies all files from the source, maintaining directory structure.
function Copy-Drivers {
  param([string]$SourceDir)

  $DestDir = "$TargetSystemDrive\Drivers"
  Write-Host "Copying drivers from $SourceDir to $DestDir"

  try {
    # Create the target directory if it does not exist
    if (-not (Test-Path $DestDir)) {
      Write-Host "Creating Directory: $DestDir"
      New-Item -ItemType Directory -Force -Path $DestDir | Out-Null
    }

    # Copy all files from the source directory to the target directory
    Get-ChildItem -Path $SourceDir -Recurse -Force | ForEach-Object {
      # Construct the destination path by replacing the source directory with the target directory
      $sourcePath = $_.FullName
      $destPath = $sourcePath.Replace($SourceDir, $DestDir)

      # Create the destination directory if the source is a directory
      if ($_.PSIsContainer) {
        if (-not (Test-Path $destPath)) {
          Write-Host "Creating Directory: $destPath"
          New-Item -ItemType Directory -Force -Path $destPath -ErrorAction SilentlyContinue | Out-Null
        }
      }
      # Copy the file to the destination
      else {
        Write-Host "Copying driver: $sourcePath to $destPath"
        Copy-Item -Path $sourcePath -Destination $destPath -Force -ErrorAction SilentlyContinue | Out-Null
      }
    }
  }
  catch {
    Write-Error "An error occurred during driver copying: $_"
  }

  Write-Output "Drivers successfully copied from $SourceDir to $DestDir"
}

# Function: Test-DriverStoreConnection
# Purpose: Verifies connectivity to the specified driver store directory.
# Parameters:
#   - BaseDir: The base directory of the driver store to check for connectivity.
# Notes: Displays a popup dialog if the connection fails, allowing the user to retry or cancel.
function Test-DriverStoreConnection {
  param([string]$BaseDir)

  $connection = $False

  # Loop until the connection is established or the user cancels
  while ($connection -eq $False) {
    Write-Host "Checking driver store connection: $BaseDir"

    # If the connection fails, display an error dialog and prompt the user to retry or cancel
    if ((Test-Path -Path $BaseDir) -eq $False) {
      Write-Host "Driver store connection failed. Displaying error dialog."
      Show-PopupDialog -Title "Driver Error" -Message "Error connecting to driver store ($BaseDir)`n`nPlease check the connection and click 'OK' to try again, or click 'Cancel' to skip driver installation."
    }
    # If the connection is successful, set the connection flag to True
    else {
      Write-Host "Driver store connection successful."
      $connection = $True
    }
  }
}

# Function: Test-DriverFolder
# Purpose: Checks for the existence of a specific driver folder within the base directory.
# Parameters:
#   - BaseDir: The base directory where driver folders are located.
#   - ComputerModel: The computer model used to construct the driver folder path.
# Notes: Displays a popup dialog if the folder does not exist, allowing the user to retry or cancel.
function Test-DriverFolder {
  param([string]$BaseDir, [string]$ComputerModel)

  $folder = $False
  $modelDirPath = Join-Path -Path $BaseDir -ChildPath $ComputerModel

  # Loop until the folder is found or the user cancels
  while ($folder -eq $False) {
    Write-Host "Checking for driver folder: $modelDirPath"

    # If the folder does not exist, display an error dialog and prompt the user to retry or cancel
    if ((Test-Path -Path $modelDirPath) -eq $False) {
      Write-Host "Driver folder not found. Displaying error dialog."
      Show-PopupDialog -Title "Driver Folder Not Found" -Message "Please upload drivers to:`n$modelDirPath`n`nClick 'OK' to check again or 'Cancel' to skip driver installation."
    }
    # If the folder exists, set the folder flag to True
    else {
      Write-Host "Driver folder found: $modelDirPath"
      $folder = $True
    }
  }
}

# Function: Find-And-Copy-Drivers
# Purpose: Searches for the best match driver folder based on the computer model and Windows version, then copies and installs the drivers.
# Parameters:
#   - BaseDir: The base directory where driver folders are located.
#   - ComputerModel: The computer model for which drivers are being installed.
#   - SpecVer: The Windows version to match against driver folders.
# Notes: Attempts to find an exact match first. If not found, it searches for the closest lower version available. Displays popup dialogs for user interaction during the process.
function Find-And-Copy-Drivers {
  param(
    [string]$BaseDir,
    [string]$ComputerModel,
    [double]$SpecVer
  )

  $driver = $False

  Write-Host "Searching for drivers in $BaseDir for model $ComputerModel and Windows version $SpecVer"

  # Check for driver store connection and folder existence
  if ($ShowDialogs) {
    Test-DriverStoreConnection -BaseDir $BaseDir
    Test-DriverFolder -BaseDir $BaseDir -ComputerModel $ComputerModel
  }

  # Loop until the driver is found or the user cancels
  while ($driver -eq $False) {

    try {
      $driverPath = Join-Path -Path $BaseDir -ChildPath "$ComputerModel\win$SpecVer"
      $modelDir = Join-Path -Path $BaseDir -ChildPath $ComputerModel
      $folders = Get-ChildItem -Path $modelDir -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^win(\d+)' }
    } catch {
      Write-Error "Unable to read from driver store: $_"
      exit
    }

    Write-Host "Checking for driver folder: $driverPath"

    # If the driver folder exists, install the drivers and exit the loop
    if (Test-Path -Path $driverPath) {
      Write-Host "Found Exact Driver Match: $driverPath"
      Copy-Drivers -SourceDir $driverPath
      Install-Drivers
      exit
    } 

    $versions = $folders | ForEach-Object { [double]($_.Name -replace '^win', '') } | Sort-Object -Descending
    $matchedVersion = $versions | Where-Object { $_ -le $SpecVer } | Select-Object -First 1

    # If a lower version match is found, copy the drivers and exit the loop
    if ($matchedVersion) {
      Write-Host "Found Closest Driver Match: $matchedVersion"
      $matchedPath = Join-Path -Path $modelDir -ChildPath ("win" + $matchedVersion)
      Copy-Drivers -SourceDir $matchedPath
      Install-Drivers
      exit
    }
    # If no match is found, use the base model folder and exit the loop
    else {
      Write-Host "No Driver Match Found. Using Base Model Folder."
      if ($ShowDialogs) {
        Show-PopupDialog -Title "Driver Folder Not Found" -Message "Please upload drivers to:`n$modelDir`n`nClick 'OK' to check again or 'Cancel' to skip driver installation."
      } else {
        exit
      }
    }
  }
}

Find-And-Copy-Drivers -BaseDir $BaseDir -ComputerModel $ComputerModel -SpecVer $SpecVer

or get the script from GitHub.

This step must be placed after your “Apply Operating System Image” step, but before you reboot into the newly applied operating system. I recommend placing this directly after the “Apply Operating System Image” step.

Ensure that you have Execution Policy set to Bypass, and pass your desired variables to configure the script.

The script accepts three variables, the path to your network share, BaseDir, your specified windows version to target, SpecVer, and whether you want to show Error / Retry dialogs ShowDialogs. For example, to set our store to “S:”, target Windows 11, and show error dialogs, set your parameters field to -BaseDir "S:" -SpecVer "11" -ShowDialogs.

Note that the script must run off the WinPE drive (X:) as you cannot perform PowerShell driver installation from an online drive to itself. If you wish to use the script inside of a package, you must specify “X:” as your “Start in” location like so:

Also note that the script will not throw an error on failure, allowing you to silently continue if drivers are not found, but it will set a task sequence variable DriversInstalled to True or False, allowing you to catch and process failures.

The script uses looping functionality to allow users to retry driver installations if they were not found. This effectively pauses your task sequence during the script and allows you to resume once drivers have been uploaded to your store, but if you would not like to pause in the middle of the task sequence, you can omit the -ShowDialogs flag.

Check Drivers Store Prior to Installation

If you would like to check for valid drivers at the beginning of the task sequence to avoid any popups in the middle of the task sequence, you can add a preflight check by utilizing the script below:

View Code Block
PowerShell
<#
.SYNOPSIS
This script checks whether a computer can connect to and retrieve matching drivers from a specified driver store.

.DESCRIPTION
The script retrieves the computer model and a specified Windows version. It then searches a given directory for a matching model and Windows version folder. 
If it can connect to the driver store, it sets the task sequence variable "DriverStoreLoaded" to "True"
If a driver is found, it sets "DriversFound" to "True"

.PARAMETERS
- BaseDir: The base directory where driver folders are located. It's expected to contain subfolders named after computer models.
- ComputerModel: Automatically retrieved model of the current computer.
- SpecVer: The specific Windows version to match against driver folders, passed as an argument to the script.

.EXAMPLE
.\DriverCheck.ps1 -BaseDir "I:" -SpecVer 11

This example searches the "I:\" for a folder matching the current computer's model and Windows version 11.

.NOTES
- Author: Carter Roeser
- Last Updated: 2024-03-08
- Source: https://cjroeser.com/2024/03/08/injecting-drivers-in-mecm-task-sequences/
- Please ensure PowerShell Execution Policy allows script execution and that all paths and dependencies are correctly configured in your environment.
- Designed to be used in an MECM task sequence, where automation of driver installation is crucial. Will fail if the Task Sequence Environment COM object is not available.
- Make sure to adjust the script paths and environment variables according to your specific deployment environment.

#>

param(
  [string]$BaseDir,
  [double]$SpecVer = 11
)

# Setup TSEnv - Read and Write default Task Sequence Variables
$TSEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment
$TSEnv.Value('DriverStoreLoaded') = "False"
$TSEnv.Value('DriversFound') = "False"
$ComputerModel = (Get-CimInstance -ClassName Win32_ComputerSystem).model

# Check if the driver store is loaded
if (Test-Path -Path $BaseDir) {
  $TSEnv.Value('DriverStoreLoaded') = "True"
  $SpecVer = ($SpecVer -as [double])
  $driverPath = Join-Path -Path $BaseDir -ChildPath "$ComputerModel\win$SpecVer"

  if (Test-Path -Path $driverPath) {
      $TSEnv.Value('DriversFound') = "True"
  } else {
      $modelDir = Join-Path -Path $BaseDir -ChildPath $ComputerModel
      $folders = Get-ChildItem -Path $modelDir -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^win(\d+)' }

      $versions = $folders | ForEach-Object { [double]($_.Name -replace '^win', '') } | Sort-Object -Descending
      $matchedVersion = $versions | Where-Object { $_ -le $SpecVer } | Select-Object -First 1

      if ($null -ne $matchedVersion) {
          $TSEnv.Value('DriversFound') = "True"
      }
  }
}

This script similarly takes the BaseDir and SpecVer parameters, but rather than showing error dialogs, it stores the status check values to the DriverStoreLoaded and DriversFound variables, allowing you to pass these to UI++ Preflight Checks for example.

This can be the first step in your task sequence after mounting the driver share, allowing you to warn of fail before performing any destructive actions.