Private/Initialize-FtdiAssembly.ps1
|
# Initialize-FtdiAssembly.ps1 # Version-aware FTDI assembly loading # # Loading strategy by runtime: # PS 5.1 / .NET Framework 4.8 : lib/net48/FTD2XX_NET.dll # PS 7.0-7.3 / .NET 6-7 : lib/netstandard20/FTD2XX_NET.dll # PS 7.4+ / .NET 8+ : lib/net8/ IoT DLLs (primary) + # lib/netstandard20/FTD2XX_NET.dll (FT232R CBUS fallback, Windows only) # # Script-scope flags set by this function: # $script:FtdiInitialized - FTD2XX_NET.dll loaded successfully # $script:IotBackendAvailable - .NET IoT DLLs loaded; IoT backend will be used # $script:FTDI_OK - FT_STATUS.FT_OK constant (when FTD2XX_NET loaded) function Initialize-FtdiAssembly { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $false)] [string]$ModuleRoot = $PSScriptRoot ) # Initialise flags; set to $true only on successful load below $script:IotBackendAvailable = $false $script:D2xxLoaded = $false try { $psVersion = $PSVersionTable.PSVersion.Major $dotnetMajor = [System.Environment]::Version.Major # 4 on net48, 6/8/9 on modern $runningOnWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT' $runningOnMacOS = (-not $runningOnWindows) -and (& { try { (& uname -s 2>$null).Trim() -eq 'Darwin' } catch { $false } }) # ------------------------------------------------------------------ # Path A: PS 7.4+ on .NET 8+ -> load IoT DLLs from lib/net8/ # Also load FTD2XX_NET (netstandard20) on Windows as fallback # for CBUS devices (FT232R) that the IoT library does not cover. # ------------------------------------------------------------------ if ($psVersion -ge 7 -and $dotnetMajor -ge 8) { Write-Verbose "PS $psVersion / .NET $dotnetMajor detected - attempting IoT backend (lib/net8/)" $iotDlls = @( 'Microsoft.Extensions.Logging.Abstractions.dll', 'UnitsNet.dll', 'System.Device.Gpio.dll', 'Iot.Device.Bindings.dll' ) $iotDir = Join-Path (Join-Path $ModuleRoot 'lib') 'net8' $iotLoaded = $true foreach ($dll in $iotDlls) { $dllPath = Join-Path $iotDir $dll if (Test-Path $dllPath) { try { [void][Reflection.Assembly]::LoadFrom($dllPath) Write-Verbose " Loaded IoT DLL: $dll" } catch { Write-Warning " Failed to load IoT DLL '$dll': $_" $iotLoaded = $false } } else { Write-Warning " IoT DLL not found: $dllPath" $iotLoaded = $false } } if ($iotLoaded) { # Verify key types are accessible $null = [Iot.Device.FtCommon.FtCommon] $null = [Iot.Device.Ft232H.Ft232HDevice] $null = [System.Device.Gpio.GpioController] Write-Verbose "IoT backend managed DLLs loaded - using Iot.Device.Bindings" # On Linux/macOS the managed DLLs load fine but the native D2XX .so is also # required at runtime. Probe common install locations and warn early so the # user gets a clear message at module import rather than a wall of P/Invoke # errors when they first call Connect-PsGadgetFtdi. if (-not $runningOnWindows) { $net8Dir = Join-Path (Join-Path $ModuleRoot 'lib') 'net8' $nativeLibName = if ($runningOnMacOS) { 'libftd2xx.dylib' } else { 'libftd2xx.so' } if ($runningOnMacOS) { $nativeLibLocations = @( (Join-Path $net8Dir 'libftd2xx.dylib'), '/usr/local/lib/libftd2xx.dylib', '/usr/lib/libftd2xx.dylib' ) } else { $nativeLibLocations = @( # Check the local lib/net8/ copy first - this survives snap confinement # because it is inside the module directory (always readable by pwsh). (Join-Path $net8Dir 'libftd2xx.so'), '/usr/local/lib/libftd2xx.so', '/usr/lib/libftd2xx.so', '/usr/lib/x86_64-linux-gnu/libftd2xx.so', '/usr/lib/aarch64-linux-gnu/libftd2xx.so', '/usr/lib/arm-linux-gnueabihf/libftd2xx.so' ) } # Use [System.IO.FileInfo]::Exists instead of Test-Path. # Snap-confined pwsh overrides Test-Path with a provider that can # return $true for paths outside the snap tree even when the file # is not readable by .NET P/Invoke or bash. FileInfo.Exists uses # the .NET System.IO stat() path which matches what the runtime sees. $nativeFound = $nativeLibLocations | Where-Object { try { ([System.IO.FileInfo]::new($_)).Exists } catch { $false } } | Select-Object -First 1 if ($nativeFound) { Write-Verbose " Native $nativeLibName found at: $nativeFound" # .NET P/Invoke on Linux searches LD_LIBRARY_PATH and the assembly directory. # On macOS, SIP strips DYLD_LIBRARY_PATH for system processes; use NativeLibrary.Load() # with an absolute path instead (done below in Fix 3). if (-not $runningOnMacOS) { # Fix 1: add the native lib's directory to LD_LIBRARY_PATH for this session. $nativeLibDir = [System.IO.Path]::GetDirectoryName($nativeFound) $existing = $env:LD_LIBRARY_PATH if (-not ($existing -split ':' | Where-Object { $_ -eq $nativeLibDir })) { $env:LD_LIBRARY_PATH = if ($existing) { "${nativeLibDir}:${existing}" } else { $nativeLibDir } Write-Verbose " Set LD_LIBRARY_PATH += $nativeLibDir" } } # Fix 2: copy libftd2xx.so into lib/net8/ so that .NET assembly-directory # probing finds it regardless of LD_LIBRARY_PATH restrictions. # A symlink is not used: snap-confined PowerShell processes cannot follow # symlinks that point outside the snap directory tree, so the symlink would # appear present but the dynamic linker would fail to open it. # # Snap confinement note: snap-confined pwsh can stat() files outside the # snap tree (so Test-Path returns $true) but AppArmor blocks open() on those # paths (Copy-Item/File.Copy throws FileNotFoundException). Never remove an # existing valid copy unconditionally -- doing so destroys a user-placed file # and leaves no way to recover without re-running the bash copy command. # Strategy: # 1. If lib/net8/libftd2xx.so already exists and is a non-empty regular # file, keep it and skip the copy entirely. # 2. If it is missing or a broken symlink, remove the stale entry and # attempt Copy-Item. # 3. If Copy-Item fails (snap AppArmor, permissions, etc.) warn the user # to run the copy from a normal bash terminal, not from snap pwsh. $net8Dir = Join-Path (Join-Path $ModuleRoot 'lib') 'net8' $localCopy = Join-Path $net8Dir $nativeLibName $needCopy = $true try { $fi = [System.IO.FileInfo]::new($localCopy) if ($fi.Exists -and $fi.Length -gt 0) { Write-Verbose " $nativeLibName already in lib/net8/ ($($fi.Length) bytes) - skipping copy" $needCopy = $false } else { # Broken symlink or zero-byte file - remove before copying Remove-Item -Path $localCopy -Force -ErrorAction SilentlyContinue } } catch { # FileInfo threw - entry may be a broken symlink; remove it Remove-Item -Path $localCopy -Force -ErrorAction SilentlyContinue } if ($needCopy) { try { Copy-Item -Path $nativeFound -Destination $localCopy -ErrorAction Stop Write-Verbose " Copied libftd2xx.so to $localCopy" } catch { Write-Warning " Could not copy libftd2xx.so to $localCopy" Write-Warning " This is expected under snap-confined pwsh (AppArmor blocks file reads from /usr/local/lib/)." Write-Warning " Run the following once in a bash terminal (not pwsh), then re-import:" Write-Warning " cp $nativeFound $localCopy" } } # Fix 3: explicitly load the native library by absolute path before P/Invoke # resolves it. Setting LD_LIBRARY_PATH is not sufficient under snap # confinement because AppArmor intercepts the environment variable. # NativeLibrary.Load() pins the .so into the process by exact path, # bypassing LD_LIBRARY_PATH and snap namespace restrictions entirely. # If the library is already loaded, Load() is a no-op (returns the handle). try { $null = [System.Runtime.InteropServices.NativeLibrary]::Load($nativeFound) Write-Verbose " NativeLibrary.Load: OK ($nativeFound)" # Register P/Invoke declarations so module code can call # FT_Open / FT_SetBitMode / FT_ReadEE directly without # needing FTD2XX_NET.dll (which is Windows-only). $piResult = Initialize-FtdiNative -LibraryPath $nativeFound -Verbose:($VerbosePreference -ne 'SilentlyContinue') if ($piResult) { Write-Verbose " FtdiNative P/Invoke: registered" } else { Write-Verbose " FtdiNative P/Invoke: registration failed (CBUS GPIO will use stub on Linux)" } } catch { $loadErr = $_.Exception.Message Write-Warning " NativeLibrary.Load failed for $nativeFound" if ($loadErr -match 'GLIBC_(\S+)\s*not found') { $reqVer = $Matches[1] Write-Warning " GLIBC version mismatch: libftd2xx.so requires GLIBC $reqVer but the" Write-Warning " snap-confined pwsh uses an older bundled glibc (core22 = 2.35)." Write-Warning " Solutions:" Write-Warning " A) Use native (non-snap) pwsh: sudo apt-get install -y powershell" Write-Warning " Then launch with: /usr/bin/pwsh (not the snap alias)" Write-Warning " B) Use older FTDI library compiled against glibc <= 2.35:" Write-Warning " cd /tmp && wget https://ftdichip.com/wp-content/uploads/2024/04/libftd2xx-linux-x86_64-1.4.30.tgz" Write-Warning " tar xzf libftd2xx-linux-x86_64-1.4.30.tgz" Write-Warning " sudo cp linux-x86_64/libftd2xx.so.1.4.30 /usr/local/lib/" Write-Warning " sudo rm -f /usr/local/lib/libftd2xx.so" Write-Warning " sudo ln -sf /usr/local/lib/libftd2xx.so.1.4.30 /usr/local/lib/libftd2xx.so" Write-Warning " sudo ldconfig" Write-Warning " cp linux-x86_64/libftd2xx.so.1.4.30 $nativeFound" } else { Write-Warning " Error: $loadErr" } Write-Warning " IoT backend will not be available until resolved." } # Fix 4: probe that GetDevices() can reach the native lib. # Two distinct failure modes: # a) 'Unable to load shared library' -- the native libftd2xx.so is not # visible to the runtime (missing file, wrong path, snap sandbox). # IotBackendAvailable = $false; sysfs handles enumeration. # b) Any other exception (device busy, ftdi_sio holds the device, etc.) -- # the DLLs are correctly loaded; the conflict is ephemeral. # IotBackendAvailable = $true; after 'sudo rmmod ftdi_sio' the user # can call Connect-PsGadgetFtdi without reimporting. try { $null = [Iot.Device.FtCommon.FtCommon]::GetDevices() $script:IotBackendAvailable = $true Write-Verbose " IoT native probe: OK - GetDevices() succeeded" } catch { $exMsg = $_.Exception.Message if ($exMsg -match 'Unable to load shared library|DllNotFoundException') { Write-Verbose " IoT native probe: native library not loadable - backend disabled" Write-Verbose " Error: $exMsg" # IotBackendAvailable stays $false } else { $script:IotBackendAvailable = $true Write-Verbose " IoT native probe: GetDevices() call failed (non-fatal: $($_.Exception.GetType().Name))" Write-Verbose " DLLs are loaded; likely ftdi_sio holds the device." Write-Verbose " Run: sudo rmmod ftdi_sio then connect without reimporting." } } # NOTE: FTD2XX_NET.dll (netstandard20) is Windows-only. # It contains [DllImport("kernel32.dll")] calls (FreeLibrary/LoadLibrary) in # its finalizer. Loading it on Linux causes an unhandled DllNotFoundException # crash at GC finalization time. FT232R EEPROM/CBUS operations via FTD2XX_NET # are therefore not available on Linux; stub mode is the correct fallback. } else { $arch = '' try { $arch = (& uname -m 2>$null).Trim() } catch {} if ($runningOnMacOS) { $net8CopyDest = Join-Path $net8Dir 'libftd2xx.dylib' Write-Warning ( "IoT FTDI DLLs loaded but native 'libftd2xx.dylib' was not found. " + "Hardware access will fall back to stub mode until it is installed.`n`n" + "Run the following in Terminal (not pwsh) to install (arch: $arch):`n" + "----------------------------------------------------------------------`n" + "# Download the D2XX macOS package from FTDI:`n" + "# https://ftdichip.com/drivers/d2xx-drivers/`n" + "# Open the .dmg or .pkg and run the installer, OR manually:`n" + "sudo cp /path/to/libftd2xx.dylib /usr/local/lib/`n" + "# Copy into lib/net8/ so the module can load it directly:`n" + "cp /usr/local/lib/libftd2xx.dylib '$net8CopyDest'`n" + "# NOTE: AppleUSBFTDI kext may claim the device before D2XX. To unload:`n" + "# sudo kextunload -b com.apple.driver.AppleUSBFTDI`n" + "----------------------------------------------------------------------`n" + "Then in pwsh: Import-Module PSGadget -Force" ) } else { # Linux: detect arch to guide the user to the right tarball $archTgz = switch ($arch) { 'x86_64' { 'libftd2xx-linux-x86_64-1.4.34.tgz' } 'aarch64' { 'libftd2xx-linux-arm-v8-1.4.34.tgz' } 'armv7l' { 'libftd2xx-linux-arm-v7-hf-1.4.34.tgz' } default { 'libftd2xx-linux-<arch>-1.4.34.tgz' } } $archUrl = switch ($arch) { 'x86_64' { 'https://ftdichip.com/wp-content/uploads/2025/11/libftd2xx-linux-x86_64-1.4.34.tgz' } 'aarch64' { 'https://ftdichip.com/drivers/d2xx-drivers/ (select ARM64 v8)' } 'armv7l' { 'https://ftdichip.com/drivers/d2xx-drivers/ (select ARM v7 HF)' } default { 'https://ftdichip.com/drivers/d2xx-drivers/' } } $net8CopyDest = Join-Path $net8Dir 'libftd2xx.so' Write-Warning ( "IoT FTDI DLLs loaded but native 'libftd2xx.so' was not found. " + "Hardware access will fall back to stub mode until it is installed.`n`n" + "Run the following in bash (not pwsh) to install (arch: $arch):`n" + "----------------------------------------------------------------------`n" + "# Step 1: download and extract`n" + "cd /tmp`n" + "wget '$archUrl' -O '$archTgz'`n" + "tar xzf '$archTgz'`n" + "# Step 2: find the versioned .so and install it`n" + "# (the extracted subdirectory name varies by arch/version)`n" + "versioned=`$(find /tmp -maxdepth 3 -name 'libftd2xx.so.*' -not -path '*/usr/*' | head -1)`n" + "sudo cp `"`$versioned`" /usr/local/lib/`n" + "sudo rm -f /usr/local/lib/libftd2xx.so`n" + "sudo ln -sf `"`$versioned`" /usr/local/lib/libftd2xx.so`n" + "sudo ldconfig`n" + "# Step 3: copy versioned .so into lib/net8/ for snap-confined pwsh`n" + "cp `"`$versioned`" '$net8CopyDest'`n" + "# NOTE: D2XX and ftdi_sio (VCP) cannot share the device.`n" + "# If device shows as /dev/ttyUSBx: sudo rmmod ftdi_sio`n" + "# To make permanent: echo 'blacklist ftdi_sio' | sudo tee /etc/modprobe.d/ftdi-d2xx.conf`n" + "----------------------------------------------------------------------`n" + "Then in pwsh: Import-Module PSGadget -Force" ) } } } else { # Windows: IoT managed DLLs loaded; native ftd2xx.dll is in system PATH via CDM driver. # Probe GetDevices() to confirm ftd2xx.dll is reachable before marking backend ready. try { $null = [Iot.Device.FtCommon.FtCommon]::GetDevices() $script:IotBackendAvailable = $true Write-Verbose " IoT native probe: OK (Windows)" } catch { Write-Verbose " IoT native probe failed on Windows: $($_.Exception.Message)" } } } else { Write-Warning "IoT DLL loading incomplete - falling back to FTD2XX_NET backend" } # On Windows: also load FTD2XX_NET (netstandard20) for FT232R CBUS fallback. # On Unix: FTD2XX_NET is not available; FT232R stays in stub mode. if ($runningOnWindows) { $d2xxPath = Join-Path (Join-Path (Join-Path $ModuleRoot 'lib') 'netstandard20') 'FTD2XX_NET.dll' if (Test-Path $d2xxPath) { try { [void][Reflection.Assembly]::LoadFrom($d2xxPath) $null = [FTD2XX_NET.FTDI] $script:FTDI_OK = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK $script:D2xxLoaded = $true Write-Verbose "FTD2XX_NET.dll also loaded (FT232R CBUS fallback)" } catch { Write-Verbose "FTD2XX_NET.dll unavailable for fallback: $_" } } } # $script:FtdiInitialized drives stub/real branching in Invoke-FtdiWindowsEnumerate. # Set it now so enumeration works even before psm1 assigns the return value. $script:FtdiInitialized = $script:D2xxLoaded # Return $true if at least one useful backend is ready return ($script:IotBackendAvailable -or $script:D2xxLoaded) } # ------------------------------------------------------------------ # Path B: Windows PS 5.1 / PS 7 on .NET < 8 -> FTD2XX_NET only # ------------------------------------------------------------------ if ($runningOnWindows) { Write-Verbose "Windows PS $psVersion / .NET $dotnetMajor detected - loading FTD2XX_NET.dll" if ($psVersion -eq 5) { $dllPath = Join-Path (Join-Path (Join-Path $ModuleRoot 'lib') 'net48') 'FTD2XX_NET.dll' } elseif ($psVersion -ge 7) { $dllPath = Join-Path (Join-Path (Join-Path $ModuleRoot 'lib') 'netstandard20') 'FTD2XX_NET.dll' } else { Write-Warning "Unsupported PowerShell version: $psVersion" return $false } if (Test-Path $dllPath) { try { [void][Reflection.Assembly]::LoadFrom($dllPath) $null = [FTD2XX_NET.FTDI] $null = [FTD2XX_NET.FTDI+FT_STATUS] $null = [FTD2XX_NET.FTDI+FT_DEVICE] $script:FTDI_OK = [FTD2XX_NET.FTDI+FT_STATUS]::FT_OK $script:D2xxLoaded = $true Write-Verbose "Successfully loaded FTD2XX_NET.dll from $dllPath" } catch { Write-Error "Failed to load FTD2XX_NET.dll: $_" return $false } } else { Write-Warning "FTD2XX_NET.dll not found at: $dllPath" Write-Verbose "Operating in stub mode - real FTDI operations will not be available" return $false } $script:FtdiInitialized = $script:D2xxLoaded return $script:D2xxLoaded } # ------------------------------------------------------------------ # Path C: Unix / Linux without IoT (.NET < 8) -> stub mode # ------------------------------------------------------------------ Write-Verbose "Unix platform / .NET $dotnetMajor - IoT backend requires .NET 8+; stub mode active" return $false } catch { Write-Error "Failed to initialize FTDI assembly: $_" return $false } } |