src/Cache.ps1
|
# ps12exe 的临时缓存公共逻辑。统一根为 %TEMP%\ps12exe\:更新检查的 txt 直接放根下,各后端的编译缓存放 # cache\<name>\(如 cache\codedom、cache\core)。这里只放与后端无关的东西:缓存根定位、内容哈希、 # 定长占位桶、原子读写,以及托管 PE 内嵌资源的定位与原地补丁。 # 临时根(不创建目录)。 function Get-TempRoot { return (Join-Path ([System.IO.Path]::GetTempPath()) 'ps12exe') } # 指定后端(codedom/core)的缓存目录,确保存在后返回。 function Get-CacheRoot([string]$Name) { $root = Join-Path (Join-Path (Get-TempRoot) 'cache') $Name New-Item -ItemType Directory -Force -Path $root | Out-Null return $root } # 删除长期未用的缓存项(活跃文件刚被 touch,不会命中)。 function Clear-StaleCache([string]$Root, [int]$Days = 14) { $cutoff = [DateTime]::UtcNow.AddDays(-$Days) Get-ChildItem -LiteralPath $Root -Force -ErrorAction Ignore | Where-Object { $_.LastWriteTimeUtc -lt $cutoff } | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Ignore } } function Get-Sha256Hex([byte[]]$Bytes) { $sha = [System.Security.Cryptography.SHA256]::Create() try { return [System.BitConverter]::ToString($sha.ComputeHash($Bytes)).Replace('-', '') } finally { $sha.Dispose() } } function Get-TextHash([string]$Text) { Get-Sha256Hex ([System.Text.Encoding]::UTF8.GetBytes($Text)) } # 找 >= Needed 的定长占位桶(64B 粒度)。占位桶决定帧模板里托管资源槽的大小,进而决定产物大小: # 粒度太粗会让「实际资源远小于槽」的产物白白多出最多一个粒度的填充。64B 粒度把额外体积压到 <64B, # 同时模板按内容寻址、不同大小各存一份,条目数上界仍是不同脚本大小的数量级。 # 超过 64MB 返回 0,表示放弃缓存走直编。 function Get-CacheBucket([long]$Needed) { if ($Needed -gt 67108864) { return 0 } [long]$b = [math]::Ceiling($Needed / 64.0) * 64 if ($b -lt 64) { $b = 64 } return [int]$b } # 编译管线源码指纹,作为产物结果缓存的失效键:入口 ps12exe.ps1 + src 顶层脚本 + 程序帧 + 运行时芯, # 任一内容变化即失效(用 src/*.ps1 通配而非逐文件清单,新增顶层脚本自动纳入)。 # 目录缺失(例如被 ps12exe 自编译成 exe、运行时没有源文件)时返回 $null,调用方据此禁用缓存。 $script:CompilerSourceFingerprint = $null $script:CompilerSourceFingerprintRepo = $null function Get-CompilerSourceFingerprint([string]$RepoRoot) { if ($script:CompilerSourceFingerprint -and $script:CompilerSourceFingerprintRepo -eq $RepoRoot) { return $script:CompilerSourceFingerprint } $entry = Join-Path $RepoRoot 'ps12exe.ps1' if (-not (Test-Path -LiteralPath $entry -PathType Leaf)) { return $null } $files = [System.Collections.Generic.List[string]]::new() $files.Add($entry) foreach ($pattern in @('src/*.ps1', 'src/programFrames/*.cs', 'src/RuntimePwsh2.0/*.ps1')) { $dir = Join-Path $RepoRoot (Split-Path -Path $pattern -Parent) if (-not (Test-Path -LiteralPath $dir -PathType Container)) { continue } Get-ChildItem -LiteralPath $dir -File -Filter (Split-Path -Path $pattern -Leaf) -ErrorAction Ignore | ForEach-Object { $files.Add($_.FullName) } } $sha = [System.Security.Cryptography.SHA256]::Create() try { $sb = [System.Text.StringBuilder]::new() foreach ($f in @($files | Sort-Object)) { $rel = $f.Substring($RepoRoot.Length).TrimStart('\', '/').Replace('\', '/') $h = [System.BitConverter]::ToString($sha.ComputeHash([System.IO.File]::ReadAllBytes($f))).Replace('-', '') [void]$sb.Append($rel).Append(':').Append($h).Append("`n") } $fp = [System.BitConverter]::ToString($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sb.ToString()))).Replace('-', '').Substring(0, 32) $script:CompilerSourceFingerprint = $fp $script:CompilerSourceFingerprintRepo = $RepoRoot return $fp } finally { $sha.Dispose() } } function Get-CachedBytes([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { return $null } # 触碰 mtime 以躲开 Clear-StaleCache(14 天):只在明显陈旧时才写回,省掉每次命中的元数据写入。 $item = Get-Item -LiteralPath $Path if ($item.LastWriteTimeUtc -lt [DateTime]::UtcNow.AddHours(-1)) { $item.LastWriteTimeUtc = [DateTime]::UtcNow } # 前置逗号阻止 PowerShell 展开字节数组(否则会被拆成 Object[],原地修改会丢在副本上)。 return , ([System.IO.File]::ReadAllBytes($Path)) } function Set-CachedBytes([string]$Path, [byte[]]$Bytes) { # 先写临时文件再原子替换,避免并发编译读到半截缓存。 $tmp = "$Path." + [Guid]::NewGuid().ToString('N') + '.tmp' [System.IO.File]::WriteAllBytes($tmp, $Bytes) Move-Item -LiteralPath $tmp -Destination $Path -Force } function Convert-RvaToOffset([byte[]]$Image, [int]$PeOffset, [int]$SectionCount, [int]$SectionsBase, [uint32]$Rva) { for ($i = 0; $i -lt $SectionCount; $i++) { $o = $SectionsBase + $i * 40 $va = [System.BitConverter]::ToUInt32($Image, $o + 12) $vs = [System.BitConverter]::ToUInt32($Image, $o + 8) $raw = [System.BitConverter]::ToUInt32($Image, $o + 16) $ptr = [System.BitConverter]::ToUInt32($Image, $o + 20) if ($Rva -ge $va -and $Rva -lt ($va + [math]::Max($vs, $raw))) { return [int]($ptr + ($Rva - $va)) } } return -1 } # 定位托管程序集里那份唯一内嵌资源:[Int32 长度][数据] 的长度前缀文件偏移,以及 CLI 头(其 +28 是资源目录大小)。 function Get-FrameLayout([byte[]]$Image) { $pe = [System.BitConverter]::ToInt32($Image, 0x3c) if ($pe -le 0 -or $pe + 24 -gt $Image.Length) { throw '模板不是 PE' } $sectionCount = [System.BitConverter]::ToUInt16($Image, $pe + 6) $optSize = [System.BitConverter]::ToUInt16($Image, $pe + 20) $magic = [System.BitConverter]::ToUInt16($Image, $pe + 24) $ddOff = if ($magic -eq 0x20b) { $pe + 24 + 112 } else { $pe + 24 + 96 } $sectionsBase = $pe + 24 + $optSize $clrRva = [System.BitConverter]::ToUInt32($Image, $ddOff + 14 * 8) $cliOffset = Convert-RvaToOffset $Image $pe $sectionCount $sectionsBase $clrRva if ($cliOffset -lt 0) { throw '模板缺少 CLI 头' } $resRva = [System.BitConverter]::ToUInt32($Image, $cliOffset + 24) $resOffset = Convert-RvaToOffset $Image $pe $sectionCount $sectionsBase $resRva if ($resOffset -lt 0) { throw '模板缺少托管资源' } return [pscustomobject]@{ CliOffset = $cliOffset; ResourceOffset = $resOffset } } # 把 [Int32 长度][数据] 原地写进托管 PE 的内嵌资源(数据不超过占位桶),并同步 CLI 资源目录大小;不重排 PE。 function Set-FrameResource([byte[]]$Template, [byte[]]$Data) { $layout = Get-FrameLayout $Template $capacity = [System.BitConverter]::ToInt32($Template, $layout.ResourceOffset) if ($Data.Length -gt $capacity) { throw "帧模板占位不足($($Data.Length) > $capacity)" } [System.Array]::Copy([System.BitConverter]::GetBytes([int]$Data.Length), 0, $Template, $layout.ResourceOffset, 4) [System.Array]::Copy($Data, 0, $Template, $layout.ResourceOffset + 4, $Data.Length) [System.Array]::Copy([System.BitConverter]::GetBytes([int](4 + $Data.Length)), 0, $Template, $layout.CliOffset + 28, 4) # 前置逗号阻止 PowerShell 展开字节数组(否则被拆成 Object[],大脚本会因逐字节装箱显著变慢)。 return , $Template } |