<# 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 环境执行相关工具链 #> 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