Files
FlyShotHost/scripts/Publish-Obfuscated.ps1
yunxiao.zhu 070d2ba5c0 ✨ feat(server): 增加混淆发布链路与调试页静态说明
- 新增 Obfuscar 混淆发布脚本、发布测试与产物隐私检查
- 调整 VS Code 发布任务和宿主发布配置,统一走混淆流程
- 为 debug 页面内嵌静态接口说明,并补充对应集成测试
- 更新 README 与设计计划文档,记录跨平台发布约束
2026-06-22 15:55:34 +08:00

636 lines
20 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
.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 '^(?<name>\S+)\s+(?<version>8\.\d+\.\d+)\s+\[(?<root>.+)\]$')
{
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
}
<#
.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
}