package provision import ( "archive/tar" "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "zlh-agent/internal/state" ) // InstallJava downloads and extracts the Java runtime, then creates a stable // symlink at /opt/zlh/runtime/java → /bin/java func InstallJava(cfg state.Config) error { if cfg.JavaPath == "" { return fmt.Errorf("java_path missing in config") } url := BuildArtifactURL(cfg.JavaPath) resp, err := http.Get(url) if err != nil { return fmt.Errorf("download java from %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("download java: status %s", resp.Status) } runtimeRoot := JavaDir(cfg) if err := os.MkdirAll(runtimeRoot, 0755); err != nil { return fmt.Errorf("create java root dir: %w", err) } // Extract into temp directory tmpDir := filepath.Join(runtimeRoot, "tmp-extract") _ = os.RemoveAll(tmpDir) if err := os.MkdirAll(tmpDir, 0755); err != nil { return fmt.Errorf("create tmp java dir: %w", err) } if err := extractTarGz(resp.Body, tmpDir); err != nil { return fmt.Errorf("extract java: %w", err) } // Detect root folder (JDK name) entries, err := os.ReadDir(tmpDir) if err != nil { return fmt.Errorf("read tmp java dir: %w", err) } if len(entries) != 1 || !entries[0].IsDir() { return fmt.Errorf("java extract: unexpected folder layout in %s", tmpDir) } rootFolder := entries[0].Name() versionPath := filepath.Join(runtimeRoot, rootFolder) // Move extracted Java dir into runtime/ _ = os.RemoveAll(versionPath) if err := os.Rename(filepath.Join(tmpDir, rootFolder), versionPath); err != nil { return fmt.Errorf("move java folder: %w", err) } // Cleanup temp _ = os.RemoveAll(tmpDir) // Make java binary executable javaBin := filepath.Join(versionPath, "bin", "java") if err := os.Chmod(javaBin, 0755); err != nil { return fmt.Errorf("chmod java binary: %w", err) } // Symlink: java → /bin/java linkPath := filepath.Join(JavaRoot, "java") _ = os.Remove(linkPath) if err := os.Symlink(javaBin, linkPath); err != nil { return fmt.Errorf("create java symlink: %w", err) } fmt.Println("[java] Installed runtime:", versionPath, "->", linkPath) return nil } // extractTarGz restores the original signature returning only "error" func extractTarGz(r io.Reader, dest string) error { gz, err := gzip.NewReader(r) if err != nil { return err } defer gz.Close() tr := tar.NewReader(gz) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return err } target := filepath.Join(dest, hdr.Name) switch hdr.Typeflag { case tar.TypeDir: if err := os.MkdirAll(target, 0755); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { return err } f, err := os.Create(target) if err != nil { return err } if _, err := io.Copy(f, tr); err != nil { f.Close() return err } f.Close() } } return nil }