src/CodeDomCompiler.ps1
|
$type = ('System.Collections.Generic.Dictionary`2') -as "Type" $type = $type.MakeGenericType(@([String], [String]) ) $o = [Activator]::CreateInstance($type) if ($isPwsh20Sma) { $o.Add("CompilerVersion", "v3.5") } else { $o.Add("CompilerVersion", "v4.0") } $cop = (New-Object Microsoft.CSharp.CSharpCodeProvider($o)) [string[]]$BaseCompilerOptions = @($CompilerOptions) $manifestParam = if (($AstAnalyzeResult.IsConst -or $virtualize) -and -not $requireAdmin) { "/nowin32manifest" } elseif ($requireAdmin -or $DPIAware -or $supportOS -or $longPaths) { "`"/win32manifest:$($outputFile+".win32manifest")`"" @" <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> $(if ($DPIAware -or $longPaths) {@" <application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> $(if ($DPIAware) {@" <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> "@})$(if ($longPaths) {@" <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> "@}) </windowsSettings> </application> "@})$(if ($requireAdmin) {@" <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> "@})$(if ($supportOS) {@" <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> </application> </compatibility> "@}) </assembly> "@ | Set-Content ($outputFile + ".win32manifest") -Encoding UTF8 } [string[]]$CompilerOptions = $BaseCompilerOptions if ($virtualize) { Write-I18n Host ForceX86byVirtualization $architecture = "x86" } $CompilerOptions += "/platform:$architecture" $CompilerOptions += "/target:$( if ($noConsole){'winexe'}else{'exe'})" $CompilerOptions += $manifestParam $configFileForEXE3 = @" <?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> $(if ($winFormsDPIAware) {'<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7"/>'} else {'<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>'}) </startup> $(if ($longPaths) {@' <runtime> <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false"/> </runtime> '@})$( if ($winFormsDPIAware) {@' <System.Windows.Forms.ApplicationConfigurationSection> <add key="DpiAwareness" value="PerMonitorV2"/> </System.Windows.Forms.ApplicationConfigurationSection> '@}) </configuration> "@ if ($iconFile) { $CompilerOptions += "`"/win32icon:$iconFile`"" } $CompilerOptions += "/define:$($Constants -join ';')" function New-CompilerParameters([string]$outFile, [string[]]$opts, [bool]$debug) { $p = New-Object System.CodeDom.Compiler.CompilerParameters($referenceAssembies, $outFile) $p.GenerateInMemory = $FALSE $p.GenerateExecutable = $TRUE $p.IncludeDebugInformation = $debug $p.CompilerOptions = ($opts -ne '') -join ' ' $p.TempFiles = New-Object System.CodeDom.Compiler.TempFileCollection($TempDir) if ($debug) { $p.TempFiles.KeepFiles = $TRUE } Write-Debug "Using Compiler Options: $($p.CompilerOptions)" return $p } # ---------- 程序帧模板缓存 ---------- # 打包路径要把「帧 + 脚本」编成 payload、再把 gzip(payload) 塞进 launcher,每次跑两次 csc;但帧的 IL # 只由「帧源码 + 编译选项 + 引用 + 编译器版本」决定,脚本 / gz 都只是内嵌资源。于是把帧预编成模板缓存: # 模板里嵌一个 bucket 大小的占位资源,每次编译用 Set-FrameResource 原地覆写 [Int32 长度][数据] 并 # 更新 CLI 资源目录大小(不重排 PE),随后照旧走 ExeSinker——产物大小与不缓存时一致。模板键哈希帧源码、 # 选项、引用、资源/版本源与占位桶等全部编译输入,所以任何输入变化都会自动生成新模板。 # 公共缓存/PE 逻辑在 src/Cache.ps1,缓存目录 %TEMP%\ps12exe\cache\codedom。 $script:CacheRoot = Get-CacheRoot 'codedom' Clear-StaleCache $script:CacheRoot # 取帧模板字节;缺失则在命名互斥量保护下用 csc 编一次并缓存。模板只含占位资源,生成后不再改动。 # AssemblyName 决定 csc 的 assembly name(payload 与 launcher 都用固定名,不随输出名变化,以便跨输出名复用模板)。 function Get-FrameTemplate([string]$Key, [int]$Bucket, [string[]]$Options, [string]$Source, [string]$ResourceName, [string]$AssemblyName) { $cachePath = Join-Path $script:CacheRoot "frame_$Key.exe" $bytes = Get-CachedBytes $cachePath if ($bytes) { return , $bytes } $mutex = [System.Threading.Mutex]::new($false, "ps12exe-frame-$Key") $locked = $mutex.WaitOne(120000) try { if (-not $locked) { return $null } $bytes = Get-CachedBytes $cachePath if ($bytes) { return , $bytes } $buildDir = Join-Path $script:CacheRoot ("build_" + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $buildDir -Force | Out-Null try { $phPath = Join-Path $buildDir $ResourceName [System.IO.File]::WriteAllBytes($phPath, [byte[]]::new($Bucket)) $templateOut = Join-Path $buildDir $AssemblyName $p = New-CompilerParameters $templateOut $Options $FALSE [VOID]$p.EmbeddedResources.Add($phPath) $r = $cop.CompileAssemblyFromSource($p, $Source) if ($r.Errors.Count -gt 0) { throw ($r.Errors -join "`n") } $bytes = [System.IO.File]::ReadAllBytes($templateOut) Set-CachedBytes $cachePath $bytes return , $bytes } finally { Remove-Item -LiteralPath $buildDir -Recurse -Force -ErrorAction Ignore } } finally { if ($locked) { try { $mutex.ReleaseMutex() } catch {} } $mutex.Dispose() } } # 默认路径:先编出普通托管程序集作为负载,gzip 后塞进一个极小的 launcher 里。launcher 启动时在内存中解压并用 Assembly.Load 载入负载,因此负载不会落到磁盘。仅当无法打包时才退化为普通编译(Build.KeepSource 需要负载源码/PDB、Build.DllExports、真实 PS2 SMA)。常量脚本的 constexpr.cs 入口是无参 Main(),与 pack launcher 的 Main(string[]) 调用约定不符,故不走 pack。 $packEnabled = ( -not $prepareDebug -and -not $isPwsh20Sma -and -not $DllExportList -and -not $AstAnalyzeResult.IsConst -and $TempDir ) # 帧里的资源/版本属性片段占位标记:payload 编译替换为空,launcher/直编替换为 $resourceAttributes(见 BuildFrame.ps1)。 $assemblyAttributesMarker = '/*__ASSEMBLY_ATTRIBUTES__*/' if ($packEnabled) { $payloadSource = $programFrame.Replace($assemblyAttributesMarker, '') $payloadOptions = @($BaseCompilerOptions) + @( "/platform:$architecture", "/target:exe", "/nowin32manifest", "/define:$($Constants -join ';')" ) $exeSinker = Join-Path $PSScriptRoot 'ExeSinker.ps1' [byte[]]$scriptBytes = [System.IO.File]::ReadAllBytes("$TempDir\main.ps1") $payloadPath = Join-Path $TempDir 'payload.exe' # payload:优先用帧模板补丁(模板不随脚本内容失效),不可用时直编。 [byte[]]$payloadBytes = $null $versionKey = if ($isPwsh20Sma) { 'v3.5' } else { 'v4.0' } $payloadBucket = Get-CacheBucket $scriptBytes.Length if ($payloadBucket) { $payloadKey = Get-TextHash (@( $payloadSource, ($payloadOptions -join "`n"), (($referenceAssembies | Where-Object { $_ }) -join ';'), "c=$versionKey", "b=$payloadBucket" ) -join "`n") try { $template = Get-FrameTemplate $payloadKey $payloadBucket $payloadOptions $payloadSource 'main.ps1' "payload.exe" $template = Set-FrameResource $template $scriptBytes [System.IO.File]::WriteAllBytes($payloadPath, $template) $payloadBytes = $template } catch { Write-Debug "CodeDom compiler: payload frame template failed: $_" } } if (-not $payloadBytes) { Write-Debug 'CodeDom compiler: payload frame template miss, compiling' $pcp = New-CompilerParameters $payloadPath $payloadOptions $FALSE [VOID]$pcp.EmbeddedResources.Add("$TempDir\main.ps1") $pcr = $cop.CompileAssemblyFromSource($pcp, $payloadSource) if ($pcr.Errors.Count -gt 0) { throw $pcr.Errors -join "`n" } } # csc 默认会塞进 manifest/版本信息资源;负载用不到这些,先剥掉再压缩省一点。 if (Test-Path $exeSinker) { & $exeSinker $payloadPath -removeResources } $payloadBytes = [System.IO.File]::ReadAllBytes($payloadPath) # 负载整体 gzip 到内存(launcher 补丁需要其字节)。 $gzMs = New-Object System.IO.MemoryStream $gzip = New-Object System.IO.Compression.GZipStream($gzMs, [System.IO.Compression.CompressionMode]::Compress, $true) try { $gzip.Write($payloadBytes, 0, $payloadBytes.Length) } finally { $gzip.Dispose() } [byte[]]$gzBytes = $gzMs.ToArray() $gzMs.Dispose() $gzPath = Join-Path $TempDir 'main' # 和 default.cs 一样:编译进 ps12exe.exe 时内嵌 pack.cs,脚本模式从磁盘读取。 #_if PSEXE #_include_as_value launcherSource "$PSScriptRoot/programFrames/pack.cs" #_else [string]$launcherSource = Get-Content $PSScriptRoot/programFrames/pack.cs -Raw -Encoding UTF8 #_endif # pack.cs 保持纯 C#;把帧里的标记替换为资源/版本属性片段,由 launcher 携带这些元数据。 $launcherSource = $launcherSource.Replace($assemblyAttributesMarker, $resourceAttributes) [string[]]$LauncherCompilerOptions = $CompilerOptions if (-not $manifestParam) { # 没有自定义清单需求时,launcher 也不需要默认清单。 $LauncherCompilerOptions += "/nowin32manifest" } # launcher:同样优先用帧模板补丁(模板不随脚本内容失效;清单/图标走内容哈希入键)。 $launcherPatched = $false $launcherBucket = Get-CacheBucket $gzBytes.Length if ($launcherBucket) { $iconHash = if ($iconFile -and (Test-Path -LiteralPath $iconFile)) { Get-Sha256Hex ([System.IO.File]::ReadAllBytes($iconFile)) } else { '' } $manifestPath = $outputFile + '.win32manifest' $manifestHash = if ($manifestParam -and (Test-Path -LiteralPath $manifestPath)) { Get-Sha256Hex ([System.IO.File]::ReadAllBytes($manifestPath)) } else { '' } # 清单/图标路径随 outputFile 变化但不影响编译结果:键里抹掉路径、改用内容哈希。 $launcherOptionsForKey = ($LauncherCompilerOptions | ForEach-Object { ($_ -replace '(?i)(?<=/win32manifest:)[^"]*', '<m>') -replace '(?i)(?<=/win32icon:)[^"]*', '<i>' }) -join "`n" # 内部程序集名固定为 output,不随输出名变化:控制台下 $PSCommandPath 取自 exe 路径; # 窗口化标题在运行期回退到 exe 文件名(见 default.cs),因此产物可跨输出名复用同一模板。 $launcherKey = Get-TextHash (@( $launcherSource, $launcherOptionsForKey, (($referenceAssembies | Where-Object { $_ }) -join ';'), "m=$manifestHash", "i=$iconHash", "b=$launcherBucket" ) -join "`n") try { $template = Get-FrameTemplate $launcherKey $launcherBucket $LauncherCompilerOptions $launcherSource 'main' "output.exe" $template = Set-FrameResource $template $gzBytes [System.IO.File]::WriteAllBytes($outputFile, $template) $launcherPatched = $true } catch { Write-Debug "CodeDom compiler: launcher frame template failed: $_" } } if (-not $launcherPatched) { Write-Debug 'CodeDom compiler: launcher frame template miss, compiling' [System.IO.File]::WriteAllBytes($gzPath, $gzBytes) $lcp = New-CompilerParameters $outputFile $LauncherCompilerOptions $FALSE [VOID]$lcp.EmbeddedResources.Add($gzPath) $cr = $cop.CompileAssemblyFromSource($lcp, $launcherSource) if ($cr.Errors.Count -gt 0) { throw $cr.Errors -join "`n" } } } else { $cp = New-CompilerParameters $outputFile $CompilerOptions $prepareDebug if (!$AstAnalyzeResult.IsConst) { [VOID]$cp.EmbeddedResources.Add("$TempDir\main.ps1") } $cr = $cop.CompileAssemblyFromSource($cp, $programFrame.Replace($assemblyAttributesMarker, $resourceAttributes)) if ($cr.Errors.Count -gt 0) { throw $cr.Errors -join "`n" } } if ( #_if PSEXE #_!! $AstAnalyzeResult.IsConst -or #_endif $requireAdmin -or $DPIAware -or $supportOS -or $longPaths ) { if (Test-Path $($outputFile + ".win32manifest")) { Remove-Item $($outputFile + ".win32manifest") -Verbose:$FALSE } } |