Compare commits
1 Commits
avalonia-g
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6212559b41 |
88
src/Metin2Launcher/Bootstrap/NativeLibraryBootstrap.cs
Normal file
88
src/Metin2Launcher/Bootstrap/NativeLibraryBootstrap.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Metin2Launcher.Bootstrap;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-resolves the native DLL search path so LoadLibrary calls from
|
||||
/// Avalonia / SkiaSharp find their deps when running as a .NET 8
|
||||
/// single-file self-contained executable under Wine.
|
||||
///
|
||||
/// Why this exists:
|
||||
///
|
||||
/// - .NET 8 single-file bundles the native libs (libSkiaSharp.dll,
|
||||
/// libHarfBuzzSharp.dll, libsodium.dll, av_libglesv2.dll) inside the
|
||||
/// exe and extracts them at startup to
|
||||
/// <c>$DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<hash>/</c>
|
||||
/// (default: <c>%TEMP%/.net/<app>/<hash>/</c>).
|
||||
/// - On native Windows, the CLR calls AddDllDirectory on the extract
|
||||
/// dir so LoadLibrary picks them up.
|
||||
/// - Under Wine, either wine's %TEMP% mapping trips over tmpfs during
|
||||
/// extract (we observed a thread stack overflow in virtual_setup_exception
|
||||
/// before Main even runs, with the default %TEMP%), or the DLL
|
||||
/// search path never learns about the extract dir, and Avalonia's
|
||||
/// P/Invoke hits DllNotFoundException when SkiaSharp tries to load.
|
||||
///
|
||||
/// The fix is two-part:
|
||||
///
|
||||
/// 1. Externally: set <c>DOTNET_BUNDLE_EXTRACT_BASE_DIR</c> to a path
|
||||
/// under the exe's own directory, so the extract writes onto the
|
||||
/// real filesystem (not wine's tmpfs).
|
||||
/// 2. Here: once the CLR is up and Main has been reached, iterate
|
||||
/// <see cref="Process.GetCurrentProcess"/>.Modules, find a loaded
|
||||
/// module that lives inside the extract dir, and call
|
||||
/// <c>SetDllDirectoryW</c> with that directory so subsequent
|
||||
/// LoadLibrary calls (from Avalonia, SkiaSharp, NSec, etc.)
|
||||
/// resolve their native deps.
|
||||
///
|
||||
/// The bootstrap is a no-op if we cannot identify an extract dir
|
||||
/// (e.g. on native Windows where the default resolver already works,
|
||||
/// or when the launcher is not running as a single-file bundle).
|
||||
/// </summary>
|
||||
internal static class NativeLibraryBootstrap
|
||||
{
|
||||
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool SetDllDirectoryW(string? lpPathName);
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
try
|
||||
{
|
||||
var extractDir = FindExtractDir();
|
||||
if (string.IsNullOrEmpty(extractDir))
|
||||
return;
|
||||
SetDllDirectoryW(extractDir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Intentionally swallow — bootstrap is best-effort. If it
|
||||
// fails the launcher will surface the real DllNotFoundException
|
||||
// from Avalonia later with a stack trace.
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: look at DOTNET_BUNDLE_EXTRACT_BASE_DIR + app name and
|
||||
// pick the single hash-suffixed subdirectory that contains a file
|
||||
// with a name like libSkiaSharp.dll. Avoids depending on which
|
||||
// native libs are already loaded at Main() entry (they usually are
|
||||
// not).
|
||||
private static string? FindExtractDir()
|
||||
{
|
||||
var baseDir = Environment.GetEnvironmentVariable("DOTNET_BUNDLE_EXTRACT_BASE_DIR");
|
||||
if (string.IsNullOrEmpty(baseDir))
|
||||
return null;
|
||||
var appName = Path.GetFileNameWithoutExtension(Environment.ProcessPath ?? "Metin2Launcher");
|
||||
var appBase = Path.Combine(baseDir, appName);
|
||||
if (!Directory.Exists(appBase))
|
||||
return null;
|
||||
// There should be exactly one hash-suffixed subdir per binary.
|
||||
// Pick whichever one contains the Skia native lib.
|
||||
foreach (var candidate in Directory.EnumerateDirectories(appBase))
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidate, "libSkiaSharp.dll")))
|
||||
return candidate;
|
||||
}
|
||||
// Fall back to whichever subdir exists first — better than nothing.
|
||||
return Directory.EnumerateDirectories(appBase).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Metin2Launcher.Bootstrap;
|
||||
using Metin2Launcher.Config;
|
||||
using Metin2Launcher.GameLaunch;
|
||||
using Metin2Launcher.Logging;
|
||||
@@ -19,6 +20,15 @@ public static class Program
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// Wine-friendly native lib bootstrap: when the launcher runs as a
|
||||
// .NET 8 single-file self-contained bundle under Wine, the CLR
|
||||
// extract dir is not on the DLL search path, so Avalonia and
|
||||
// SkiaSharp cannot P/Invoke their natives. Locate the extract
|
||||
// dir via the loaded module list and SetDllDirectoryW it before
|
||||
// any Avalonia code runs. No-op on native Windows and on non-
|
||||
// single-file dev builds.
|
||||
NativeLibraryBootstrap.Apply();
|
||||
|
||||
// Velopack must be wired at the very top of Main so it can handle the
|
||||
// install / firstrun / uninstall hooks without the rest of our code running.
|
||||
VelopackApp.Build().SetArgs(args).Run();
|
||||
|
||||
Reference in New Issue
Block a user