From 9d65439ff418e6322b5fdb730b58a42479f7031d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:56:01 -0700 Subject: [PATCH] fix: require only the runtime native libraries on Linux ModernUO mandated -dev packages on production servers for one reason: DllImport never asks for a versioned SONAME, so libdeflate.so.0 and libargon2.so.1 sitting in /usr/lib went unfound and the -dev package's unversioned symlink was the only thing making resolution work. That is fixed in the binding packages, so pick them up and stop asking for build tooling on machines that compile nothing. LibDeflate.Bindings 1.0.3 -> 1.0.4 Argon2.Bindings 1.17.0 -> 1.19.0 Also drops zstd, on every platform including macOS. ZstdNet bundles libzstd for linux-x64, linux-arm64, osx-x64, osx-arm64 and win, and nothing shells out to the CLI. Verified: the 15 ManagedArchive round-trip tests pass in a container with no zstd package installed. NativeLibraryChecker now asks whether the loader can find each library rather than whether a named package is installed. Package-name checks are what forced -dev in the first place, and no hardcoded name works for ICU anyway: its apt package is release-specific (libicu74 on 24.04, libicu76 on Alpine, libicu77 on Fedora). ldconfig -p is version agnostic and is the loader's own cache, so one code path replaces the apt/dnf/generic split. ldconfig is treated as a positive signal only. musl's exits 0 while producing no usable cache, so trusting a negative reported every library missing on Alpine when all were installed; anything it does not vouch for is now dlopen'd before being called missing. That fallback bounds the .so.N probe per library, because a single small bound reports ICU missing when it is present. CI installs runtime packages only, deliberately: installing -dev there would restore the unversioned symlink and mask the very resolution this depends on, so a regression would sail through. Adds --check-prereqs. The interactive flow was the only path that ran these checks, so there was no way to verify a deployment target from a script or a container. Audited the rest of the codebase for the same hazard. ModernUO's only other native imports are ws2_32.dll, which is always present on Windows, and libc in SocketHelper. libc is not affected even though libc.so is a libc6-dev linker script: DllImport("libc") was verified to resolve and call successfully on both glibc and musl with no -dev package installed. Nothing else P/Invokes, and no code here registers a DllImportResolver. Verified with 1.0.4 and 1.19.0: build plus 810 Server.Tests and 642 UOContent.Tests, and --check-prereqs reporting correctly on Debian, Ubuntu, Fedora and Alpine with only the runtime packages installed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-test.yml | 9 +- Projects/BuildTool/BuildOptions.cs | 7 + .../Prerequisites/NativeLibraryChecker.cs | 234 +++++++++++------- Projects/BuildTool/Program.cs | 29 +++ Projects/Server/Server.csproj | 2 +- Projects/UOContent/UOContent.csproj | 4 +- README.md | 13 +- 7 files changed, 196 insertions(+), 102 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 92684562b..359efdcfc 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -46,7 +46,7 @@ jobs: - name: Install Prerequisites run: | brew update - brew install icu4c libdeflate zstd argon2 + brew install icu4c libdeflate argon2 - name: Set Library Path run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - name: Build @@ -124,11 +124,14 @@ jobs: dnf config-manager --set-enabled crb dnf install -y epel-release if: ${{ matrix.epel }} + # Runtime packages only, deliberately. Installing the -dev packages here would add the + # unversioned .so symlink and mask the very thing the binding packages now probe for, so a + # regression in versioned-SONAME resolution would sail through CI. - name: Install Prerequisites using dnf - run: dnf makecache --refresh && dnf install -y findutils libicu libdeflate-devel zstd libargon2-devel + run: dnf makecache --refresh && dnf install -y findutils libicu libdeflate libargon2 if: ${{ matrix.packageManager == 'dnf' }} - name: Install Prerequisites using apt - run: apt-get update -y && apt-get install -y curl libicu-dev libdeflate-dev zstd libargon2-dev tzdata + run: apt-get update -y && apt-get install -y curl libicu-dev libdeflate0 libargon2-1 tzdata if: ${{ matrix.packageManager == 'apt' }} - uses: actions/checkout@v7 with: diff --git a/Projects/BuildTool/BuildOptions.cs b/Projects/BuildTool/BuildOptions.cs index 9aea5010d..9e1d56388 100644 --- a/Projects/BuildTool/BuildOptions.cs +++ b/Projects/BuildTool/BuildOptions.cs @@ -14,4 +14,11 @@ public sealed class BuildOptions public string? Arch { get; set; } public bool SkipPrereqs { get; set; } public bool Interactive { get; set; } + + /// + /// Report the native library prerequisites and exit. The interactive flow is the only other + /// path that runs these checks, so without this there is no way to verify a deployment target + /// from a script or a container. + /// + public bool CheckPrereqsOnly { get; set; } } diff --git a/Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs b/Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs index 4f84aa373..8050f6533 100644 --- a/Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs +++ b/Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using BuildTool.Platform; using BuildTool.Publishing; @@ -31,8 +32,9 @@ public static class NativeLibraryChecker "Linux", [ ".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0", - "Debian/Ubuntu: sudo apt-get install -y libicu-dev libdeflate-dev zstd libargon2-dev", - "Fedora/RHEL: sudo dnf install -y libicu libdeflate-devel zstd libargon2-devel", + "Debian/Ubuntu: sudo apt-get install -y libdeflate0 libargon2-1 libicuNN", + " (libicuNN varies by release, e.g. libicu76 — run build-tool --check-prereqs there for the exact name)", + "Fedora/RHEL: sudo dnf install -y libdeflate libargon2 libicu", "CentOS: Also requires epel-release and CRB enabled" ] ), @@ -176,82 +178,70 @@ public static class NativeLibraryChecker return results; } + /// + /// Libraries the server needs from the system on Linux. + /// + /// zstd is absent because ZstdNet bundles libzstd for every RID. liburing is absent because + /// IORingGroup issues io_uring syscalls directly and imports only libc, libSystem.dylib, + /// kernel32.dll, kernelbase.dll and ws2_32.dll. + /// + /// ICU is here because the server does not set InvariantGlobalization and its runtimeconfig + /// sets System.Globalization.PredefinedCulturesOnly to false, so it genuinely needs ICU. + /// + /// MaxSoVersion bounds the dlopen fallback used when ldconfig cannot answer. It is per library + /// because the SONAME digit is: libdeflate is .so.0 and libargon2 is .so.1 on the same machine, + /// while ICU tracks its own release train and was .so.74 on Ubuntu 24.04, .so.76 on Alpine and + /// .so.77 on Fedora. A single small bound silently reports ICU missing when it is installed. + /// + private static readonly (string Name, int MaxSoVersion)[] _linuxLibraries = + [ + ("libicuuc", 99), + ("libdeflate", 9), + ("libargon2", 9) + ]; + private static List CheckLinux(PlatformInfo platform) { - return platform.PackageManager switch - { - PackageManager.Apt => CheckLinuxApt(), - PackageManager.Dnf => CheckLinuxDnf(platform), - _ => CheckLinuxGeneric(platform) - }; - } + // Ask whether the loader can find each library rather than whether a named package is + // installed. Package names were why the -dev packages were mandated, and no hardcoded name + // works for ICU anyway: its apt package is release-specific (libicu72, libicu74, ...). + // ldconfig -p is the loader's own cache, so matching on the "libfoo.so" prefix covers + // libdeflate.so.0, libargon2.so.1 and libicuuc.so.76 alike. + // + // It is only ever a fast *positive* signal. musl's ldconfig exits 0 while producing no + // usable cache, so trusting a negative from it reports every library missing on Alpine even + // when all of them are installed. A cache can also be stale or omit LD_LIBRARY_PATH. + // Anything it does not vouch for gets dlopen'd for real before being called missing. + var ldResult = ProcessRunner.RunCaptured("ldconfig", "-p"); + var cache = ldResult.Success ? ldResult.StandardOutput : null; - private static List CheckLinuxApt() - { var results = new List(); - var packages = new[] { "libicu-dev", "libdeflate-dev", "zstd", "libargon2-dev" }; var missing = new List(); - foreach (var package in packages) + foreach (var (name, maxSoVersion) in _linuxLibraries) { - var result = ProcessRunner.RunCaptured("dpkg", $"-l {package}"); - var installed = result.Success && result.StandardOutput.Contains("ii"); + var found = cache?.Contains($"{name}.so", StringComparison.Ordinal) == true || + CanLoad(name, maxSoVersion); - if (!installed) + if (!found) { - missing.Add(package); + missing.Add(name); } results.Add(new PrerequisiteResult { - Name = package, - Passed = installed, - Details = installed ? "Installed" : "Not installed" + Name = name, + Passed = found, + Details = found ? "Found" : "Not found" }); } - if (missing.Count > 0) + if (missing.Count == 0) { - results.Add(new PrerequisiteResult - { - Name = "Install all missing", - Passed = false, - IsWarning = true, - Details = "Run the following command to install all missing dependencies:", - InstallCommand = $"sudo apt-get install -y {string.Join(' ', missing)}" - }); + return results; } - return results; - } - - private static List CheckLinuxDnf(PlatformInfo platform) - { - var results = new List(); - var packages = new[] { "libicu", "libdeflate-devel", "zstd", "libargon2-devel" }; - var missing = new List(); - - foreach (var package in packages) - { - var result = ProcessRunner.RunCaptured("rpm", $"-q {package}"); - var installed = result.Success; - - if (!installed) - { - missing.Add(package); - } - - results.Add(new PrerequisiteResult - { - Name = package, - Passed = installed, - Details = installed ? "Installed" : "Not installed" - }); - } - - // Check if this is CentOS (needs EPEL) - var isCentOs = platform.DistroId?.Equals("centos", StringComparison.OrdinalIgnoreCase) == true; - if (isCentOs && missing.Count > 0) + if (platform.DistroId?.Equals("centos", StringComparison.OrdinalIgnoreCase) == true) { results.Add(new PrerequisiteResult { @@ -263,48 +253,106 @@ public static class NativeLibraryChecker }); } - if (missing.Count > 0) + results.Add(new PrerequisiteResult { - results.Add(new PrerequisiteResult - { - Name = "Install all missing", - Passed = false, - IsWarning = true, - Details = "Run the following command to install all missing dependencies:", - InstallCommand = $"sudo dnf install -y {string.Join(' ', missing)}" - }); - } + Name = "Install all missing", + Passed = false, + IsWarning = true, + Details = "Install the runtime libraries. The -dev/-devel packages are not required:", + InstallCommand = BuildInstallCommand(platform, missing) + }); return results; } - private static List CheckLinuxGeneric(PlatformInfo platform) + /// + /// Asks the loader directly, for when ldconfig cannot answer. Mirrors the binding packages' + /// own probing: the unversioned name first, then libfoo.so.N descending. Bare names go through + /// the full loader search path, so LD_LIBRARY_PATH and /etc/ld.so.conf.d still apply. + /// + private static bool CanLoad(string library, int maxSoVersion) { - var results = new List(); - - // Use ldconfig to check for shared libraries - var ldResult = ProcessRunner.RunCaptured("ldconfig", "-p"); - var ldOutput = ldResult.Success ? ldResult.StandardOutput : ""; - - var libraries = new Dictionary + if (TryLoadAndFree($"{library}.so")) { - ["libicu"] = "libicuuc", - ["libdeflate"] = "libdeflate", - ["zstd"] = "libzstd", - ["libargon2"] = "libargon2" - }; - - foreach (var (name, soName) in libraries) - { - var found = ldOutput.Contains(soName, StringComparison.OrdinalIgnoreCase); - results.Add(new PrerequisiteResult - { - Name = name, - Passed = found, - Details = found ? "Found" : "Not found — install using your package manager" - }); + return true; } - return results; + for (var soVersion = maxSoVersion; soVersion >= 0; soVersion--) + { + if (TryLoadAndFree($"{library}.so.{soVersion}")) + { + return true; + } + } + + return false; + } + + private static bool TryLoadAndFree(string candidate) + { + if (!NativeLibrary.TryLoad(candidate, out var handle)) + { + return false; + } + + NativeLibrary.Free(handle); + return true; + } + + private static string BuildInstallCommand(PlatformInfo platform, List missing) + { + switch (platform.PackageManager) + { + case PackageManager.Apt: + { + var packages = missing.Select( + library => library switch + { + "libdeflate" => "libdeflate0", + "libargon2" => "libargon2-1", + _ => ResolveAptIcuPackage() + } + ); + + return $"sudo apt-get install -y {string.Join(' ', packages)}"; + } + case PackageManager.Dnf: + { + var packages = missing.Select( + library => library switch + { + "libdeflate" => "libdeflate", + "libargon2" => "libargon2", + _ => "libicu" + } + ); + + return $"sudo dnf install -y {string.Join(' ', packages)}"; + } + default: + return $"Install your distribution's runtime packages for: {string.Join(", ", missing)}"; + } + } + + /// + /// ICU's apt package carries the ABI version in its name and there is no stable alias, so ask + /// apt which one this release actually ships instead of printing a name that rots. + /// + private static string ResolveAptIcuPackage() + { + var result = ProcessRunner.RunCaptured("apt-cache", "search --names-only ^libicu[0-9]+$"); + if (!result.Success) + { + return "libicu"; + } + + var best = result.StandardOutput + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Split(' ', 2)[0].Trim()) + .Where(name => name.StartsWith("libicu", StringComparison.Ordinal)) + .OrderBy(name => int.TryParse(name.AsSpan(6), out var version) ? version : 0) + .LastOrDefault(); + + return best ?? "libicu"; } } diff --git a/Projects/BuildTool/Program.cs b/Projects/BuildTool/Program.cs index 3b36066da..dd2b16479 100644 --- a/Projects/BuildTool/Program.cs +++ b/Projects/BuildTool/Program.cs @@ -36,6 +36,29 @@ options.Os ??= detectedPlatform.OsRid; options.Arch ??= detectedPlatform.ArchRid; var rid = $"{options.Os}-{options.Arch}"; +if (options.CheckPrereqsOnly) +{ + var allPassed = true; + + foreach (var result in NativeLibraryChecker.Check(detectedPlatform)) + { + if (!result.IsWarning) + { + Console.WriteLine($"{result.Name,-16} {(result.Passed ? "OK" : "MISSING"),-8} {result.Details}"); + allPassed &= result.Passed; + continue; + } + + Console.WriteLine(result.Details); + if (result.InstallCommand is not null) + { + Console.WriteLine($" {result.InstallCommand}"); + } + } + + return allPassed ? 0 : 1; +} + // Run prerequisite checks unless skipped if (!options.SkipPrereqs) { @@ -108,6 +131,12 @@ static BuildOptions ParseArguments(string[] args) hasNamedArgs = true; break; } + case "--check-prereqs": + { + options.CheckPrereqsOnly = true; + hasNamedArgs = true; + break; + } case "--interactive": { options.Interactive = true; diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index b022c584c..8ad6a610b 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -36,7 +36,7 @@ - + diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 4f230902d..bf288e629 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -40,12 +40,12 @@ false - + - + diff --git a/README.md b/README.md index b61204743..1b76ce804 100644 --- a/README.md +++ b/README.md @@ -87,18 +87,25 @@ dnf install -y dnf-plugins-core dnf config-manager --set-enabled crb dnf install -y epel-release # Prerequisites -dnf install -y findutils libicu libdeflate-devel zstd libargon2-devel +dnf install -y findutils libicu libdeflate libargon2 ``` ### Ubuntu, Debian, etc ```shell apt-get update -y -apt-get install -y libicu-dev libdeflate-dev zstd libargon2-dev +apt-get install -y libicu-dev libdeflate0 libargon2-1 ``` +Only the runtime libraries are needed — the `-dev`/`-devel` packages are not. ICU is the exception +on Debian and Ubuntu, where the runtime package carries the ABI version in its name (`libicu74`, +`libicu76`, …) and there is no stable alias, so `libicu-dev` is the version-independent way to pull +it in. Run `./build-tool --check-prereqs` to print the exact packages your release needs. + +`zstd` is no longer listed because ZstdNet bundles `libzstd` for every platform. + ## OSX Requirements ```shell -brew install icu4c libdeflate zstd argon2 +brew install icu4c libdeflate argon2 ``` ## Running the Server