|
<# |
|
.SYNOPSIS |
|
Create a GPU-P Guest driver package. |
|
.DESCRIPTION |
|
Gathers the necessary files for a GPU-P enabled Windows guest to run. |
|
.EXAMPLE |
|
New-GPUPDriverPackage -DestinationPath '.' |
|
.EXAMPLE |
|
New-GPUPDriverPackage -Filter 'nvidia' -DestinationPath '.' |
|
.INPUTS |
|
None. |
|
.OUTPUTS |
|
A driver package .zip |
|
.NOTES |
|
This has some mildly dodgy use of CIM cmdlets... |
|
.COMPONENT |
|
PSHyperTools |
|
.ROLE |
|
GPUP |
|
.FUNCTIONALITY |
|
Creates a guest driver package. |
|
#> |
|
|
|
[CmdletBinding( |
|
SupportsShouldProcess = $true, |
|
PositionalBinding = $true, |
|
DefaultParameterSetName = 'NoPathProvided', |
|
HelpUri = 'http://www.microsoft.com/', |
|
ConfirmImpact = 'Low')] |
|
[Alias()] |
|
[OutputType([String])] |
|
Param ( |
|
# Path to output directory. |
|
# If no file name is specified the filename will be GPUPDriverPackage-YYYYMMMDD.zip |
|
[Parameter( |
|
Mandatory = $false, |
|
ParameterSetName = 'PathProvided', |
|
HelpMessage = "Path to one or more locations.")] |
|
[Alias("PSPath", "Path")] |
|
[ValidateNotNullOrEmpty()] |
|
[string] |
|
$DestinationPath, |
|
|
|
# Device friendly name filter. |
|
# Only devices whose friendly names contain the supplied string will be processed |
|
[Parameter( |
|
Mandatory = $false, |
|
HelpMessage = "Only add drivers for devices whose friendly names contain the supplied string.")] |
|
[ValidateNotNullOrEmpty] |
|
[String] |
|
$Filter |
|
) |
|
|
|
process { |
|
try { |
|
# make me a temporary folder, and assemble the structure |
|
$fTempFolder = Join-Path -Path $Env:TEMP -ChildPath "GPUPDriverPackage" |
|
(New-Item -ItemType Directory -Path "$fTempFolder/System32/HostDriverStore/FileRepository" -Force -ErrorAction SilentlyContinue | Out-Null) |
|
(New-Item -ItemType Directory -Path "$fTempFolder/SysWOW64" -Force -ErrorAction SilentlyContinue | Out-Null) |
|
|
|
# Set default archive name |
|
$ArchiveName = ('GPUPDriverPackage-{0}.zip' -f $(Get-Date -UFormat '+%Y%b%d')) |
|
|
|
# Check if DestinationPath has been provided |
|
if ($PSCmdlet.ParameterSetName -eq "PathProvided") { |
|
switch ($DestinationPath) { |
|
{ (Split-Path -Path $_ -Leaf) -match '(\.zip)' } { |
|
# Path we've been provided is a full file path, so we'll just use it |
|
$ArchiveFolder = Split-Path -Path $DestinationPath -Parent |
|
$ArchiveName = Split-Path -Path $DestinationPath -Leaf |
|
break |
|
} |
|
{ Test-Path -Path $_ -PathType Container } { |
|
# Path exists and is a directory, so we place our file in it with the default name. |
|
$ArchiveFolder = $DestinationPath |
|
break |
|
} |
|
Default { |
|
# Path doesn't end in .zip and doesn't exist, so we're going to assume it's a directory and place the file in it with the default name. |
|
$ArchiveFolder = $DestinationPath |
|
(New-Item -ItemType Directory -Path $ArchiveFolder -Force -ErrorAction SilentlyContinue | Out-Null) |
|
break |
|
} |
|
} |
|
} else { |
|
# if DestinationPath not supplied, use current directory and default name |
|
$ArchiveFolder = (Get-Location).Path |
|
} |
|
|
|
# just double check that one |
|
if (-not $ArchiveFolder) { $ArchiveFolder = (Get-Location).Path } |
|
|
|
Write-Output -InputObject ('Creating GPU-P driver package for host {0}' -f $Env:COMPUTERNAME) |
|
Write-Output -InputObject ('Destination path: {0}' -f (Join-Path -Path $ArchiveFolder -ChildPath $ArchiveName)) |
|
|
|
<# |
|
Determine which cmdlet we should use to gather the list of GPU-P capable GPUs. |
|
On Windows builds before Server 2022/21H2, the cmdlet is 'Get-VMPartitionableGpu' |
|
On later builds, it's 'Get-VMHostPartitionableGpu', and the old cmdlet just prints an error. |
|
So, we check if Get-VMHostPartitionableGpu is a valid cmdlet to determine whether we should use it. |
|
#> |
|
Write-Output -InputObject "Getting all GPU-P capable GPUs in the current system..." |
|
if (Get-Command -Name 'Get-VMHostPartitionableGpu' -ErrorAction SilentlyContinue) { |
|
Write-Verbose -Message 'Using new Get-VMHostPartitionableGpu cmdlet' |
|
$PVCapableGPUs = Get-VMHostPartitionableGpu |
|
} else { |
|
Write-Verbose -Message 'Using old Get-VMPartitionableGpu cmdlet' |
|
$PVCapableGPUs = Get-VMPartitionableGpu |
|
} |
|
|
|
# if we found no GPU-P capable GPUs, throw an exception |
|
if ($PVCapableGPUs.Count -lt 1) { |
|
throw [System.Management.Automation.ItemNotFoundException]::new('Did not find any GPU-P capable GPUs in this system.') |
|
} elseif ($PvGPUs.Count -gt 1) { |
|
Write-Warning -Message ( |
|
("You have {0} GPU-P capable GPUs in this system. `n" -f $PvGPUs.Count) + |
|
" At present, there is no way to control which one is assigned to a given VM.`n" + |
|
" Unless one of the available GPUs is an intel IGP, it is highly recommended`n" + |
|
" that you disable the GPU(s) you do not wish to use.`n") |
|
$choices = '&Yes', '&No' |
|
$question = 'Do you wish to proceed without disabling the extra GPU(s)?' |
|
if ($Host.UI.PromptForChoice('', $question, $choices, 1) -eq 1) { |
|
throw [System.Management.Automation.ActionPreferenceStopException]::new('User requested to cancel.') |
|
} |
|
} |
|
|
|
# Map each PVCapableGPU to the corresponding PnPDevice. Regex (mostly) extracts the InstanceId from the VMPartitionableGpu 'name' property. |
|
Write-Output -InputObject ('Mapping GPU-P capable GPUs to their corresponding PnPDevice objects...') |
|
$InstanceExpr = [regex]::New('^\\\\\?\\(.+)#.*$') |
|
$TargetGPUs = $PVCapableGPUs.Name | ForEach-Object -Process { |
|
# I'm not proud of this dirty regex trick, but it works. |
|
Get-PnpDevice -InstanceId $InstanceExpr.Replace($_, '$1').Replace('#', '\') |
|
} |
|
|
|
# OK, now that we have some actual device names, we can filter them if we've been asked to |
|
if ($null -ne $Filter) { |
|
Write-Output -InputObject ('Applying filter "{0}" to device list...' -f $Filter) |
|
$TargetGPUs = $TargetGPUs | Where-Object { $_.FriendlyName -like ('*{0}*' -f $Filter) } |
|
} |
|
Write-Output -InputObject ('Will create driver package for {0} GPUs:' -f $TargetGPUs.Count) |
|
$TargetGPUs.FriendlyName | ForEach-Object { Write-Output -InputObject (' - {0}' -f $_) } |
|
} catch { throw $PSItem } |
|
|
|
# Last chance to turn back, traveler. Are you sure? |
|
if ($pscmdlet.ShouldProcess("Driver Package", "Create")) { |
|
try { |
|
Write-Output -InputObject ('The next few steps may take some time, depending on how many devices & driver packages are installed.') |
|
Write-Output -InputObject ('If the script appears hung, please give it a few minutes to complete before terminating.') |
|
# Get display class devices |
|
Write-Output -InputObject ('Gathering display device CIM objects...') |
|
$PnPEntities = Get-CimInstance -ClassName 'Win32_PnPEntity' | Where-Object { $_.Class -like 'Display' } |
|
Write-Verbose -Message ('Found {0} display devices' -f $PnPEntities.Count) |
|
($PnPEntities | Format-Table -AutoSize | Out-String).Trim().Split("`n") | ForEach-Object -Process { Write-Verbose -Message (' {0}' -f $_) } |
|
|
|
# Get display class drivers |
|
Write-Output -InputObject ('Gathering display device driver CIM objects...') |
|
$PnPSignedDrivers = Get-CimInstance -ClassName 'Win32_PnPSignedDriver' -Filter "DeviceClass = 'DISPLAY'" |
|
Write-Verbose -Message ('Found {0} display device drivers' -f $PnPSignedDrivers.Count) |
|
$PnPSignedInfo = ($PnPSignedDrivers | Select-Object -Property DeviceName,DriverProviderName,InfName,DriverVersion,Description | Format-Table -AutoSize | Out-String).Trim().Split("`n") |
|
$PnPSignedInfo | ForEach-Object -Process { Write-Verbose -Message (' {0}' -f $_) } |
|
|
|
# next we have to get every PnPSignedDriverCIMDataFile, because Get-CimAssociatedInstance doesn't wanna play ball |
|
Write-Output -InputObject ('Gathering all driver file objects... (this is the slow one. Blame Microsoft.)') # or me not understanding CIM i guess? |
|
$SignedDriverFiles = Get-CimInstance -ClassName 'Win32_PNPSignedDriverCIMDataFile' |
|
Write-Output -InputObject ('Found {0} files across all system drivers.' -f $SignedDriverFiles.Count) |
|
|
|
foreach ($GPU in $TargetGPUs) { |
|
Write-Output -InputObject ('Getting driver package for {0}' -f $GPU.FriendlyName) |
|
$PnPEntity = $PnPEntities | Where-Object { $_.InstanceId -eq $GPU.InstanceId }[0] |
|
Write-Verbose -Message ('Device PnP Entity:') |
|
($PnPEntity | Format-Table -AutoSize | Out-String).Trim().Split("`n") | ForEach-Object -Process { Write-Verbose -Message (' {0}' -f $_) } |
|
|
|
$PnPSignedDriver = $PnPSignedDrivers | Where-Object { $_.DeviceId -eq $GPU.InstanceId } |
|
Write-Verbose -Message ('Device PnPSignedDriver:') |
|
($PnPSignedDriver | Format-Table -AutoSize | Out-String).Trim().Split("`n") | ForEach-Object -Process { Write-Verbose -Message (' {0}' -f $_) } |
|
|
|
$SystemDriver = Get-CimAssociatedInstance -InputObject $PnPEntity -Association Win32_SystemDriverPNPEntity |
|
Write-Verbose -Message ('Device SystemDriver:') |
|
($SystemDriver | Format-Table -AutoSize | Out-String).Trim().Split("`n") | ForEach-Object -Process { Write-Verbose -Message (' {0}' -f $_) } |
|
|
|
Write-Verbose -Message ('SystemDriver main/anchor file: {0}' -f $SystemDriver.PathName) |
|
$DriverStoreFolder = Get-Item -Path (Split-Path -Path $SystemDriver.PathName -Parent) |
|
while ((Get-Item (Split-Path $DriverStoreFolder)).Name -notlike 'FileRepository') { |
|
$DriverStoreFolder = Get-Item -Path (Split-Path -Path $DriverStoreFolder -Parent) |
|
} |
|
Write-Verbose -Message ('Device DriverStoreFolder: {0}' -f $DriverStoreFolder.FullName) |
|
|
|
Write-Output -InputObject ('Found package {0}, copying DriverStore folder {1} to temporary directory' -f $PnPSignedDriver.InfName, (Split-Path $DriverStoreFolder -Leaf)) |
|
$TempDriverStore = ('{0}/System32/HostDriverStore/FileRepository/{1}' -f $fTempFolder, $DriverStoreFolder.Name) |
|
$DriverStoreFolder | Copy-Item -Destination $TempDriverStore -Recurse -Force |
|
Write-Output -InputObject ('Copied {0} of {1} files to temporary directory' -f (Get-ChildItem -Path $TempDriverStore -Recurse).Count, (Get-ChildItem -Path $DriverStoreFolder -Recurse).Count) |
|
|
|
# Get driver files from system32 etc and copy |
|
Write-Output -InputObject ('Gathering files from System32 and SysWOW64') |
|
$DriverFiles = ($SignedDriverFiles | Where-Object { $_.Antecedent.DeviceID -like $GPU.DeviceID }).Dependent.Name | Sort-Object |
|
$NonDriverStoreFiles = $DriverFiles.Where{$_ -notlike '*DriverStore*'} |
|
|
|
Write-Output -InputObject ('Found {0} files, copying to temporary directory...' -f $NonDriverStoreFiles.Count) |
|
$NonDriverStoreFiles | ForEach-Object -Process { |
|
$TargetPath = Join-Path -Path $fTempFolder -ChildPath $_.ToLower().Replace(('{0}\' -f $Env:SYSTEMROOT.ToLower()),'') |
|
# make sure the parent folder exists |
|
(New-Item -ItemType directory -Path (Split-Path -Path $TargetPath -Parent) -Force -ErrorAction SilentlyContinue | Out-Null) |
|
Write-Output -InputObject (' - {0} -> {1}' -f $_, $TargetPath) |
|
Copy-Item -Path $_ -Destination $TargetPath -Force -Recurse |
|
} |
|
Write-Output -InputObject ('Finished gathering files for {0}' -f $GPU.FriendlyName) |
|
} |
|
Write-Output -InputObject ('All driver files have been collected, creating archive file') |
|
$Location = (Get-Location).Path |
|
Set-Location -Path (Split-Path -Path $fTempFolder -Parent) |
|
Compress-Archive -Path $fTempFolder -DestinationPath (Join-Path -Path $ArchiveFolder -ChildPath $ArchiveName) -CompressionLevel Fastest -Confirm:$false |
|
Set-Location -Path $Location |
|
Write-Output -InputObject ('GPU driver package has been created at path {0}\{1}' -f $ArchiveFolder, $ArchiveName) |
|
} catch { |
|
throw $PSItem |
|
} finally { |
|
Write-Output -InputObject ('Cleaning up temporary directory {0}' -f $fTempFolder) |
|
Remove-Item -Recurse -Force -Path $fTempFolder |
|
} |
|
} |
|
Write-Output -InputObject ('Driver package generation complete.') |
|
Write-Output -InputObject ('Please copy it to your guest and extract the archive into C:\Windows\') |
|
} |
Can either of you confirm whether
$SystemDriver.PathName
for that driver is:or
please? because from @mmikeww's screenshot above, and the behaviour they've described, it looks like it should be the second one. If it is the second one, I don't need to do anything and this script is working just fine.
If it is the first one, then this is a problem that's specific to AMD drivers - possibly ASUS's OEM AMD drivers, even - where for some reason they've decided to put the main
.inf
a folder deeper than everyone else does.Your solution may fix the issue for your specific AMD machine/driver, but it will break the script for everyone else, since their drivers don't have the extra subdirectory. All your solution would end up doing is grabbing the host's entire FileRepository, which is completely unnecessary and kind of insane.
There's a better solution to the 'two folders' problem, if it even is a problem:
That'll recurse up the directory tree until it finds the folder whose parent is
$Env:SYSTEMROOT\System32\DriverStore\FileRepository
.Technically, I don't need to bother with the DriverStoreFolder stuff at all, since the contents are all tagged to the driver's DriverFiles. You could replace
$NonDriverStoreFiles = $DriverFiles.Where{$_ -notlike '*DriverStore*'}
with just$NonDriverStoreFiles = $DriverFiles
and that'd work, it's just cleaner to copy the whole subdirectory tree.