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);
}