213 lines
7.7 KiB
PowerShell
213 lines
7.7 KiB
PowerShell
<#
|
||
Hua.Todo Linux 发布脚本(产出 .tar.gz)
|
||
|
||
目标:
|
||
- 为 Linux 提供可分发的目录产物(dotnet publish 输出)
|
||
- 在 Windows 开发机上也能交叉发布 linux-x64(便于 CI 前的本地验证)
|
||
|
||
约束:
|
||
- Avalonia Linux 入口项目需先落地(参见 docs/project/v1.2.0-tasks/01-*)
|
||
- 仅打包为 tar.gz;Flatpak/AppImage 需要在 Linux 环境执行相关工具链(参见 pack/linux/ 说明)
|
||
#>
|
||
|
||
param(
|
||
[ValidateSet("linux-x64", "linux-arm64")]
|
||
[string]$RuntimeIdentifier = "linux-x64",
|
||
|
||
[string]$TargetFramework = "net10.0",
|
||
|
||
[switch]$SelfContained,
|
||
|
||
[switch]$SkipProcessStop,
|
||
|
||
[switch]$SkipRestore,
|
||
|
||
[ValidateSet("Release", "Debug")]
|
||
[string]$Configuration = "Release",
|
||
|
||
[string]$BaseIntermediateOutputPath
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
$ScriptPath = $PSScriptRoot
|
||
$DirectoryBuildProps = Join-Path $ScriptPath "Directory.Build.props"
|
||
|
||
function Get-FirstExistingFilePath {
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string[]]$Candidates
|
||
)
|
||
|
||
foreach ($candidate in $Candidates) {
|
||
if (Test-Path $candidate) {
|
||
return $candidate
|
||
}
|
||
}
|
||
|
||
return $null
|
||
}
|
||
|
||
function Read-ProjectVersion {
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$ProjectFile,
|
||
|
||
[string]$DirectoryBuildProps
|
||
)
|
||
|
||
$currentVersion = "0.0.0"
|
||
|
||
# 1. Try Directory.Build.props first
|
||
if ($null -ne $DirectoryBuildProps -and (Test-Path $DirectoryBuildProps)) {
|
||
[xml]$props = Get-Content $DirectoryBuildProps -Raw
|
||
$versionNode = $props.SelectSingleNode("//Version")
|
||
if ($null -ne $versionNode -and -not [string]::IsNullOrWhiteSpace($versionNode.InnerText)) {
|
||
return $versionNode.InnerText.Trim()
|
||
}
|
||
}
|
||
|
||
# 2. Try .csproj
|
||
[xml]$csproj = Get-Content $ProjectFile -Raw
|
||
|
||
$versionNode = $csproj.SelectSingleNode("//Version")
|
||
if ($null -ne $versionNode -and -not [string]::IsNullOrWhiteSpace($versionNode.InnerText)) {
|
||
return $versionNode.InnerText.Trim()
|
||
}
|
||
|
||
$versionNode = $csproj.SelectSingleNode("//ApplicationDisplayVersion")
|
||
if ($null -ne $versionNode -and -not [string]::IsNullOrWhiteSpace($versionNode.InnerText)) {
|
||
return $versionNode.InnerText.Trim()
|
||
}
|
||
|
||
return $currentVersion
|
||
}
|
||
|
||
function Stop-ProjectProcesses {
|
||
Write-Host "Shutting down dotnet build servers..." -ForegroundColor Yellow
|
||
dotnet build-server shutdown | Out-Null
|
||
|
||
Write-Host "Checking for running processes to prevent file locks..." -ForegroundColor Yellow
|
||
|
||
# Aggressively look for anything related to the project or MSBuild/dotnet background tasks
|
||
$processesToKill = Get-Process | Where-Object {
|
||
$_.ProcessName -like "*Hua.Todo*" -or
|
||
$_.ProcessName -eq "MSBuild" -or
|
||
($_.ProcessName -eq "dotnet" -and ($_.CommandLine -like "*Hua.Todo*" -or $_.CommandLine -like "*msbuild*"))
|
||
}
|
||
|
||
if ($processesToKill) {
|
||
Write-Host "Stopping $($processesToKill.Count) running processes..." -ForegroundColor Yellow
|
||
foreach ($p in $processesToKill) {
|
||
try {
|
||
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
||
} catch {
|
||
Write-Warning "Failed to stop process $($p.ProcessName) (ID: $($p.Id))"
|
||
}
|
||
}
|
||
Start-Sleep -Seconds 2 # Give OS more time to release file handles
|
||
} else {
|
||
Write-Host "No conflicting processes found." -ForegroundColor Green
|
||
}
|
||
}
|
||
|
||
if (!$SkipProcessStop.IsPresent) {
|
||
Stop-ProjectProcesses
|
||
}
|
||
|
||
$candidateProjects = @(
|
||
(Join-Path $ScriptPath "src\Hua.Todo.Avalonia\Hua.Todo.Avalonia.csproj"),
|
||
(Join-Path $ScriptPath "src\Hua.Todo.Desktop.Avalonia\Hua.Todo.Desktop.Avalonia.csproj")
|
||
)
|
||
|
||
$ProjectFile = Get-FirstExistingFilePath -Candidates $candidateProjects
|
||
if ($null -eq $ProjectFile) {
|
||
$candidatesText = ($candidateProjects | ForEach-Object { " - $_" }) -join "`n"
|
||
Write-Error @"
|
||
未找到 Avalonia Linux 入口项目(无法生成 Linux 交付产物)。
|
||
|
||
请先完成 docs/project/v1.2.0-tasks/01-Linux-Avalonia入口与WebView.md 对应实现,并确保下列路径之一存在:
|
||
$candidatesText
|
||
"@
|
||
exit 1
|
||
}
|
||
|
||
$version = Read-ProjectVersion -ProjectFile $ProjectFile -DirectoryBuildProps $DirectoryBuildProps
|
||
$ProjectBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ProjectFile)
|
||
$artifactRoot = Join-Path $ScriptPath "artifacts\linux\$RuntimeIdentifier"
|
||
$publishDir = Join-Path $artifactRoot "publish"
|
||
|
||
if (Test-Path $artifactRoot) {
|
||
Remove-Item -Recurse -Force $artifactRoot
|
||
}
|
||
New-Item -ItemType Directory -Path $publishDir | Out-Null
|
||
|
||
$selfContainedValue = if ($SelfContained.IsPresent) { "true" } else { "false" }
|
||
|
||
if ($Configuration -ne "Release") {
|
||
Write-Host "⚠️ WARNING: You are publishing in $Configuration configuration!" -ForegroundColor Yellow
|
||
Write-Host " Typically, production artifacts MUST be in Release configuration." -ForegroundColor Yellow
|
||
Write-Host ""
|
||
}
|
||
|
||
if (!$SkipRestore.IsPresent) {
|
||
Write-Host "Restoring $ProjectBaseName for $RuntimeIdentifier ($TargetFramework)..." -ForegroundColor Yellow
|
||
$restoreArgs = @("restore", $ProjectFile, "-r", $RuntimeIdentifier, "-p:IsDesktopBuild=true", "-p:SkipWebBuild=true", "--verbosity", "minimal")
|
||
if (![string]::IsNullOrWhiteSpace($BaseIntermediateOutputPath)) {
|
||
# Ensure trailing slash and avoid backslash escaping the quote in CLI
|
||
$path = $BaseIntermediateOutputPath.TrimEnd('\') + '\'
|
||
$restoreArgs += "-p:BaseIntermediateOutputPath=$path"
|
||
}
|
||
& dotnet @restoreArgs
|
||
|
||
Write-Host "Cleaning $ProjectBaseName ($TargetFramework)..." -ForegroundColor Yellow
|
||
$cleanArgs = @("clean", $ProjectFile, "-c", $Configuration, "-r", $RuntimeIdentifier, "-p:IsDesktopBuild=true", "-p:SkipWebBuild=true", "--verbosity", "minimal")
|
||
if (![string]::IsNullOrWhiteSpace($BaseIntermediateOutputPath)) {
|
||
$path = $BaseIntermediateOutputPath.TrimEnd('\') + '\'
|
||
$cleanArgs += "-p:BaseIntermediateOutputPath=$path"
|
||
}
|
||
& dotnet @cleanArgs
|
||
}
|
||
|
||
$maxAttempts = 3
|
||
$publishSuccess = $false
|
||
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
||
Write-Host "Publishing (RID=$RuntimeIdentifier, TFM=$TargetFramework, SelfContained=$selfContainedValue, Config=$Configuration, Attempt $attempt/$maxAttempts)..." -ForegroundColor Cyan
|
||
$publishArgs = @("publish", $ProjectFile, "-c", $Configuration, "-r", $RuntimeIdentifier, "--framework", $TargetFramework, "--self-contained", $selfContainedValue, "-p:IsDesktopBuild=true", "-p:SkipWebBuild=true", "-o", $publishDir)
|
||
if (![string]::IsNullOrWhiteSpace($BaseIntermediateOutputPath)) {
|
||
$path = $BaseIntermediateOutputPath.TrimEnd('\') + '\'
|
||
$publishArgs += "-p:BaseIntermediateOutputPath=$path"
|
||
}
|
||
$publishOutput = (& dotnet @publishArgs 2>&1 | Out-String)
|
||
$exitCode = $LASTEXITCODE
|
||
|
||
if ($exitCode -eq 0) {
|
||
$publishSuccess = $true
|
||
break
|
||
}
|
||
|
||
Write-Host $publishOutput
|
||
if ($attempt -lt $maxAttempts -and ($publishOutput -match 'Access to the path .* is denied|being used by another process|Sharing violation')) {
|
||
Write-Host "⚠️ Build failed due to file lock. Retrying in 3 seconds..." -ForegroundColor Yellow
|
||
Start-Sleep -Seconds 3
|
||
Stop-ProjectProcesses # Try killing processes again before retry
|
||
continue
|
||
}
|
||
|
||
Write-Error "dotnet publish failed after $attempt attempts."
|
||
exit 1
|
||
}
|
||
|
||
$packageName = "hua.todo-$version-$RuntimeIdentifier.tar.gz"
|
||
$packagePath = Join-Path $artifactRoot $packageName
|
||
|
||
Write-Host "Creating tar.gz: $packageName" -ForegroundColor Cyan
|
||
tar -C $publishDir -czf $packagePath .
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Error "tar.gz packaging failed"
|
||
exit 1
|
||
}
|
||
|
||
Write-Host "Linux artifact created: $packagePath" -ForegroundColor Green
|