using System.Formats.Tar; using System.IO.Compression; using System.Runtime.InteropServices; namespace BrassAndSigil.Server.Services; /// /// Downloads + extracts Adoptium Temurin JRE 21 to server/java/. Used as a fallback /// when system Java is missing or too old. Adoptium's API gives us a stable /// platform-keyed download URL without needing API keys or auth. /// public sealed class JavaInstaller { private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromMinutes(15) }; public string GetJavaInstallDir(string serverDir) => Path.Combine(serverDir, "java"); public string GetJavaInstallDir(string serverDir, int majorVersion) => Path.Combine(serverDir, "java" + majorVersion); /// If a previous install put a java executable under serverDir/java/, return its path. public string? FindBundledJava(string serverDir) { var javaDir = GetJavaInstallDir(serverDir); if (!Directory.Exists(javaDir)) return null; var exe = OperatingSystem.IsWindows() ? "java.exe" : "java"; return Directory.EnumerateFiles(javaDir, exe, SearchOption.AllDirectories).FirstOrDefault(); } /// Find a Java install for a specific major version (e.g. javaXX/jdk-XX*/bin/java). public string? FindBundledJava(string serverDir, int majorVersion) { var javaDir = GetJavaInstallDir(serverDir, majorVersion); if (!Directory.Exists(javaDir)) return null; var exe = OperatingSystem.IsWindows() ? "java.exe" : "java"; return Directory.EnumerateFiles(javaDir, exe, SearchOption.AllDirectories).FirstOrDefault(); } public Task InstallJre21Async(string serverDir, IProgress? progress, CancellationToken ct) => InstallJreAsync(21, serverDir, GetJavaInstallDir(serverDir), progress, ct); /// /// Download + extract Adoptium Temurin JRE for a specific major version into /// . Used by BlueMap to get JRE 25 alongside the /// JRE 21 we use for Minecraft. /// public async Task InstallJreAsync(int majorVersion, string serverDir, string installDir, IProgress? progress, CancellationToken ct) { var javaDir = installDir; Directory.CreateDirectory(javaDir); var (url, archiveName, isZip) = PickAdoptiumDownload(majorVersion); if (url is null) { progress?.Report($"[err] No supported Adoptium binary for {RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture}."); return null; } var archivePath = Path.Combine(javaDir, archiveName!); progress?.Report($"Downloading Adoptium Temurin JRE 21 ({(isZip ? "zip" : "tar.gz")})..."); try { using (var resp = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct)) { resp.EnsureSuccessStatusCode(); await using var src = await resp.Content.ReadAsStreamAsync(ct); await using var dst = File.Create(archivePath); await src.CopyToAsync(dst, ct); } } catch (Exception ex) { progress?.Report($" [err] Download failed: {ex.Message}"); return null; } progress?.Report($" Downloaded {new FileInfo(archivePath).Length:N0} bytes"); progress?.Report("Extracting..."); try { if (isZip) { ZipFile.ExtractToDirectory(archivePath, javaDir, overwriteFiles: true); } else { await using var fs = File.OpenRead(archivePath); await using var gzip = new GZipStream(fs, CompressionMode.Decompress); await TarFile.ExtractToDirectoryAsync(gzip, javaDir, overwriteFiles: true, ct); } } catch (Exception ex) { progress?.Report($" [err] Extract failed: {ex.Message}"); return null; } try { File.Delete(archivePath); } catch { /* best-effort */ } var javaExe = FindBundledJava(serverDir); if (javaExe is null) { progress?.Report(" [err] Extracted, but couldn't locate bin/java in the result."); return null; } // On Linux/macOS, make sure java is executable. TarFile preserves mode bits in // most setups, but be defensive. if (!OperatingSystem.IsWindows()) { try { File.SetUnixFileMode(javaExe, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute); } catch { } } progress?.Report($"Java {majorVersion} ready: {javaExe}"); return javaExe; } private static (string? Url, string? ArchiveName, bool IsZip) PickAdoptiumDownload(int majorVersion) { // Adoptium API picks the latest GA release matching our os/arch. // Docs: https://api.adoptium.net/q/swagger-ui/ var arch = RuntimeInformation.OSArchitecture switch { Architecture.X64 => "x64", Architecture.Arm64 => "aarch64", _ => null }; if (arch is null) return (null, null, false); if (OperatingSystem.IsWindows()) { return ($"https://api.adoptium.net/v3/binary/latest/{majorVersion}/ga/windows/{arch}/jre/hotspot/normal/eclipse", $"jre{majorVersion}.zip", true); } if (OperatingSystem.IsLinux()) { return ($"https://api.adoptium.net/v3/binary/latest/{majorVersion}/ga/linux/{arch}/jre/hotspot/normal/eclipse", $"jre{majorVersion}.tar.gz", false); } if (OperatingSystem.IsMacOS()) { return ($"https://api.adoptium.net/v3/binary/latest/{majorVersion}/ga/mac/{arch}/jre/hotspot/normal/eclipse", $"jre{majorVersion}.tar.gz", false); } return (null, null, false); } }