|
| 1 | +// Copyright 2025 The Toodofun Authors |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http:www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package java |
| 16 | + |
| 17 | +import ( |
| 18 | + "context" |
| 19 | + "encoding/json" |
| 20 | + "fmt" |
| 21 | + "net/url" |
| 22 | + "os" |
| 23 | + "path/filepath" |
| 24 | + "runtime" |
| 25 | + "strconv" |
| 26 | + "strings" |
| 27 | + |
| 28 | + goversion "github.com/hashicorp/go-version" |
| 29 | + |
| 30 | + "github.com/toodofun/gvm/internal/core" |
| 31 | + "github.com/toodofun/gvm/internal/http" |
| 32 | + "github.com/toodofun/gvm/internal/log" |
| 33 | + "github.com/toodofun/gvm/internal/util/compress" |
| 34 | + "github.com/toodofun/gvm/internal/util/env" |
| 35 | + "github.com/toodofun/gvm/internal/util/path" |
| 36 | + "github.com/toodofun/gvm/languages" |
| 37 | +) |
| 38 | + |
| 39 | +const ( |
| 40 | + lang = "java" |
| 41 | + zuluUrl = "https://api.azul.com/metadata/v1/zulu/packages" |
| 42 | +) |
| 43 | + |
| 44 | +type Java struct{} |
| 45 | + |
| 46 | +func (j *Java) Name() string { |
| 47 | + return lang |
| 48 | +} |
| 49 | + |
| 50 | +func (j *Java) getCurrentSystemInfo() (os, arch, hwBitness string) { |
| 51 | + switch strings.ToLower(runtime.GOOS) { |
| 52 | + case "linux": |
| 53 | + os = "linux" |
| 54 | + case "windows": |
| 55 | + os = "windows" |
| 56 | + case "darwin": |
| 57 | + os = "macos" |
| 58 | + } |
| 59 | + |
| 60 | + switch strings.ToLower(runtime.GOARCH) { |
| 61 | + case "amd64": |
| 62 | + arch = "x86" |
| 63 | + hwBitness = "64" |
| 64 | + case "arm64": |
| 65 | + arch = "arm" |
| 66 | + hwBitness = "64" |
| 67 | + if runtime.GOOS == "darwin" { |
| 68 | + arch = "" |
| 69 | + hwBitness = "" |
| 70 | + } |
| 71 | + case "386": |
| 72 | + arch = "x86" |
| 73 | + hwBitness = "32" |
| 74 | + case "arm": |
| 75 | + arch = "arm" |
| 76 | + hwBitness = "32" |
| 77 | + } |
| 78 | + return |
| 79 | +} |
| 80 | + |
| 81 | +func (j *Java) fetchRemote( |
| 82 | + ctx context.Context, |
| 83 | + page, size int, |
| 84 | + callback func(version *core.RemoteVersion), |
| 85 | +) (more bool, err error) { |
| 86 | + logger := log.GetLogger(ctx) |
| 87 | + params := url.Values{} |
| 88 | + params.Set("page", strconv.Itoa(page)) |
| 89 | + params.Set("page_size", strconv.Itoa(size)) |
| 90 | + params.Set("availability_types", "ca") |
| 91 | + params.Set("release_status", "both") |
| 92 | + params.Set( |
| 93 | + "include_fields", |
| 94 | + "java_package_features,release_status,support_term,os,arch,hw_bitness,abi,java_package_type,javafx_bundled,sha256_hash,cpu_gen,size,archive_type,certifications,lib_c_type,crac_supported", |
| 95 | + ) |
| 96 | + params.Set("azul_com", "true") |
| 97 | + params.Set("archive_type", "tar.gz") |
| 98 | + params.Set("lib_c_type", "glibc") |
| 99 | + |
| 100 | + osStr, arch, hwBitness := j.getCurrentSystemInfo() |
| 101 | + params.Set("os", osStr) |
| 102 | + if len(arch) > 0 { |
| 103 | + params.Set("arch", arch) |
| 104 | + } |
| 105 | + if len(hwBitness) > 0 { |
| 106 | + params.Set("hw_bitness", hwBitness) |
| 107 | + } |
| 108 | + |
| 109 | + targetUrl := zuluUrl + "?" + params.Encode() |
| 110 | + |
| 111 | + logger.Infof("Fetching %s", targetUrl) |
| 112 | + body, err := http.Default().Get(ctx, targetUrl) |
| 113 | + if err != nil { |
| 114 | + logger.Errorf("Failed to fetch %s: %s", targetUrl, err) |
| 115 | + return false, err |
| 116 | + } |
| 117 | + |
| 118 | + versions := make([]Version, 0) |
| 119 | + if err := json.Unmarshal(body, &versions); err != nil { |
| 120 | + logger.Errorf("Failed to unmarshal %s: %s", targetUrl, err) |
| 121 | + return false, err |
| 122 | + } |
| 123 | + |
| 124 | + for _, v := range versions { |
| 125 | + if v.Os == "linux" && v.LibCType != "glibc" { |
| 126 | + continue |
| 127 | + } |
| 128 | + if v.JavaPackageType != "jdk" { |
| 129 | + continue |
| 130 | + } |
| 131 | + vs := make([]string, len(v.JavaVersion)) |
| 132 | + for i, num := range v.JavaVersion { |
| 133 | + vs[i] = strconv.Itoa(num) |
| 134 | + } |
| 135 | + |
| 136 | + ver, err := goversion.NewVersion(strings.Join(vs, ".") + "-zulu-" + v.Sha256Hash[:4]) |
| 137 | + if err != nil { |
| 138 | + logger.Errorf("Failed to parse version %s: %s", v.Name, err) |
| 139 | + return false, err |
| 140 | + } |
| 141 | + |
| 142 | + comment := strings.ReplaceAll(v.Name, ".tar.gz", "") |
| 143 | + |
| 144 | + callback(&core.RemoteVersion{ |
| 145 | + Version: ver, |
| 146 | + Origin: v.DownloadUrl, |
| 147 | + Comment: comment, |
| 148 | + }) |
| 149 | + } |
| 150 | + return len(versions) == 1000, nil |
| 151 | +} |
| 152 | + |
| 153 | +func (j *Java) ListRemoteVersions(ctx context.Context) ([]*core.RemoteVersion, error) { |
| 154 | + logger := log.GetLogger(ctx) |
| 155 | + res := make([]*core.RemoteVersion, 0) |
| 156 | + |
| 157 | + page := 1 |
| 158 | + pageSize := 1000 |
| 159 | + for { |
| 160 | + more, err := j.fetchRemote(ctx, page, pageSize, func(version *core.RemoteVersion) { |
| 161 | + logger.Debugf("Fetching remote version %+v", version) |
| 162 | + res = append(res, version) |
| 163 | + }) |
| 164 | + if err != nil { |
| 165 | + return nil, err |
| 166 | + } |
| 167 | + if !more { |
| 168 | + break |
| 169 | + } |
| 170 | + page++ |
| 171 | + } |
| 172 | + return res, nil |
| 173 | +} |
| 174 | + |
| 175 | +func (j *Java) ListInstalledVersions(ctx context.Context) ([]*core.InstalledVersion, error) { |
| 176 | + return languages.NewLanguage(j).ListInstalledVersions(ctx, "bin") |
| 177 | +} |
| 178 | + |
| 179 | +func (j *Java) SetDefaultVersion(ctx context.Context, version string) error { |
| 180 | + envs := []env.KV{ |
| 181 | + { |
| 182 | + Key: "PATH", |
| 183 | + Value: filepath.Join(path.GetLangRoot(lang), path.Current, "bin"), |
| 184 | + Append: true, |
| 185 | + }, |
| 186 | + } |
| 187 | + return languages.NewLanguage(j).SetDefaultVersion(ctx, version, envs) |
| 188 | +} |
| 189 | + |
| 190 | +func (j *Java) GetDefaultVersion(ctx context.Context) *core.InstalledVersion { |
| 191 | + return languages.NewLanguage(j).GetDefaultVersion() |
| 192 | +} |
| 193 | + |
| 194 | +func (j *Java) Uninstall(ctx context.Context, version string) error { |
| 195 | + return languages.NewLanguage(j).Uninstall(version) |
| 196 | +} |
| 197 | + |
| 198 | +func (j *Java) Install(ctx context.Context, version *core.RemoteVersion) error { |
| 199 | + logger := log.GetLogger(ctx) |
| 200 | + logger.Debugf("Install version: %+v", version) |
| 201 | + logger.Infof("Installing %s(%s)", version.Version.String(), version.Comment) |
| 202 | + |
| 203 | + file, err := http.Default(). |
| 204 | + Download(ctx, version.Origin, filepath.Join(path.GetLangRoot(lang), version.Version.String()), fmt.Sprintf("%s.%s-%s.tar.gz", version.Version.String(), runtime.GOOS, "amd64")) |
| 205 | + logger.Infof("") |
| 206 | + if err != nil { |
| 207 | + return fmt.Errorf("failed to download version: %s(%s): %w", version.Version.String(), version.Comment, err) |
| 208 | + } |
| 209 | + |
| 210 | + installDir := filepath.Join(path.GetLangRoot(lang), version.Version.String()) |
| 211 | + |
| 212 | + if err := compress.UnTarGz(ctx, file, installDir); err != nil { |
| 213 | + return fmt.Errorf("failed to unTarGz: %s(%s): %w", version.Version.String(), version.Comment, err) |
| 214 | + } |
| 215 | + |
| 216 | + if err = os.RemoveAll(file); err != nil { |
| 217 | + logger.Warnf("failed to clean %s: %v", file, err) |
| 218 | + } |
| 219 | + |
| 220 | + dirs, err := filepath.Glob(filepath.Join(installDir, "/*")) |
| 221 | + if err != nil { |
| 222 | + logger.Errorf("failed to glob %s: %v", installDir, err) |
| 223 | + return err |
| 224 | + } |
| 225 | + for _, dir := range dirs { |
| 226 | + sourceDir := dir |
| 227 | + files, err := os.ReadDir(sourceDir) |
| 228 | + if err != nil { |
| 229 | + return err |
| 230 | + } |
| 231 | + |
| 232 | + for _, f := range files { |
| 233 | + sourcePath := filepath.Join(sourceDir, f.Name()) |
| 234 | + destPath := filepath.Join(installDir, f.Name()) |
| 235 | + |
| 236 | + if _, err := os.Stat(destPath); err == nil { |
| 237 | + logger.Warnf("%s already exists", destPath) |
| 238 | + continue |
| 239 | + } |
| 240 | + |
| 241 | + err := os.Rename(sourcePath, destPath) |
| 242 | + if err != nil { |
| 243 | + return err |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + logger.Infof( |
| 249 | + "Version %s(%s) was successfully installed in %s", |
| 250 | + version.Version.String(), |
| 251 | + version.Comment, |
| 252 | + installDir, |
| 253 | + ) |
| 254 | + |
| 255 | + return nil |
| 256 | +} |
| 257 | + |
| 258 | +type Version struct { |
| 259 | + Abi string `json:"abi"` |
| 260 | + Arch string `json:"arch"` |
| 261 | + ArchiveType string `json:"archive_type"` |
| 262 | + AvailabilityType string `json:"availability_type"` |
| 263 | + Certifications []string `json:"certifications"` |
| 264 | + CpuGen []interface{} `json:"cpu_gen"` |
| 265 | + CracSupported bool `json:"crac_supported"` |
| 266 | + DistroVersion []int `json:"distro_version"` |
| 267 | + DownloadUrl string `json:"download_url"` |
| 268 | + HwBitness int `json:"hw_bitness"` |
| 269 | + JavaPackageFeatures []string `json:"java_package_features"` |
| 270 | + JavaPackageType string `json:"java_package_type"` |
| 271 | + JavaVersion []int `json:"java_version"` |
| 272 | + JavafxBundled bool `json:"javafx_bundled"` |
| 273 | + Latest bool `json:"latest"` |
| 274 | + LibCType string `json:"lib_c_type"` |
| 275 | + Name string `json:"name"` |
| 276 | + OpenjdkBuildNumber int `json:"openjdk_build_number"` |
| 277 | + Os string `json:"os"` |
| 278 | + PackageUuid string `json:"package_uuid"` |
| 279 | + Product string `json:"product"` |
| 280 | + ReleaseStatus string `json:"release_status"` |
| 281 | + Sha256Hash string `json:"sha256_hash"` |
| 282 | + Size int `json:"size"` |
| 283 | + SupportTerm string `json:"support_term"` |
| 284 | +} |
| 285 | + |
| 286 | +func init() { |
| 287 | + core.RegisterLanguage(&Java{}) |
| 288 | +} |
0 commit comments