using System.Runtime.InteropServices; using BuildTool.Platform; using BuildTool.Publishing; namespace BuildTool.Prerequisites; public static class NativeLibraryChecker { /// /// Returns the required prerequisites for a target OS (without checking the local machine). /// Used to inform users what they need to install on the deployment target after cross-compiling. /// public static (string Description, string[] InstallCommands) GetRequirementsForTarget(string targetOs) { return targetOs switch { "win" => ( "Windows", [ ".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0", "VC++ Redistributable v14 — https://aka.ms/vs/17/release/vc_redist.x64.exe" ] ), "osx" => ( "macOS", [ ".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0", "brew install icu4c libdeflate zstd argon2" ] ), "linux" => ( "Linux", [ ".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0", "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" ] ), _ => ("Unknown", [".NET 10 Runtime — https://dotnet.microsoft.com/download/dotnet/10.0"]) }; } public static List Check(PlatformInfo platform) { if (platform.IsWindows) { return CheckWindows(platform); } if (platform.IsMacOS) { return CheckMacOS(); } if (platform.IsLinux) { return CheckLinux(platform); } return []; } private static List CheckWindows(PlatformInfo platform) { var results = new List(); // Check VC++ Redistributable via registry var vcRedistInstalled = CheckVcRedist(platform.ArchRid); var downloadUrl = platform.ArchRid == "arm64" ? "https://aka.ms/vs/17/release/vc_redist.arm64.exe" : "https://aka.ms/vs/17/release/vc_redist.x64.exe"; results.Add(new PrerequisiteResult { Name = "VC++ Redistributable v14", Passed = vcRedistInstalled, Details = vcRedistInstalled ? "Installed" : "Not found", DownloadUrl = vcRedistInstalled ? null : downloadUrl }); return results; } private static bool CheckVcRedist(string arch) { if (!OperatingSystem.IsWindows()) { return false; } // Check multiple known registry paths for VC++ 14.x Redistributable string[] registryPaths = [ $@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\{arch}", $@"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\{arch}" ]; foreach (var path in registryPaths) { try { using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(path); if (key?.GetValue("Installed") is int installed && installed == 1) { return true; } } catch { // Registry access may fail, continue checking } } return false; } private static List CheckMacOS() { var results = new List(); // Check if Homebrew is installed var brewResult = ProcessRunner.RunCaptured("which", "brew"); if (!brewResult.Success) { results.Add(new PrerequisiteResult { Name = "Homebrew", Passed = false, Details = "Homebrew is required to install native dependencies", DownloadUrl = "https://brew.sh" }); return results; } // Check required Homebrew formulae var formulae = new[] { "icu4c", "libdeflate", "zstd", "argon2" }; var listResult = ProcessRunner.RunCaptured("brew", "list --formula"); var installedFormulae = listResult.Success ? new HashSet( listResult.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase) : new HashSet(StringComparer.OrdinalIgnoreCase); var missing = new List(); foreach (var formula in formulae) { // Check exact match or versioned variant (e.g. "icu4c@78" matches "icu4c") var installed = installedFormulae.Contains(formula) || installedFormulae.Any(f => f.StartsWith($"{formula}@", StringComparison.OrdinalIgnoreCase)); if (!installed) { missing.Add(formula); } results.Add(new PrerequisiteResult { Name = formula, Passed = installed, Details = installed ? "Installed" : "Not installed", InstallCommand = installed ? null : $"brew install {formula}" }); } 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 = $"brew install {string.Join(' ', missing)}" }); } 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) { // 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; var results = new List(); var missing = new List(); foreach (var (name, maxSoVersion) in _linuxLibraries) { var found = cache?.Contains($"{name}.so", StringComparison.Ordinal) == true || CanLoad(name, maxSoVersion); if (!found) { missing.Add(name); } results.Add(new PrerequisiteResult { Name = name, Passed = found, Details = found ? "Found" : "Not found" }); } if (missing.Count == 0) { return results; } if (platform.DistroId?.Equals("centos", StringComparison.OrdinalIgnoreCase) == true) { results.Add(new PrerequisiteResult { Name = "EPEL Repository", Passed = false, IsWarning = true, Details = "CentOS requires EPEL for some packages. Enable it first:", InstallCommand = "sudo dnf install -y epel-release epel-next-release && sudo dnf config-manager --set-enabled crb" }); } results.Add(new PrerequisiteResult { 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; } /// /// 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) { if (TryLoadAndFree($"{library}.so")) { return true; } 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"; } }