diff --git a/.github/workflows/installer-build.yml b/.github/workflows/installer-build.yml index d42a8f9..b3f0487 100644 --- a/.github/workflows/installer-build.yml +++ b/.github/workflows/installer-build.yml @@ -57,15 +57,21 @@ jobs: shell: pwsh run: | $publishDir = Join-Path $env:GITHUB_WORKSPACE '${{ env.INSTALLER_ARTIFACT_DIR }}' + $tempDir = Join-Path $env:GITHUB_WORKSPACE 'artifacts/installer-online/tmp' if (Test-Path $publishDir) { Remove-Item -LiteralPath $publishDir -Recurse -Force } New-Item -ItemType Directory -Path $publishDir -Force | Out-Null + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + $env:TEMP = $tempDir + $env:TMP = $tempDir - dotnet restore '${{ env.INSTALLER_PROJECT }}' -r '${{ env.INSTALLER_RUNTIME }}' + dotnet restore '${{ env.INSTALLER_PROJECT }}' ` + -r '${{ env.INSTALLER_RUNTIME }}' ` + -p:PublishAot=true if ($LASTEXITCODE -ne 0) { - throw "Online installer runtime restore failed with exit code $LASTEXITCODE." + throw "Online installer NativeAOT restore failed with exit code $LASTEXITCODE." } dotnet publish '${{ env.INSTALLER_PROJECT }}' ` @@ -74,6 +80,9 @@ jobs: -r '${{ env.INSTALLER_RUNTIME }}' ` -p:PublishAot=true ` -p:UseAppHost=true ` + -p:DebugType=none ` + -p:DebugSymbols=false ` + -p:StripSymbols=true ` -o $publishDir ` -v minimal @@ -90,7 +99,32 @@ jobs: Write-Warning "dotnet publish exited with $LASTEXITCODE after producing the installer artifact." } - Get-ChildItem -Path $publishDir -File -Filter 'LanDesktopPLONDS.installer*' | + Get-ChildItem -Path $publishDir -Recurse -Filter '*.pdb' | + Remove-Item -Force + + $jitFiles = @( + 'coreclr.dll', + 'clrjit.dll', + 'hostfxr.dll', + 'hostpolicy.dll', + 'LanDesktopPLONDS.installer.deps.json', + 'LanDesktopPLONDS.installer.runtimeconfig.json' + ) + foreach ($file in $jitFiles) { + if (Test-Path (Join-Path $publishDir $file)) { + throw "JIT runtime artifact found in NativeAOT output: $file" + } + } + + $unexpectedFiles = Get-ChildItem -Path $publishDir -File | + Where-Object { $_.Name -ne 'LanDesktopPLONDS.installer.exe' } + if ($unexpectedFiles) { + $names = ($unexpectedFiles | Select-Object -ExpandProperty Name) -join ', ' + throw "Unexpected files in single-exe NativeAOT installer artifact: $names" + } + + Get-ChildItem -Path $publishDir -File | + Sort-Object Name | Select-Object Name, Length - name: Upload online installer artifact diff --git a/Directory.Packages.props b/Directory.Packages.props index aa88f8a..e566f35 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ + @@ -16,6 +17,7 @@ + @@ -32,6 +34,7 @@ + @@ -40,4 +43,4 @@ - \ No newline at end of file + diff --git a/LanDesktopPLONDS.installer/App.axaml b/LanDesktopPLONDS.installer/App.axaml index dea67f8..61f871a 100644 --- a/LanDesktopPLONDS.installer/App.axaml +++ b/LanDesktopPLONDS.installer/App.axaml @@ -1,6 +1,6 @@ @@ -69,7 +69,7 @@ - + diff --git a/LanDesktopPLONDS.installer/Compress-NativeLibrary.ps1 b/LanDesktopPLONDS.installer/Compress-NativeLibrary.ps1 new file mode 100644 index 0000000..51c23c0 --- /dev/null +++ b/LanDesktopPLONDS.installer/Compress-NativeLibrary.ps1 @@ -0,0 +1,45 @@ +param( + [Parameter(Mandatory = $true)] + [string] $SourcePath, + + [Parameter(Mandatory = $true)] + [string] $DestinationPath +) + +$ErrorActionPreference = 'Stop' + +$source = Get-Item -LiteralPath $SourcePath +$destinationDirectory = Split-Path -Parent $DestinationPath +New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null + +$existing = Get-Item -LiteralPath $DestinationPath -ErrorAction SilentlyContinue +if ($existing -and $existing.LastWriteTimeUtc -ge $source.LastWriteTimeUtc -and $existing.Length -gt 0) { + return +} + +$temporaryPath = "$DestinationPath.$PID.tmp" +if (Test-Path -LiteralPath $temporaryPath) { + Remove-Item -LiteralPath $temporaryPath -Force +} + +$inputStream = [System.IO.File]::OpenRead($source.FullName) +try { + $outputStream = [System.IO.File]::Create($temporaryPath) + try { + $gzipStream = New-Object System.IO.Compression.GZipStream($outputStream, [System.IO.Compression.CompressionMode]::Compress) + try { + $inputStream.CopyTo($gzipStream) + } + finally { + $gzipStream.Dispose() + } + } + finally { + $outputStream.Dispose() + } +} +finally { + $inputStream.Dispose() +} + +Move-Item -LiteralPath $temporaryPath -Destination $DestinationPath -Force diff --git a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.AOT.props b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.AOT.props index 6fc6a74..902609b 100644 --- a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.AOT.props +++ b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.AOT.props @@ -19,12 +19,52 @@ - + + + + + + + + + + + + + + + + + + + + false false diff --git a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj index 5733597..dd44690 100644 --- a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj +++ b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj @@ -20,10 +20,13 @@ + - + + + diff --git a/LanDesktopPLONDS.installer/NativeDependencyBootstrapper.cs b/LanDesktopPLONDS.installer/NativeDependencyBootstrapper.cs new file mode 100644 index 0000000..b43ca0e --- /dev/null +++ b/LanDesktopPLONDS.installer/NativeDependencyBootstrapper.cs @@ -0,0 +1,170 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.IO.Compression; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace LanDesktopPLONDS.Installer; + +internal static class NativeDependencyBootstrapper +{ + private const string CacheRootEnvironmentVariable = "LANDESKTOPPLONDS_INSTALLER_NATIVE_CACHE"; + private const string ResourcePrefix = "LanDesktopPLONDS.Installer.NativeLibraries."; + + private static readonly string[] NativeLibraryNames = + [ + "av_libglesv2.dll", + "libHarfBuzzSharp.dll", + "libSkiaSharp.dll" + ]; + + public static void Prepare() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + var nativeDirectory = GetNativeDirectory(); + Directory.CreateDirectory(nativeDirectory); + + var extractedLibraries = new List(NativeLibraryNames.Length); + foreach (var libraryName in NativeLibraryNames) + { + extractedLibraries.Add(ExtractLibrary(nativeDirectory, libraryName)); + } + + AddToProcessDllSearchPath(nativeDirectory); + + foreach (var libraryPath in extractedLibraries) + { + NativeLibrary.Load(libraryPath); + } + } + + private static string GetNativeDirectory() + { + var configuredCacheRoot = Environment.GetEnvironmentVariable(CacheRootEnvironmentVariable); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var cacheRoot = !string.IsNullOrWhiteSpace(configuredCacheRoot) + ? configuredCacheRoot + : string.IsNullOrWhiteSpace(localAppData) + ? Path.GetTempPath() + : localAppData; + + string? versionStamp = null; + if (!string.IsNullOrWhiteSpace(Environment.ProcessPath)) + { + versionStamp = FileVersionInfo.GetVersionInfo(Environment.ProcessPath).ProductVersion; + } + + if (string.IsNullOrWhiteSpace(versionStamp)) + { + versionStamp = "dev"; + } + + return Path.Combine( + cacheRoot, + "LanDesktopPLONDS", + "Installer", + "native", + RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(), + SanitizePathSegment(versionStamp)); + } + + private static string ExtractLibrary(string nativeDirectory, string libraryName) + { + var resourceName = ResourcePrefix + libraryName + ".gz"; + var assembly = Assembly.GetExecutingAssembly(); + using var resource = assembly.GetManifestResourceStream(resourceName); + if (resource is null) + { + var availableResources = string.Join(", ", assembly.GetManifestResourceNames()); + throw new FileNotFoundException( + $"Missing embedded native installer library resource '{resourceName}'. Available resources: {availableResources}"); + } + + var destinationPath = Path.Combine(nativeDirectory, libraryName); + var temporaryPath = destinationPath + "." + Guid.NewGuid().ToString("N") + ".tmp"; + using (var gzip = new GZipStream(resource, CompressionMode.Decompress)) + using (var output = File.Create(temporaryPath)) + { + gzip.CopyTo(output); + } + + if (File.Exists(destinationPath) && FilesEqual(destinationPath, temporaryPath)) + { + File.Delete(temporaryPath); + return destinationPath; + } + + File.Move(temporaryPath, destinationPath, overwrite: true); + return destinationPath; + } + + private static void AddToProcessDllSearchPath(string nativeDirectory) + { + var currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + if (!currentPath.Contains(nativeDirectory, StringComparison.OrdinalIgnoreCase)) + { + Environment.SetEnvironmentVariable("PATH", nativeDirectory + Path.PathSeparator + currentPath); + } + + if (!SetDllDirectory(nativeDirectory)) + { + throw new Win32Exception(Marshal.GetLastPInvokeError(), "Failed to update the process native DLL search path."); + } + } + + private static string SanitizePathSegment(string value) + { + foreach (var invalidChar in Path.GetInvalidFileNameChars()) + { + value = value.Replace(invalidChar, '_'); + } + + return value; + } + + private static bool FilesEqual(string leftPath, string rightPath) + { + var left = new FileInfo(leftPath); + var right = new FileInfo(rightPath); + if (left.Length != right.Length) + { + return false; + } + + using var leftStream = File.OpenRead(leftPath); + using var rightStream = File.OpenRead(rightPath); + var leftBuffer = new byte[81920]; + var rightBuffer = new byte[81920]; + + while (true) + { + var leftRead = leftStream.Read(leftBuffer, 0, leftBuffer.Length); + var rightRead = rightStream.Read(rightBuffer, 0, rightBuffer.Length); + if (leftRead != rightRead) + { + return false; + } + + if (leftRead == 0) + { + return true; + } + + for (var i = 0; i < leftRead; i++) + { + if (leftBuffer[i] != rightBuffer[i]) + { + return false; + } + } + } + } + + [DllImport("kernel32", EntryPoint = "SetDllDirectoryW", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetDllDirectory(string pathName); +} diff --git a/LanDesktopPLONDS.installer/Program.cs b/LanDesktopPLONDS.installer/Program.cs index a976aa7..e252155 100644 --- a/LanDesktopPLONDS.installer/Program.cs +++ b/LanDesktopPLONDS.installer/Program.cs @@ -7,6 +7,7 @@ public static class Program [STAThread] public static void Main(string[] args) { + NativeDependencyBootstrapper.Prepare(); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); }