diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ceeb801..dd72a74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -179,6 +179,7 @@ jobs: -p:PublishSingleFile=false ` -p:DebugType=none ` -p:DebugSymbols=false ` + -p:SkipAirAppHostBuild=true ` -p:PublishTrimmed=false ` -p:PublishReadyToRun=false ` -p:Version=${{ needs.prepare.outputs.version }} ` @@ -193,6 +194,7 @@ jobs: -p:PublishSingleFile=false ` -p:DebugType=none ` -p:DebugSymbols=false ` + -p:SkipAirAppHostBuild=true ` -p:PublishTrimmed=false ` -p:PublishReadyToRun=false ` -p:Version=${{ needs.prepare.outputs.version }} ` @@ -202,6 +204,48 @@ jobs: } shell: pwsh + - name: Publish AirAppHost + run: | + $arch = "${{ matrix.arch }}" + $selfContained = "${{ matrix.self_contained }}" -eq "true" + $publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" } + + if ($selfContained) { + dotnet publish LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj ` + -c Release ` + -o ./$publishDir ` + --self-contained:false ` + -r win-$arch ` + -p:PublishSingleFile=false ` + -p:DebugType=none ` + -p:DebugSymbols=false ` + -p:PublishTrimmed=false ` + -p:PublishReadyToRun=false ` + -p:BuildingAirAppHost=true ` + -p:SkipAirAppHostBuild=true ` + -p:Version=${{ needs.prepare.outputs.version }} ` + -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} ` + -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` + -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} + } else { + dotnet publish LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj ` + -c Release ` + -o ./$publishDir ` + --self-contained:false ` + -p:PublishSingleFile=false ` + -p:DebugType=none ` + -p:DebugSymbols=false ` + -p:PublishTrimmed=false ` + -p:PublishReadyToRun=false ` + -p:BuildingAirAppHost=true ` + -p:SkipAirAppHostBuild=true ` + -p:Version=${{ needs.prepare.outputs.version }} ` + -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} ` + -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` + -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} + } + shell: pwsh + - name: Restructure for Launcher run: | $version = "${{ needs.prepare.outputs.version }}" @@ -227,6 +271,18 @@ jobs: Move-Item -Path $newStructure -Destination $publishDir -Force shell: pwsh + - name: Optimize and Guard Windows Payload + run: | + $arch = "${{ matrix.arch }}" + $selfContained = "${{ matrix.self_contained }}" -eq "true" + $publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" } + + ./LanMountainDesktop/scripts/Optimize-PublishPayload.ps1 ` + -PublishDir $publishDir ` + -RuntimeIdentifier "win-$arch" ` + -AssertClean + shell: pwsh + - name: Install Inno Setup and 7z run: | choco install innosetup -y --no-progress @@ -418,6 +474,7 @@ jobs: -p:SelfContained=true \ -p:DebugType=none \ -p:DebugSymbols=false \ + -p:SkipAirAppHostBuild=true \ -p:PublishTrimmed=false \ -p:PublishReadyToRun=false \ -p:Version=${{ needs.prepare.outputs.version }} \ @@ -606,6 +663,7 @@ jobs: -p:SelfContained=true \ -p:DebugType=none \ -p:DebugSymbols=false \ + -p:SkipAirAppHostBuild=true \ -p:PublishTrimmed=false \ -p:PublishReadyToRun=false \ -p:Version=${{ needs.prepare.outputs.version }} \ diff --git a/LanMountainDesktop.Launcher/Services/AirApp/AirAppHostLocator.cs b/LanMountainDesktop.Launcher/Services/AirApp/AirAppHostLocator.cs index 3967eb4..ebd0b31 100644 --- a/LanMountainDesktop.Launcher/Services/AirApp/AirAppHostLocator.cs +++ b/LanMountainDesktop.Launcher/Services/AirApp/AirAppHostLocator.cs @@ -33,6 +33,8 @@ internal sealed class AirAppHostLocator { yield return Path.Combine(deploymentDirectory, "AirAppHost", WindowsExecutableName); yield return Path.Combine(deploymentDirectory, "AirAppHost", DllName); + yield return Path.Combine(deploymentDirectory, WindowsExecutableName); + yield return Path.Combine(deploymentDirectory, DllName); } } } diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 69b9eaa..c992aa9 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -126,9 +126,11 @@ SkipUnchangedFiles="true" /> - + - <_AirAppHostPublishOutput Include="..\LanMountainDesktop.AirAppHost\bin\$(Configuration)\$(TargetFramework)\**\*" /> + <_AirAppHostPublishOutput Include="$(AirAppHostPublishDir)\**\*" /> " } else { $group.Name } + Count = $group.Count + Bytes = [long]$bytes + } + } + + foreach ($row in @($extensionRows | Sort-Object Bytes -Descending)) { + Write-Host (" {0,10} {1,5} {2}" -f (Format-Size -Bytes $row.Bytes), $row.Count, $row.Extension) + } + + $runtimeRoots = @(Get-ChildItem -LiteralPath $Root -Recurse -Directory -Filter "runtimes" -ErrorAction SilentlyContinue) + if ($runtimeRoots.Count -gt 0) { + Write-Host "Runtime directories:" + foreach ($runtimeRoot in $runtimeRoots) { + Get-ChildItem -LiteralPath $runtimeRoot.FullName -Directory -ErrorAction SilentlyContinue | + Sort-Object Name | + ForEach-Object { + $relative = Get-RelativePathCompat -Root $Root -Path $_.FullName + Write-Host (" {0,10} {1}" -f (Format-Size -Bytes (Get-DirectorySize -Path $_.FullName)), $relative) + } + } + } +} + +function Remove-PdbFiles { + param([Parameter(Mandatory = $true)][string]$Root) + + if ($KeepSymbols) { + Write-Host "Keeping PDB files because -KeepSymbols was specified." + return + } + + $pdbFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter "*.pdb" -ErrorAction SilentlyContinue) + foreach ($file in $pdbFiles) { + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop + } + + Write-Host "Removed PDB files: $($pdbFiles.Count)" +} + +function Remove-NonTargetRuntimeDirectories { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string]$Rid + ) + + if ($Rid -notlike "win-*") { + return + } + + $runtimeRoots = @(Get-ChildItem -LiteralPath $Root -Recurse -Directory -Filter "runtimes" -ErrorAction SilentlyContinue) + $removed = 0 + foreach ($runtimeRoot in $runtimeRoots) { + Get-ChildItem -LiteralPath $runtimeRoot.FullName -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ne $Rid } | + ForEach-Object { + Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop + $removed++ + } + } + + Write-Host "Removed non-target runtime directories: $removed" +} + +function Assert-WindowsPayloadClean { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string]$Rid + ) + + if ($Rid -notlike "win-*") { + return + } + + $violations = [System.Collections.Generic.List[string]]::new() + $forbiddenExtensions = @(".pdb", ".so", ".dylib", ".a") + + Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $forbiddenExtensions -contains $_.Extension.ToLowerInvariant() } | + ForEach-Object { + $violations.Add((Get-RelativePathCompat -Root $Root -Path $_.FullName)) + } + + Get-ChildItem -LiteralPath $Root -Recurse -Directory -Filter "runtimes" -ErrorAction SilentlyContinue | + ForEach-Object { + Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ne $Rid } | + ForEach-Object { + $violations.Add((Get-RelativePathCompat -Root $Root -Path $_.FullName)) + } + } + + if ($violations.Count -gt 0) { + $sample = ($violations | Select-Object -First 50) -join [Environment]::NewLine + throw "Windows publish payload contains forbidden files or runtime directories for ${Rid}:$([Environment]::NewLine)$sample" + } + + Write-Host "Windows payload guard passed for $Rid." +} + +$resolvedPublishDir = [System.IO.Path]::GetFullPath($PublishDir) +if (-not (Test-Path -LiteralPath $resolvedPublishDir)) { + throw "Publish directory not found: $resolvedPublishDir" +} + +Write-Host "Optimizing publish payload for $RuntimeIdentifier..." +Remove-PdbFiles -Root $resolvedPublishDir +Remove-NonTargetRuntimeDirectories -Root $resolvedPublishDir -Rid $RuntimeIdentifier +Write-PayloadAudit -Root $resolvedPublishDir + +if ($AssertClean) { + Assert-WindowsPayloadClean -Root $resolvedPublishDir -Rid $RuntimeIdentifier +} diff --git a/LanMountainDesktop/scripts/package.ps1 b/LanMountainDesktop/scripts/package.ps1 index e01064c..e56c6ad 100644 --- a/LanMountainDesktop/scripts/package.ps1 +++ b/LanMountainDesktop/scripts/package.ps1 @@ -199,6 +199,60 @@ function Add-LinuxDesktopAssets { Copy-Item -LiteralPath $installScriptSource -Destination (Join-Path $PublishedDirectory "install.sh") -Force } +function Invoke-PublishPayloadOptimization { + param( + [Parameter(Mandatory = $true)][string]$PublishedDirectory, + [Parameter(Mandatory = $true)][string]$Rid + ) + + $optimizer = Join-Path $scriptRoot "Optimize-PublishPayload.ps1" + if (-not (Test-Path -LiteralPath $optimizer)) { + throw "Publish payload optimizer is missing: $optimizer" + } + + & $optimizer ` + -PublishDir $PublishedDirectory ` + -RuntimeIdentifier $Rid ` + -AssertClean ` + -KeepSymbols:$KeepSymbols + if ($LASTEXITCODE -ne 0) { + throw "Publish payload optimization failed with exit code $LASTEXITCODE." + } +} + +function Publish-AirAppHostPayload { + param( + [Parameter(Mandatory = $true)][string]$PublishedDirectory, + [Parameter(Mandatory = $true)][string]$Rid, + [Parameter(Mandatory = $true)][string]$VersionValue + ) + + $airAppHostProject = Join-Path $repoRoot "..\LanMountainDesktop.AirAppHost\LanMountainDesktop.AirAppHost.csproj" + $airAppHostProject = Resolve-ExistingPath -PathValue $airAppHostProject + Write-Host "Publishing AirAppHost payload..." + $airPublishArgs = @( + "publish", + $airAppHostProject, + "-c", $Configuration, + "-r", $Rid, + "--self-contained", "false", + "-p:PublishSingleFile=false", + "-p:PublishTrimmed=false", + "-p:PublishReadyToRun=false", + "-p:DebugType=None", + "-p:DebugSymbols=false", + "-p:BuildingAirAppHost=true", + "-p:SkipAirAppHostBuild=true", + "-p:Version=$VersionValue", + "-o", $PublishedDirectory + ) + + & dotnet @airPublishArgs + if ($LASTEXITCODE -ne 0) { + throw "AirAppHost publish failed with exit code $LASTEXITCODE." + } +} + $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Resolve-ExistingPath -PathValue (Join-Path $scriptRoot "..") @@ -231,6 +285,7 @@ $publishArgs = @( "-p:PublishTrimmed=false", "-p:DebugType=None", "-p:DebugSymbols=false", + "-p:SkipAirAppHostBuild=true", "-p:Version=$Version", "-o", $PublishDir ) @@ -240,6 +295,7 @@ if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE." } +Publish-AirAppHostPayload -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier -VersionValue $Version Remove-LibVlcForOtherArch -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier Remove-LegacyOutputArtifacts -TargetDirectory $PublishDir @@ -247,11 +303,7 @@ if ($RuntimeIdentifier -like "linux-*") { Add-LinuxDesktopAssets -PublishedDirectory $PublishDir -RepoRoot $repoRoot } -if (-not $KeepSymbols) { - Get-ChildItem -Path $PublishDir -Recurse -File -Filter "*.pdb" | ForEach-Object { - [System.IO.File]::Delete($_.FullName) - } -} +Invoke-PublishPayloadOptimization -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier if (Is-WindowsRuntimeIdentifier -Rid $RuntimeIdentifier) { if (-not $InstallerOutputDir) {