#Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] param( [string] $TlbImpPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $expectedTypeLibId = '{2B7F2D64-3A8D-401C-BE73-5C0747BA342C}' $expectedTypeLibVersion = '1.0' $expectedThreadingModel = 'Apartment' $amd64Machine = 0x8664 $expectedClasses = @( [pscustomobject]@{ Name = 'KAEngine' ClassId = '{D756CDBE-AA31-42B2-9CC7-018753CA61BF}' ProgId = 'K3DAsyncEngine.KAEngine.1' }, [pscustomobject]@{ Name = 'KAEventHandler' ClassId = '{39828C77-EFF0-4E59-979B-8673C028C718}' ProgId = 'K3DAsyncEngine.KAEventHandler.1' } ) function Get-RegistryValue { param( [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryKey] $BaseKey, [Parameter(Mandatory = $true)] [string] $SubKey, [string] $Name = '' ) $key = $BaseKey.OpenSubKey($SubKey, $false) if ($null -eq $key) { return $null } try { return $key.GetValue( $Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } finally { $key.Dispose() } } function Assert-TextEqual { param( [AllowNull()] [object] $Actual, [Parameter(Mandatory = $true)] [string] $Expected, [Parameter(Mandatory = $true)] [string] $Description ) if ($null -eq $Actual -or -not [string]::Equals( [string] $Actual, $Expected, [StringComparison]::OrdinalIgnoreCase)) { throw "$Description mismatch. Expected '$Expected'; found '$Actual'." } } function Assert-GuidEqual { param( [AllowNull()] [object] $Actual, [Parameter(Mandatory = $true)] [string] $Expected, [Parameter(Mandatory = $true)] [string] $Description ) $actualGuid = [Guid]::Empty $expectedGuid = [Guid]::Empty if ($null -eq $Actual -or -not [Guid]::TryParse([string] $Actual, [ref] $actualGuid) -or -not [Guid]::TryParse($Expected, [ref] $expectedGuid) -or $actualGuid -ne $expectedGuid) { throw "$Description mismatch. Expected '$Expected'; found '$Actual'." } } function Resolve-RegisteredFilePath { param( [AllowNull()] [object] $RegistryValue, [Parameter(Mandatory = $true)] [string] $Description ) if ($null -eq $RegistryValue -or [string]::IsNullOrWhiteSpace([string] $RegistryValue)) { throw "$Description is not registered." } $expanded = [Environment]::ExpandEnvironmentVariables([string] $RegistryValue) $expanded = $expanded.Trim().Trim('"') if (-not [IO.Path]::IsPathRooted($expanded)) { throw "$Description is not an absolute path." } $fullPath = [IO.Path]::GetFullPath($expanded) if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { throw "$Description does not exist: $fullPath" } return $fullPath } function Get-PeMachine { param( [Parameter(Mandatory = $true)] [string] $Path ) $stream = [IO.File]::Open( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, ([IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete)) $reader = [IO.BinaryReader]::new($stream) try { if ($stream.Length -lt 64 -or $reader.ReadUInt16() -ne 0x5A4D) { throw "Not a PE file: $Path" } $stream.Position = 0x3C $peOffset = $reader.ReadInt32() if ($peOffset -lt 0 -or ($peOffset + 6) -gt $stream.Length) { throw "Invalid PE header offset: $Path" } $stream.Position = $peOffset if ($reader.ReadUInt32() -ne 0x00004550) { throw "Invalid PE signature: $Path" } $machineCode = $reader.ReadUInt16() $machineName = switch ($machineCode) { 0x014C { 'I386' } 0x8664 { 'AMD64' } 0xAA64 { 'ARM64' } default { 'Unknown' } } return [pscustomobject]@{ Code = $machineCode Name = $machineName } } finally { $reader.Dispose() $stream.Dispose() } } function Assert-Amd64File { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Description ) $machine = Get-PeMachine -Path $Path if ($machine.Code -ne $amd64Machine) { throw "$Description is not AMD64. PE machine is $($machine.Name) (0x$('{0:X4}' -f $machine.Code))." } } function Test-RegistryKeyExists { param( [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryKey] $BaseKey, [Parameter(Mandatory = $true)] [string] $SubKey ) $key = $BaseKey.OpenSubKey($SubKey, $false) if ($null -eq $key) { return $false } $key.Dispose() return $true } function Assert-NoCurrentUserOverrides { param( [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryKey] $CurrentUserRoot ) $paths = @("Software\Classes\TypeLib\$expectedTypeLibId") foreach ($registration in $expectedClasses) { $paths += "Software\Classes\CLSID\$($registration.ClassId)" $paths += "Software\Classes\$($registration.ProgId)" } foreach ($path in $paths) { if (Test-RegistryKeyExists -BaseKey $CurrentUserRoot -SubKey $path) { throw "A per-user Registry64 K3D COM override is present at HKCU:\$path. Remove the override through the approved vendor recovery procedure before use." } } } function Assert-ComClassRegistration { param( [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryKey] $MachineRoot, [Parameter(Mandatory = $true)] [object] $Registration ) $classesPrefix = 'SOFTWARE\Classes' $progIdClassId = Get-RegistryValue ` -BaseKey $MachineRoot ` -SubKey "$classesPrefix\$($Registration.ProgId)\CLSID" Assert-GuidEqual ` -Actual $progIdClassId ` -Expected $Registration.ClassId ` -Description "$($Registration.Name) ProgID to CLSID mapping" $classProgId = Get-RegistryValue ` -BaseKey $MachineRoot ` -SubKey "$classesPrefix\CLSID\$($Registration.ClassId)\ProgID" Assert-TextEqual ` -Actual $classProgId ` -Expected $Registration.ProgId ` -Description "$($Registration.Name) CLSID to ProgID mapping" $classTypeLibId = Get-RegistryValue ` -BaseKey $MachineRoot ` -SubKey "$classesPrefix\CLSID\$($Registration.ClassId)\TypeLib" Assert-GuidEqual ` -Actual $classTypeLibId ` -Expected $expectedTypeLibId ` -Description "$($Registration.Name) TypeLib GUID" $classTypeLibVersion = Get-RegistryValue ` -BaseKey $MachineRoot ` -SubKey "$classesPrefix\CLSID\$($Registration.ClassId)\Version" if ($null -ne $classTypeLibVersion -and -not [string]::IsNullOrWhiteSpace([string] $classTypeLibVersion)) { Assert-TextEqual ` -Actual $classTypeLibVersion ` -Expected $expectedTypeLibVersion ` -Description "$($Registration.Name) TypeLib version" } $inprocKey = "$classesPrefix\CLSID\$($Registration.ClassId)\InprocServer32" $serverPath = Resolve-RegisteredFilePath ` -RegistryValue (Get-RegistryValue -BaseKey $MachineRoot -SubKey $inprocKey) ` -Description "$($Registration.Name) InprocServer32 path" $threadingModel = Get-RegistryValue ` -BaseKey $MachineRoot ` -SubKey $inprocKey ` -Name 'ThreadingModel' Assert-TextEqual ` -Actual $threadingModel ` -Expected $expectedThreadingModel ` -Description "$($Registration.Name) threading model" Assert-Amd64File -Path $serverPath -Description "$($Registration.Name) COM server DLL" } function Resolve-X64TlbImpPath { param( [AllowEmptyString()] [string] $ExplicitPath ) if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { if (-not [IO.Path]::IsPathRooted($ExplicitPath)) { throw 'TlbImpPath must be an absolute path.' } $resolvedExplicitPath = [IO.Path]::GetFullPath($ExplicitPath) if (-not (Test-Path -LiteralPath $resolvedExplicitPath -PathType Leaf)) { throw "TlbImp.exe was not found: $resolvedExplicitPath" } if (-not [string]::Equals( [IO.Path]::GetFileName($resolvedExplicitPath), 'TlbImp.exe', [StringComparison]::OrdinalIgnoreCase)) { throw 'TlbImpPath must identify TlbImp.exe.' } Assert-Amd64File -Path $resolvedExplicitPath -Description 'TlbImp.exe' return $resolvedExplicitPath } $programFilesX86 = [Environment]::GetFolderPath( [Environment+SpecialFolder]::ProgramFilesX86) $sdkBin = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin' $candidates = @( (Join-Path $sdkBin 'NETFX 4.8.1 Tools\x64\TlbImp.exe'), (Join-Path $sdkBin 'NETFX 4.8 Tools\x64\TlbImp.exe'), (Join-Path $sdkBin 'NETFX 4.7.2 Tools\x64\TlbImp.exe'), (Join-Path $sdkBin 'NETFX 4.6.2 Tools\x64\TlbImp.exe') ) foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate -PathType Leaf) { Assert-Amd64File -Path $candidate -Description 'TlbImp.exe' return [IO.Path]::GetFullPath($candidate) } } $candidateList = $candidates -join [Environment]::NewLine throw "An x64 TlbImp.exe was not found in the known Windows SDK locations. Install the .NET Framework SDK or pass -TlbImpPath with an x64 SDK path. Checked:$([Environment]::NewLine)$candidateList" } if (-not [Environment]::Is64BitOperatingSystem) { throw 'K3D x64 interop generation requires a 64-bit Windows operating system.' } $machineRoot = [Microsoft.Win32.RegistryKey]::OpenBaseKey( [Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) $currentUserRoot = [Microsoft.Win32.RegistryKey]::OpenBaseKey( [Microsoft.Win32.RegistryHive]::CurrentUser, [Microsoft.Win32.RegistryView]::Registry64) try { Assert-NoCurrentUserOverrides -CurrentUserRoot $currentUserRoot $typeLibKey = "SOFTWARE\Classes\TypeLib\$expectedTypeLibId\$expectedTypeLibVersion\0\win64" $registeredTypeLibPath = Resolve-RegisteredFilePath ` -RegistryValue (Get-RegistryValue -BaseKey $machineRoot -SubKey $typeLibKey) ` -Description 'K3D x64 type library path' Assert-Amd64File -Path $registeredTypeLibPath -Description 'K3D type library DLL' foreach ($registration in $expectedClasses) { Assert-ComClassRegistration ` -MachineRoot $machineRoot ` -Registration $registration } } finally { $currentUserRoot.Dispose() $machineRoot.Dispose() } $resolvedTlbImpPath = Resolve-X64TlbImpPath -ExplicitPath $TlbImpPath $repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $outputDirectory = [IO.Path]::GetFullPath( (Join-Path $repositoryRoot 'artifacts\K3DInterop')) $outputPath = Join-Path $outputDirectory 'Interop.K3DAsyncEngineLib.dll' if (-not $PSCmdlet.ShouldProcess( $outputPath, 'Generate an x64 diagnostic interop assembly from the registered K3D type library')) { [pscustomobject][ordered]@{ Status = 'NotGenerated' TypeLibId = $expectedTypeLibId TypeLibVersion = $expectedTypeLibVersion TypeLibPath = $registeredTypeLibPath RegistrationHive = 'HKLM' CurrentUserOverrides = 'None' ValidatedClasses = @($expectedClasses.Name) TlbImpPath = $resolvedTlbImpPath OutputPath = $outputPath Machine = 'AMD64' ComActivated = $false } return } [void] (New-Item -ItemType Directory -Path $outputDirectory -Force) $stagingDirectory = Join-Path ` $outputDirectory ` ('.staging-{0}-{1}' -f $PID, [Guid]::NewGuid().ToString('N')) $stagedOutputPath = Join-Path $stagingDirectory 'Interop.K3DAsyncEngineLib.dll' [void] (New-Item -ItemType Directory -Path $stagingDirectory) try { $tlbImpArguments = @( $registeredTypeLibPath, "/out:$stagedOutputPath", '/namespace:K3DAsyncEngineLib', '/machine:X64', '/silent' ) $tlbImpOutput = & $resolvedTlbImpPath @tlbImpArguments 2>&1 $tlbImpExitCode = $LASTEXITCODE if ($tlbImpExitCode -ne 0) { $diagnosticText = ($tlbImpOutput | Out-String).Trim() throw "TlbImp.exe failed with exit code $tlbImpExitCode. $diagnosticText" } if (-not (Test-Path -LiteralPath $stagedOutputPath -PathType Leaf)) { throw 'TlbImp.exe reported success but did not create the interop assembly.' } Assert-Amd64File -Path $stagedOutputPath -Description 'Generated interop assembly' [IO.File]::Copy($stagedOutputPath, $outputPath, $true) Assert-Amd64File -Path $outputPath -Description 'Final interop assembly' $hash = Get-FileHash -LiteralPath $outputPath -Algorithm SHA256 [pscustomobject][ordered]@{ Status = 'Generated' TypeLibId = $expectedTypeLibId TypeLibVersion = $expectedTypeLibVersion TypeLibPath = $registeredTypeLibPath RegistrationHive = 'HKLM' CurrentUserOverrides = 'None' ValidatedClasses = @($expectedClasses.Name) TlbImpPath = $resolvedTlbImpPath OutputPath = $outputPath Machine = 'AMD64' Sha256 = $hash.Hash ComActivated = $false } } finally { if (Test-Path -LiteralPath $stagingDirectory) { Remove-Item -LiteralPath $stagingDirectory -Recurse -Force } }