<# .SYNOPSIS 发布并混淆 Flyshot 跨平台框架依赖部署包。 .DESCRIPTION 脚本先通过 dotnet publish 生成不区分运行平台的框架依赖产物,再调用环境中安装的 Obfuscar 全局工具混淆全部 Flyshot.* 自研程序集。第三方程序集不会进入混淆模块, 映射文件会保存到独立目录,不会随部署包交付。目标机器需要安装 .NET 8 运行时。 .PARAMETER OutputRoot 最终混淆部署包目录。 .PARAMETER MappingRoot Obfuscar 映射文件的保存根目录。该目录不得作为部署包交付。 .PARAMETER GenerateConfigOnly 仅生成 Obfuscar 配置,供自动化测试和问题诊断使用,不执行发布或混淆。 .PARAMETER InputDirectory GenerateConfigOnly 模式下的程序集输入目录。 .PARAMETER ObfuscatedDirectory GenerateConfigOnly 模式下的混淆输出目录。 .PARAMETER ConfigPath GenerateConfigOnly 模式下生成的 Obfuscar XML 文件路径。 .PARAMETER ObfuscateExistingPublish 仅混淆已经由 Visual Studio 或 dotnet publish 生成的现有发布目录,不再次执行发布。 .PARAMETER ExistingPublishDirectory ObfuscateExistingPublish 模式下需要原地替换自研程序集的发布目录。 #> [CmdletBinding()] param( [string]$OutputRoot, [string]$MappingRoot, [switch]$GenerateConfigOnly, [string]$InputDirectory, [string]$ObfuscatedDirectory, [string]$ConfigPath, [switch]$ObfuscateExistingPublish, [string]$ExistingPublishDirectory ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # 该清单固定了当前解决方案必须保护的全部自研程序集,避免新增或丢失模块时静默发布。 $script:FlyshotAssemblyNames = @( 'Flyshot.Server.Host.dll' 'Flyshot.ControllerClientCompat.dll' 'Flyshot.Core.Config.dll' 'Flyshot.Core.Domain.dll' 'Flyshot.Core.Planning.dll' 'Flyshot.Core.Triggering.dll' 'Flyshot.Runtime.Fanuc.dll' 'Flyshot.Runtime.Common.dll' ) <# .SYNOPSIS 将路径转换为规范化绝对路径。 .PARAMETER Path 需要规范化的文件或目录路径,可以是相对路径或绝对路径。 .OUTPUTS 返回由 System.IO.Path 解析后的绝对路径字符串。 #> function ConvertTo-AbsolutePath { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) return [System.IO.Path]::GetFullPath($Path) } <# .SYNOPSIS 检查发布目录是否包含全部预期的 Flyshot 自研程序集。 .PARAMETER Directory 待检查的发布输入目录。 .OUTPUTS 按固定清单顺序返回每个程序集的绝对路径。 .EXCEPTION 当目录不存在或任一预期程序集缺失时抛出异常,阻止生成未完整保护的发布包。 #> function Get-FlyshotAssemblyPaths { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Directory ) $absoluteDirectory = ConvertTo-AbsolutePath -Path $Directory if (-not (Test-Path -LiteralPath $absoluteDirectory -PathType Container)) { throw "程序集输入目录不存在:$absoluteDirectory" } foreach ($assemblyName in $script:FlyshotAssemblyNames) { $assemblyPath = Join-Path $absoluteDirectory $assemblyName if (-not (Test-Path -LiteralPath $assemblyPath -PathType Leaf)) { throw "发布目录缺少必须混淆的自研程序集:$assemblyName" } } # 固定清单负责阻止当前核心模块缺失,通配发现负责自动覆盖未来新增的 Flyshot 自研程序集。 $assemblyPaths = Get-ChildItem -LiteralPath $absoluteDirectory -Filter 'Flyshot.*.dll' -File | Sort-Object Name | ForEach-Object { ConvertTo-AbsolutePath -Path $_.FullName } return @($assemblyPaths) } <# .SYNOPSIS 发现当前环境中与项目目标框架匹配的 .NET 8 共享框架目录。 .DESCRIPTION 框架依赖发布不会把 Microsoft.NETCore.App 与 Microsoft.AspNetCore.App 的程序集复制到发布目录。 Obfuscar 仍需要这些目录解析继承关系,因此通过 dotnet --list-runtimes 动态发现实际安装位置。 .OUTPUTS 返回 Microsoft.NETCore.App 与 Microsoft.AspNetCore.App 最新 .NET 8 运行时的绝对目录。 .EXCEPTION 当环境未安装所需的 .NET 8 共享框架时抛出异常,避免生成无法解析依赖的混淆配置。 #> function Get-DotNetSharedFrameworkSearchPaths { [CmdletBinding()] param() $runtimeEntries = & dotnet --list-runtimes if ($LASTEXITCODE -ne 0) { throw "无法查询已安装的 .NET 运行时,dotnet --list-runtimes 退出码为 $LASTEXITCODE。" } $requiredFrameworks = @( 'Microsoft.NETCore.App' 'Microsoft.AspNetCore.App' ) $searchPaths = foreach ($frameworkName in $requiredFrameworks) { $candidates = foreach ($runtimeEntry in $runtimeEntries) { if ($runtimeEntry -match '^(?\S+)\s+(?8\.\d+\.\d+)\s+\[(?.+)\]$') { if ($Matches.name -eq $frameworkName) { [pscustomobject]@{ Version = [version]$Matches.version Path = Join-Path $Matches.root $Matches.version } } } } $selectedRuntime = $candidates | Sort-Object Version -Descending | Select-Object -First 1 if ($null -eq $selectedRuntime -or -not (Test-Path -LiteralPath $selectedRuntime.Path -PathType Container)) { throw "环境缺少 Obfuscar 解析依赖所需的 .NET 8 共享框架:$frameworkName" } ConvertTo-AbsolutePath -Path $selectedRuntime.Path } return @($searchPaths) } <# .SYNOPSIS 生成仅包含 Flyshot 自研程序集的 Obfuscar XML 配置。 .PARAMETER SourceDirectory 包含发布程序集及其依赖项的输入目录。 .PARAMETER TargetDirectory Obfuscar 写入混淆程序集的独立输出目录。 .PARAMETER DestinationPath 要写入的 Obfuscar XML 配置文件路径。 .OUTPUTS 返回生成后的 Obfuscar XML 配置绝对路径。 #> function New-ObfuscarConfiguration { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SourceDirectory, [Parameter(Mandatory)] [string]$TargetDirectory, [Parameter(Mandatory)] [string]$DestinationPath ) $absoluteSourceDirectory = ConvertTo-AbsolutePath -Path $SourceDirectory $absoluteTargetDirectory = ConvertTo-AbsolutePath -Path $TargetDirectory $absoluteDestinationPath = ConvertTo-AbsolutePath -Path $DestinationPath $assemblyPaths = Get-FlyshotAssemblyPaths -Directory $absoluteSourceDirectory $assemblySearchPaths = Get-DotNetSharedFrameworkSearchPaths $destinationParent = Split-Path -Parent $absoluteDestinationPath New-Item -ItemType Directory -Path $destinationParent -Force | Out-Null New-Item -ItemType Directory -Path $absoluteTargetDirectory -Force | Out-Null $settings = [ordered]@{ InPath = $absoluteSourceDirectory OutPath = $absoluteTargetDirectory KeepPublicApi = 'true' HidePrivateApi = 'true' RenameProperties = 'false' RenameEvents = 'false' RenameFields = 'true' SkipGenerated = 'true' SkipSpecialName = 'true' } $xmlSettings = [System.Xml.XmlWriterSettings]::new() $xmlSettings.Indent = $true $xmlSettings.Encoding = [System.Text.UTF8Encoding]::new($false) $writer = [System.Xml.XmlWriter]::Create($absoluteDestinationPath, $xmlSettings) try { $writer.WriteStartDocument() $writer.WriteStartElement('Obfuscator') foreach ($setting in $settings.GetEnumerator()) { $writer.WriteStartElement('Var') $writer.WriteAttributeString('name', [string]$setting.Key) $writer.WriteAttributeString('value', [string]$setting.Value) $writer.WriteEndElement() } foreach ($assemblySearchPath in $assemblySearchPaths) { # 框架依赖发布不携带共享框架 DLL,Obfuscar 通过这些目录解析 ASP.NET Core 与运行时依赖。 $writer.WriteStartElement('AssemblySearchPath') $writer.WriteAttributeString('path', $assemblySearchPath) $writer.WriteEndElement() } foreach ($assemblyPath in $assemblyPaths) { # 使用明确的绝对路径,兼容 Obfuscar 当前版本并避免工作目录差异。 $writer.WriteStartElement('Module') $writer.WriteAttributeString('file', $assemblyPath) # 匿名类型会被 ASP.NET Core 的 System.Text.Json 反射序列化,必须完整保留类型及构造参数元数据。 $writer.WriteStartElement('SkipType') $writer.WriteAttributeString('name', '^<>f__AnonymousType.*') $writer.WriteAttributeString('skipMethods', 'true') $writer.WriteAttributeString('skipFields', 'true') $writer.WriteAttributeString('skipProperties', 'true') $writer.WriteAttributeString('skipEvents', 'true') $writer.WriteEndElement() $writer.WriteEndElement() } $writer.WriteEndElement() $writer.WriteEndDocument() } finally { $writer.Dispose() } return $absoluteDestinationPath } <# .SYNOPSIS 确认发布依赖的外部命令已安装并可从 PATH 调用。 .PARAMETER CommandName 要检查的命令名称。 .PARAMETER InstallHint 命令缺失时展示给构建人员的安装提示。 #> function Assert-EnvironmentCommand { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$CommandName, [Parameter(Mandatory)] [string]$InstallHint ) if (-not (Get-Command $CommandName -ErrorAction SilentlyContinue)) { throw "环境缺少命令 '$CommandName'。$InstallHint" } } <# .SYNOPSIS 执行外部命令并在非零退出码时终止发布。 .PARAMETER FilePath 需要执行的外部命令。 .PARAMETER ArgumentList 按顺序传入外部命令的参数数组。 #> function Invoke-CheckedCommand { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$FilePath, [Parameter(Mandatory)] [string[]]$ArgumentList ) & $FilePath @ArgumentList if ($LASTEXITCODE -ne 0) { throw "命令执行失败,退出码为 $LASTEXITCODE:$FilePath $($ArgumentList -join ' ')" } } <# .SYNOPSIS 将 Obfuscar 输出安全替换进发布目录并隔离映射文件。 .PARAMETER PublishedDirectory dotnet publish 生成的最终部署目录。 .PARAMETER ObfuscatedOutputDirectory Obfuscar 独立输出目录。 .PARAMETER RuntimeMappingDirectory 框架依赖发布对应的映射文件保存目录。 #> function Install-ObfuscatedAssemblies { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$PublishedDirectory, [Parameter(Mandatory)] [string]$ObfuscatedOutputDirectory, [Parameter(Mandatory)] [string]$RuntimeMappingDirectory ) $assemblyNames = Get-FlyshotAssemblyPaths -Directory $PublishedDirectory | ForEach-Object { [System.IO.Path]::GetFileName($_) } foreach ($assemblyName in $assemblyNames) { $obfuscatedAssemblyPath = Join-Path $ObfuscatedOutputDirectory $assemblyName if (-not (Test-Path -LiteralPath $obfuscatedAssemblyPath -PathType Leaf)) { throw "Obfuscar 未生成预期程序集:$assemblyName" } } foreach ($assemblyName in $assemblyNames) { Copy-Item ` -LiteralPath (Join-Path $ObfuscatedOutputDirectory $assemblyName) ` -Destination (Join-Path $PublishedDirectory $assemblyName) ` -Force $pdbName = [System.IO.Path]::ChangeExtension($assemblyName, '.pdb') $obfuscatedPdbPath = Join-Path $ObfuscatedOutputDirectory $pdbName if (Test-Path -LiteralPath $obfuscatedPdbPath -PathType Leaf) { Copy-Item ` -LiteralPath $obfuscatedPdbPath ` -Destination (Join-Path $PublishedDirectory $pdbName) ` -Force } } $mappingPath = Join-Path $ObfuscatedOutputDirectory 'Mapping.txt' if (-not (Test-Path -LiteralPath $mappingPath -PathType Leaf)) { throw 'Obfuscar 未生成 Mapping.txt,无法确认混淆映射结果。' } New-Item -ItemType Directory -Path $RuntimeMappingDirectory -Force | Out-Null Copy-Item -LiteralPath $mappingPath -Destination (Join-Path $RuntimeMappingDirectory 'Mapping.txt') -Force # 部署目录不得包含符号映射,避免把重命名关系直接交付给最终用户。 $publishedMappingPath = Join-Path $PublishedDirectory 'Mapping.txt' if (Test-Path -LiteralPath $publishedMappingPath) { Remove-Item -LiteralPath $publishedMappingPath -Force } # XML 文档包含源码 /// 注释,PDB 包含调试符号;生产混淆包不得向反编译器暴露这些辅助信息。 Get-ChildItem -LiteralPath $PublishedDirectory -File | Where-Object { $_.Name -like 'Flyshot*.xml' -or $_.Name -like 'Flyshot*.pdb' } | Remove-Item -Force # Rust ICSP 原生库必须随框架依赖包一起交付,否则默认 method=icsp 会在运行时加载失败。 $nativePlanningLibraryPath = Join-Path $PublishedDirectory 'flyshot_planning.dll' if (-not (Test-Path -LiteralPath $nativePlanningLibraryPath -PathType Leaf)) { throw "发布目录缺少 Rust 原生规划库 flyshot_planning.dll:$nativePlanningLibraryPath" } } <# .SYNOPSIS 发布并混淆 Flyshot 框架依赖主程序。 .PARAMETER RepositoryRoot Flyshot Replacement 仓库根目录。 .PARAMETER PublishedOutputDirectory 最终混淆部署包目录。 .PARAMETER MappingOutputRoot 全部混淆映射文件的根目录。 #> function Publish-ObfuscatedFrameworkDependent { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RepositoryRoot, [Parameter(Mandatory)] [string]$PublishedOutputDirectory, [Parameter(Mandatory)] [string]$MappingOutputRoot ) $hostProject = Join-Path $RepositoryRoot 'src\Flyshot.Server.Host\Flyshot.Server.Host.csproj' $workingDirectory = Join-Path $RepositoryRoot '.tmp\obfuscation\framework-dependent' $obfuscatedOutputDirectory = Join-Path $workingDirectory 'obfuscated' $configurationPath = Join-Path $workingDirectory 'obfuscar.xml' $runtimeMappingDirectory = Join-Path $MappingOutputRoot 'framework-dependent' # 每次发布使用干净目录,避免旧程序集或旧映射掩盖本轮失败。 foreach ($directory in @($PublishedOutputDirectory, $workingDirectory, $runtimeMappingDirectory)) { if (Test-Path -LiteralPath $directory) { Remove-Item -LiteralPath $directory -Recurse -Force } } New-Item -ItemType Directory -Path $PublishedOutputDirectory -Force | Out-Null Write-Host '正在发布跨平台框架依赖包 ...' Invoke-CheckedCommand -FilePath 'dotnet' -ArgumentList @( 'publish' $hostProject '-c' 'Release' '--self-contained' 'false' '-p:PublishSingleFile=false' '-p:UseAppHost=false' '-p:DebugType=none' '-o' $PublishedOutputDirectory ) New-ObfuscarConfiguration ` -SourceDirectory $PublishedOutputDirectory ` -TargetDirectory $obfuscatedOutputDirectory ` -DestinationPath $configurationPath | Out-Null Write-Host '正在混淆框架依赖包中的 Flyshot 自研程序集 ...' Invoke-CheckedCommand -FilePath 'obfuscar.console' -ArgumentList @($configurationPath) Install-ObfuscatedAssemblies ` -PublishedDirectory $PublishedOutputDirectory ` -ObfuscatedOutputDirectory $obfuscatedOutputDirectory ` -RuntimeMappingDirectory $runtimeMappingDirectory Write-Host "已生成跨平台框架依赖混淆包:$PublishedOutputDirectory" } <# .SYNOPSIS 混淆 Visual Studio 已生成的现有发布目录。 .PARAMETER PublishedDirectory 包含全部发布文件的目录;混淆成功后会原地替换其中的 Flyshot 自研程序集。 .PARAMETER RepositoryRoot Flyshot Replacement 仓库根目录。 .PARAMETER MappingOutputRoot 混淆映射文件的根目录;映射文件不会进入发布目录。 #> function Protect-ExistingPublishedDirectory { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$PublishedDirectory, [Parameter(Mandatory)] [string]$RepositoryRoot, [Parameter(Mandatory)] [string]$MappingOutputRoot ) $absolutePublishedDirectory = ConvertTo-AbsolutePath -Path $PublishedDirectory $workingDirectory = Join-Path $RepositoryRoot '.tmp\obfuscation\visual-studio\framework-dependent' $obfuscatedOutputDirectory = Join-Path $workingDirectory 'obfuscated' $configurationPath = Join-Path $workingDirectory 'obfuscar.xml' $runtimeMappingDirectory = Join-Path $MappingOutputRoot 'framework-dependent' if (-not (Test-Path -LiteralPath $absolutePublishedDirectory -PathType Container)) { throw "Visual Studio 发布目录不存在:$absolutePublishedDirectory" } # 后处理必须使用独立输出目录,避免 Obfuscar 读写同一个程序集导致文件锁或半成品。 foreach ($directory in @($workingDirectory, $runtimeMappingDirectory)) { if (Test-Path -LiteralPath $directory) { Remove-Item -LiteralPath $directory -Recurse -Force } } New-ObfuscarConfiguration ` -SourceDirectory $absolutePublishedDirectory ` -TargetDirectory $obfuscatedOutputDirectory ` -DestinationPath $configurationPath | Out-Null Write-Host "正在混淆 Visual Studio 发布目录中的 Flyshot 自研程序集:$absolutePublishedDirectory" Invoke-CheckedCommand -FilePath 'obfuscar.console' -ArgumentList @($configurationPath) Install-ObfuscatedAssemblies ` -PublishedDirectory $absolutePublishedDirectory ` -ObfuscatedOutputDirectory $obfuscatedOutputDirectory ` -RuntimeMappingDirectory $runtimeMappingDirectory Write-Host "Visual Studio 发布目录混淆完成:$absolutePublishedDirectory" } try { if ($GenerateConfigOnly) { foreach ($requiredValue in @{ InputDirectory = $InputDirectory ObfuscatedDirectory = $ObfuscatedDirectory ConfigPath = $ConfigPath }.GetEnumerator()) { if ([string]::IsNullOrWhiteSpace([string]$requiredValue.Value)) { throw "GenerateConfigOnly 模式缺少参数:$($requiredValue.Key)" } } New-ObfuscarConfiguration ` -SourceDirectory $InputDirectory ` -TargetDirectory $ObfuscatedDirectory ` -DestinationPath $ConfigPath | Out-Null exit 0 } $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if ([string]::IsNullOrWhiteSpace($OutputRoot)) { $OutputRoot = Join-Path $repositoryRoot 'outputs\obfuscated\framework-dependent' } if ([string]::IsNullOrWhiteSpace($MappingRoot)) { $MappingRoot = Join-Path $repositoryRoot 'outputs\obfuscation-maps' } Assert-EnvironmentCommand -CommandName 'dotnet' -InstallHint '请安装仓库 global.json 指定的 .NET SDK。' Assert-EnvironmentCommand ` -CommandName 'obfuscar.console' ` -InstallHint '请执行:dotnet tool install --global Obfuscar.GlobalTool --version 2.2.50' if ($ObfuscateExistingPublish) { if ([string]::IsNullOrWhiteSpace($ExistingPublishDirectory)) { throw 'ObfuscateExistingPublish 模式缺少参数:ExistingPublishDirectory' } Protect-ExistingPublishedDirectory ` -PublishedDirectory $ExistingPublishDirectory ` -RepositoryRoot $repositoryRoot ` -MappingOutputRoot (ConvertTo-AbsolutePath -Path $MappingRoot) exit 0 } Publish-ObfuscatedFrameworkDependent ` -RepositoryRoot $repositoryRoot ` -PublishedOutputDirectory (ConvertTo-AbsolutePath -Path $OutputRoot) ` -MappingOutputRoot (ConvertTo-AbsolutePath -Path $MappingRoot) Write-Host 'Flyshot 跨平台框架依赖混淆发布包已生成。' exit 0 } catch { Write-Error $_ exit 1 }