package main import ( "archive/tar" "compress/gzip" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "strings" ) func DownloadAndExtractServer(version string, installBaseDir string) error { targetDir := filepath.Join(installBaseDir, version) if _, err := os.Stat(filepath.Join(targetDir, "VintagestoryServer")); err == nil { fmt.Printf("[Downloader] Server version %s is already installed\n", version) return nil } url := fmt.Sprintf("https://cdn.vintagestory.at/gamefiles/stable/vs_server_linux-x64_%s.tar.gz", version) fmt.Printf("Establishing connection to '%s'\n", url) resp, err := http.Get(url) if err != nil { return fmt.Errorf("Error connecting to '%s', %w", url, err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return fmt.Errorf("Version %s not found on the stable CDN branch (404)", version) } else if resp.StatusCode != http.StatusOK { return fmt.Errorf("Unexpected CDN response status: %s", resp.Status) } fmt.Printf("Downloading and extracting archive to '%s'\n", targetDir) if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("Failed to create version installation directory: %w", err) } gzipReader, err := gzip.NewReader(resp.Body) if err != nil { return fmt.Errorf("Failed to initialize gzip decompressor: %w", err) } defer gzipReader.Close() tarReader := tar.NewReader(gzipReader) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return fmt.Errorf("Error reading TAR stream: %w", err) } targetFilePath := filepath.Join(targetDir, header.Name) switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(targetFilePath, 0755); err != nil { return fmt.Errorf("failed creating archive directory entry: %w", err) } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(targetFilePath), 0755); err != nil { return fmt.Errorf("failed creating parent path for file: %w", err) } outFile, err := os.OpenFile(targetFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, header.FileInfo().Mode()) if err != nil { return fmt.Errorf("failed creating target system file: %w", err) } if _, err := io.Copy(outFile, tarReader); err != nil { outFile.Close() return fmt.Errorf("failed streaming file payload extraction: %w", err) } outFile.Close() } } binaryExecutor := filepath.Join(targetDir, "VintagestoryServer") if err := patchBinaryForNixos(binaryExecutor); err != nil { return fmt.Errorf("failed fixing platform compatibility gates: %w", err) } fmt.Printf("Server version %s successfully installed\n", version) return nil } func patchBinaryForNixos(binaryPath string) error { if _, err := os.Stat("/etc/NIXOS"); os.IsNotExist(err) { return nil } fmt.Println("[NixOS Detected] Patching server interpreter pathway...") dotnetRoot := os.Getenv("DOTNET_ROOT") var interpreter string if dotnetRoot != "" { // Use the glibc version that matches the active .NET runtime exactly // Finds the underlying ld-linux-x86-64.so.2 link within the .NET store closure out, err := exec.Command("patchelf", "--print-interpreter", filepath.Join(dotnetRoot, "dotnet")).Output() if err == nil && len(out) > 0 { interpreter = strings.TrimSpace(string(out)) } } if interpreter == "" { interpreter = "/lib/ld-linux-x86-64.so.2" } cmd := exec.Command("patchelf", "--set-interpreter", interpreter, binaryPath) if err := cmd.Run(); err != nil { return fmt.Errorf("patchelf failed to link binary: %w", err) } return nil }