MSIX.VcRuntime.ps1
|
# ============================================================================= # Visual C++ Runtime detection + bundling # ----------------------------------------------------------------------------- # Many legacy Win32 apps depend on the VC++ redistributable (msvcp140.dll, # vcruntime140.dll, ucrtbase.dll …). When packaged as MSIX they cannot rely on # WinSxS-shared copies, and the OS image MAY not have the right runtime. # # This module: # - Detects which VC runtime DLLs an unpacked MSIX references. # - Identifies whether they're already bundled in the package. # - Optionally copies the right architecture-matched DLLs in from a source # folder (e.g. Microsoft Visual Studio's redist), so the package becomes # self-contained. # # References: # - TMEditX UCVCRuntimes (v0.9 modelled on this) # - https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files # ============================================================================= # Canonical runtime DLL set (release + debug). $script:KnownVcRuntimeDlls = @( # Release CRT 'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'vcruntime140.dll', 'vcruntime140_1.dll', 'ucrtbase.dll', 'concrt140.dll', 'mfc140.dll', 'mfc140u.dll', # Debug CRT 'msvcp140d.dll', 'msvcp140_1d.dll', 'msvcp140_2d.dll', 'vcruntime140d.dll', 'vcruntime140_1d.dll', 'ucrtbased.dll', 'concrt140d.dll', 'mfc140d.dll', 'mfc140ud.dll', # Older toolsets that some apps still ship 'msvcr120.dll', 'msvcr110.dll', 'msvcr100.dll', 'msvcp120.dll', 'msvcp110.dll', 'msvcp100.dll', 'msvcr120_clr0400.dll' ) function Get-MsixVcRuntimeReference { <# .SYNOPSIS Walks the unpacked package and returns: - References the VC runtime DLLs imported by .exe/.dll files (best-effort: scans PE imports via dumpbin or string match) - Bundled the VC runtime DLLs that ARE present in the package - Missing References that are NOT present in the package .DESCRIPTION Static check, nothing is mutated. The package is unpacked into a temporary workspace, all PE files (.exe/.dll) are scanned for references to known VC runtime DLLs via a best-effort string scan (no dumpbin dependency), and the result is reported alongside what the package already bundles. Use Add-MsixVcRuntimeBundle to bring the missing DLLs into the package. .PARAMETER PackagePath .msix file (will be unpacked into a workspace, then cleaned up). .OUTPUTS [pscustomobject] with: - PackagePath full path of the analysed file - References hashtable: dll name -> list of importing files - Bundled array of pscustomobject (Name, Path, SizeBytes, Architecture) for runtime DLLs already in the package - Missing string[] of runtime DLL names referenced but absent .EXAMPLE $r = Get-MsixVcRuntimeReference -PackagePath .\app.msix $r.Missing #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$PackagePath ) $toolsRoot = Get-MsixToolsRoot $fileinfo = Get-Item $PackagePath $workspace = New-MsixWorkspace "$($fileinfo.BaseName)-vcrt" try { $r = Invoke-MsixProcess "$toolsRoot\Tools\MakeAppx.exe" -ArgumentList @('unpack', '/p', $fileinfo.FullName, '/d', $workspace, '/o') Assert-MsixProcessSuccess $r 'MakeAppx unpack' $allFiles = Get-ChildItem $workspace -Recurse -File -ErrorAction SilentlyContinue $bundled = $allFiles | Where-Object { $script:KnownVcRuntimeDlls -contains $_.Name.ToLower() } | ForEach-Object { [pscustomobject]@{ Name = $_.Name.ToLower() Path = $_.FullName.Substring($workspace.Length + 1) SizeBytes = $_.Length Architecture = (_GetPeArchitecture $_.FullName) } } # Best-effort PE import scan. $references = @{} foreach ($exe in @($allFiles | Where-Object { $_.Extension -in '.exe','.dll' })) { $imports = _GetPeImports $exe.FullName foreach ($imp in $imports) { $low = $imp.ToLower() if ($script:KnownVcRuntimeDlls -contains $low) { if (-not $references.ContainsKey($low)) { $references[$low] = @() } $references[$low] += $exe.FullName.Substring($workspace.Length + 1) } } } $bundledNames = @($bundled.Name) $missing = @($references.Keys | Where-Object { $_ -notin $bundledNames }) return [pscustomobject]@{ PackagePath = $fileinfo.FullName References = $references Bundled = $bundled Missing = $missing } } finally { Remove-Item $workspace -Recurse -Force -ErrorAction SilentlyContinue } } function _GetPeArchitecture { param([string]$Path) try { # Read PE header machine field at the offset stored at 0x3C $bytes = [byte[]]::new(2) $fs = [System.IO.File]::OpenRead($Path) try { $fs.Position = 0x3C $offBuf = [byte[]]::new(4) $null = $fs.Read($offBuf, 0, 4) $peOffset = [BitConverter]::ToInt32($offBuf, 0) $fs.Position = $peOffset + 4 $null = $fs.Read($bytes, 0, 2) } finally { $fs.Dispose() } switch ([BitConverter]::ToUInt16($bytes, 0)) { 0x014C { 'x86' } 0x8664 { 'x64' } 0xAA64 { 'arm64' } default { 'unknown' } } } catch { 'unknown' } } function _GetPeImports { param([string]$Path) # Cheap fallback: scan strings for known DLL names. Works for almost all # Win32 executables without needing dumpbin (Visual Studio). try { $stream = [System.IO.File]::OpenRead($Path) try { $buf = New-Object byte[] ([math]::Min($stream.Length, 8MB)) $n = $stream.Read($buf, 0, $buf.Length) $txt = [System.Text.Encoding]::ASCII.GetString($buf, 0, $n) } finally { $stream.Dispose() } $hits = New-Object System.Collections.Generic.HashSet[string] foreach ($dll in $script:KnownVcRuntimeDlls) { if ($txt -match [regex]::Escape($dll)) { $null = $hits.Add($dll) } } return @($hits) } catch { @() } } function Add-MsixVcRuntimeBundle { <# .SYNOPSIS Copies VC++ runtime DLLs into an MSIX package so the app no longer relies on host-side WinSxS / VCRedist. .DESCRIPTION Detects the missing VC runtime DLLs (per Get-MsixVcRuntimeReference), finds them under -SourceFolder (architecture-aware), copies them next to the application executable(s), repacks, and signs. .PARAMETER PackagePath .msix to modify. .PARAMETER SourceFolder Folder containing release-built VC runtime DLLs. Typically the VS redist directory: %ProgramFiles%\Microsoft Visual Studio\<ver>\<edition>\VC\Redist\MSVC\<ver>\<arch>\Microsoft.VC*.CRT .PARAMETER Architecture x86 or x64. Defaults to whichever the package's first executable uses. .PARAMETER Names Override the DLL list (default: missing DLLs detected by analysis). .PARAMETER OutputPath Where to write the repacked .msix. Defaults to overwriting -PackagePath. .PARAMETER SkipSigning Do not re-sign after repacking. Alias: -NoSign. .PARAMETER Pfx Path to the signing PFX. Required unless -SkipSigning is set. .PARAMETER PfxPassword SecureString password for -Pfx. .OUTPUTS [pscustomobject] with PackagePath, Bundled (the DLL names copied in) and Architecture. Nothing is returned when no DLLs were copied. .EXAMPLE Add-MsixVcRuntimeBundle -PackagePath .\app.msix ` -SourceFolder 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC\14.40.33807\x64\Microsoft.VC143.CRT' ` -Pfx .\cert.pfx -PfxPassword (Read-Host -AsSecureString) .EXAMPLE # Bundle a specific DLL only Add-MsixVcRuntimeBundle -PackagePath .\app.msix ` -SourceFolder C:\Redist -Names 'msvcp140.dll' -SkipSigning #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$PackagePath, [Parameter(Mandatory)] [string]$SourceFolder, [ValidateSet('x86','x64','auto')] [string]$Architecture = 'auto', [string[]]$Names, [string]$OutputPath, [Alias('NoSign')] [switch]$SkipSigning, [string]$Pfx, [SecureString]$PfxPassword ) if (-not (Test-Path $SourceFolder)) { throw "VC runtime source folder not found: $SourceFolder" } $toolsRoot = Get-MsixToolsRoot $fileinfo = Get-Item $PackagePath $workspace = New-MsixWorkspace $fileinfo.BaseName try { $r = Invoke-MsixProcess "$toolsRoot\Tools\MakeAppx.exe" -ArgumentList @('unpack', '/p', $fileinfo.FullName, '/d', $workspace, '/o') Assert-MsixProcessSuccess $r 'MakeAppx unpack' $null = Test-MsixManifest "$workspace\AppxManifest.xml" [xml]$manifest = Get-MsixManifest "$workspace\AppxManifest.xml" $apps = @(Get-MsixManifestApplication $manifest) # Determine architecture if auto if ($Architecture -eq 'auto') { $sample = Join-Path $workspace $apps[0].GetAttribute('Executable') $Architecture = if (Test-Path $sample) { (_GetPeArchitecture $sample) } else { 'x86' } if ($Architecture -notin 'x86','x64') { $Architecture = 'x86' } Write-MsixLog Info "Architecture auto-detected: $Architecture" } # Resolve DLLs to copy if (-not $Names) { $analysis = Get-MsixVcRuntimeReference -PackagePath $PackagePath $Names = $analysis.Missing if (-not $Names) { Write-MsixLog Info 'No missing VC runtime DLLs detected; nothing to bundle.' return } Write-MsixLog Info "Will bundle missing DLLs: $($Names -join ', ')" } # Locate each DLL under SourceFolder. Heuristic search. $copied = @() foreach ($name in $Names) { $hit = Get-ChildItem $SourceFolder -Recurse -Filter $name -ErrorAction SilentlyContinue | Where-Object { (_GetPeArchitecture $_.FullName) -eq $Architecture } | Select-Object -First 1 if (-not $hit) { Write-MsixLog Warning "$name not found under $SourceFolder for $Architecture" continue } # Copy into the same folder as the first executable $exeRel = $apps[0].Executable $destDir = if ($exeRel.Contains('\')) { Join-Path $workspace $exeRel.Substring(0, $exeRel.LastIndexOf('\')) } else { $workspace } if ($PSCmdlet.ShouldProcess($destDir, "Copy $name")) { Copy-Item $hit.FullName $destDir -Force $copied += $name } } if ($copied.Count -eq 0) { Write-MsixLog Warning 'No VC runtime DLLs were copied; aborting.' return } # Repack $target = if ($OutputPath) { $OutputPath } else { $fileinfo.FullName } $r = Invoke-MsixProcess "$toolsRoot\Tools\MakeAppx.exe" -ArgumentList @('pack', '/p', $target, '/d', $workspace, '/o') Assert-MsixProcessSuccess $r 'MakeAppx pack' if (-not $SkipSigning) { Invoke-MsixSigning -PackagePath $target -Pfx $Pfx -PfxPassword $PfxPassword } return [pscustomobject]@{ PackagePath = $target; Bundled = $copied; Architecture = $Architecture } } finally { Remove-Item $workspace -Recurse -Force -ErrorAction SilentlyContinue } } # Backward-compatible plural aliases Set-Alias Get-MsixVcRuntimeReferences Get-MsixVcRuntimeReference |