From 3e1a38aa5c800334bc415812f6530a4ca1d21e24 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sun, 18 Oct 2026 13:47:19 +0800 Subject: [PATCH] fix: install the newest 25.0 build of Oracle GraalVM for a bare major version instead of a feature release --- docs/e2e.md | 30 ++++++++++++------- hooks/pre_install.lua | 16 ++++++++++ lib/distribution_version.lua | 10 +++++++ tests/e2e/common/common.go | 52 +++++++++++++++++++++++++++++++++ tests/e2e/common/common_test.go | 31 ++++++++++++++++++++ tests/e2e/linux/e2e_test.go | 3 ++ tests/e2e/linux/setup.go | 26 +++++++++++++++++ tests/e2e/windows/e2e_test.go | 3 ++ tests/e2e/windows/setup.go | 27 +++++++++++++++++ tests/pre_install_test.lua | 35 ++++++++++++++++++++++ 10 files changed, 222 insertions(+), 11 deletions(-) diff --git a/docs/e2e.md b/docs/e2e.md index 98a8555..0f8b718 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -70,13 +70,21 @@ JDK, so `java` can only come from what vfox installs: (`+99`, e.g. `21.0.8+99-tem`), proving the lookup keeps the build number: Foojay reads an unencoded `+` as a space and drops the build, and the release's newest build would install instead. -3. The pinned release installs under the name `PreInstall` gives it: the +3. On the GraalVM Community rows, Oracle GraalVM's bare major `25-graal` + installs the newest build of its 25.0 line (`25.0.4.1-graal`), the line + Oracle's own `25/latest` download follows, not one of the monthly feature + releases Foojay lists before it (`25.4.4.1-graal`). The builds of 25 that + `vfox search java graal` lists are faked as installed first, so + `vfox install` names the one `PreInstall` picked and nothing downloads. + GA 25 is left out, as vfox would find `25-graal` itself installed without + asking `PreInstall`. +4. The pinned release installs under the name `PreInstall` gives it: the requested version plus the build number (`21.0.8-tem` installs as `21.0.8+9-tem`). `vfox search java ` (the `Available` hook) lists that same name as installed, noting JavaFX on `-fx` builds. On Windows that includes JetBrains Runtime 21.0.5, which ships only as tar.gz there. -4. Before any `vfox use`, a `.sdkmanrc` holding the SDKMAN identifier +5. Before any `vfox use`, a `.sdkmanrc` holding the SDKMAN identifier without the build number selects the JDK through `ParseLegacyFile`: `java=21.0.8-tem`, `java=21.0.5-jbr` (SDKMAN's name for JetBrains) or `java=25.0.4.fx-librca` (its spelling of JavaFX builds). So does one @@ -99,7 +107,7 @@ JDK, so `java` can only come from what vfox installs: weeks, so that install is faked: its `bin` holds a stand-in `java` that only prints the name. On Windows the file is written with CRLF line endings. -5. After `vfox use --global`, the JDK on `PATH` reports the requested +6. After `vfox use --global`, the JDK on `PATH` reports the requested `java.version`, the runner's `os.arch` (`amd64` / `aarch64`, which also catches an x64 JDK emulated on Windows ARM64) and the distribution's `java.vendor`. `JAVA_HOME` holds that same JDK, `javac` compiles a @@ -108,7 +116,7 @@ JDK, so `java` can only come from what vfox installs: `GRAALVM_HOME` set beforehand keeps its value, as users point it at a GraalVM of their own; for GraalVM, the plugin used to replace it with the JDK in use. -6. `PreUse` maps the spellings users type to the installed version without +7. `PreUse` maps the spellings users type to the installed version without another download: the bare major (`java@21`, left to vfox core's fuzzy matching), the spec the JDK was installed with (`java@21.0.8-tem`, whose build number `PreUse` finds among the installed versions) and the @@ -123,13 +131,13 @@ JDK, so `java` can only come from what vfox installs: The default matrix runs every distribution against both vfox channels (`latest`, `main`): -| Distribution | Release | Covers | amd64 | arm64 | -| ------------ | ------- | -------------------------------------------------------- | ----- | ----- | -| `open` | 25.0.2 | the default distribution, versions without a suffix | ✓ | | -| `tem` | 21.0.8 | the common path (tar.gz on Linux, zip on Windows) | ✓ | ✓ | -| `librca-fx` | 25.0.4 | JavaFX bundled builds (`-fx`) | ✓ | ✓ | -| `jb` | 21.0.5 | the tar.gz path on Windows, which has no zip for it | ✓ | ✓ | -| `graalce` | 25.0.2 | a `GRAALVM_HOME` set beforehand keeps its value | ✓ | | +| Distribution | Release | Covers | amd64 | arm64 | +| ------------ | ------- | ---------------------------------------------------------------------------- | ----- | ----- | +| `open` | 25.0.2 | the default distribution, versions without a suffix | ✓ | | +| `tem` | 21.0.8 | the common path (tar.gz on Linux, zip on Windows) | ✓ | ✓ | +| `librca-fx` | 25.0.4 | JavaFX bundled builds (`-fx`) | ✓ | ✓ | +| `jb` | 21.0.5 | the tar.gz path on Windows, which has no zip for it | ✓ | ✓ | +| `graalce` | 25.0.2 | a `GRAALVM_HOME` set beforehand keeps its value; Oracle GraalVM's `25-graal` | ✓ | | Every pinned release is published through Foojay for each platform its row runs on. OpenJDK and GraalVM Community ship no `windows-aarch64` build, so diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index f399025..71dbed2 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -69,6 +69,22 @@ function PLUGIN:PreInstall(ctx) ) end + -- For Oracle GraalVM, latest=available lists the monthly feature releases + -- (25.4.4.1) before the 25.0 line, which Oracle's own 25/latest download + -- tracks; a bare major takes the newest 25.0 build instead. + if distribution_version.distribution.name == "graalvm" and distribution_version.version:match("^%d+$") then + local releases = {} + for _, candidate in ipairs(filtered_jdks) do + if not distribution_version_parser.is_feature_release(candidate.java_version) then + table.insert(releases, candidate) + end + end + -- A major with feature releases only keeps them. + if #releases > 0 then + filtered_jdks = releases + end + end + local jdk = filtered_jdks[1] -- Windows lists a version's zip and tar.gz packages in either order; -- install the zip where there is one. diff --git a/lib/distribution_version.lua b/lib/distribution_version.lua index 6671acf..bf2cc12 100644 --- a/lib/distribution_version.lua +++ b/lib/distribution_version.lua @@ -91,6 +91,16 @@ function distribution_version.convert_version_for_api(version) return version end +--- Reports whether a GraalVM version is a feature release. Since 25.1, GraalVM +--- puts one out every month on top of its 25.0 line, numbered in the second +--- component (25.1.3, 25.4.4.1), which the 25.0 line keeps at 0 (25.0.4.1) or +--- leaves out (25). +--- @param version string Foojay java_version +--- @return boolean +function distribution_version.is_feature_release(version) + return version:match("^%d+%.[1-9]") ~= nil +end + function distribution_version.parse_version(arg) local version_parts = strings.split(arg, "-") local version diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go index 41afd0c..f72170d 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -77,6 +77,20 @@ var distributions = map[string]distribution{ "graalce": {longName: "graalvm_community", vendor: "GraalVM Community"}, } +// OracleGraalVMSpec is Oracle GraalVM's bare major 25. Since 25.1, Oracle +// GraalVM puts out a feature release every month on top of its 25.0 line +// (25.1.3 ... 25.4.4.1) and Foojay lists them first, but the bare major must +// install the newest build of the 25.0 line, the one Oracle's own 25/latest +// download follows (vfox-java#97). +const OracleGraalVMSpec = "25-graal" + +// ChecksOracleGraalVM reports whether a combo also checks Oracle GraalVM's +// bare major (see OracleGraalVMSpec), which downloads nothing. GraalVM +// Community's combos do: Foojay publishes both for the same platforms. +func ChecksOracleGraalVM(distribution string) bool { + return Base(distribution) == "graalce" +} + // PassThroughEnvs are host env knobs recorded in each target container's // environment, so `docker inspect` shows what a combo ran with. Forwarded // only when set, so unset stays default. @@ -519,6 +533,44 @@ func SearchEntry(output, version string) (string, bool) { return "", false } +// SearchedVersions extracts the versions `vfox search` lists, in its order. +func SearchedVersions(output string) []string { + var versions []string + for _, line := range strings.Split(StripANSI(output), "\n") { + if entry, ok := strings.CutPrefix(strings.TrimRight(line, "\r"), " - "); ok { + version, _, _ := strings.Cut(entry, " ") + versions = append(versions, version) + } + } + return versions +} + +// featureRelease matches a GraalVM feature release, numbered in the second +// component (25.4.4.1), which the 25.0 line keeps at 0 or leaves out +// (25.0.4.1, 25). +var featureRelease = regexp.MustCompile(`^\d+\.[1-9]`) + +// BareMajorBuilds picks out of the versions `vfox search` lists, newest +// first, the builds of a bare major spec's major and distribution +// (25.4.4.1-graal ... 25.0.1-graal for 25-graal), and the one the spec must +// install: the newest that is no feature release (25.0.4.1-graal). The +// build named like the spec (25-graal, GA 25) is left out: vfox reports a +// spec naming an installed version as installed without calling PreInstall. +func BareMajorBuilds(searched []string, spec string) (builds []string, want string) { + major, distribution, _ := strings.Cut(spec, "-") + for _, version := range searched { + core, ok := strings.CutSuffix(version, "-"+distribution) + if !ok || Major(core) != major || version == spec { + continue + } + builds = append(builds, version) + if want == "" && !featureRelease.MatchString(core) { + want = version + } + } + return builds, want +} + // ContainerName returns a unique container name per run: the combo slug stays // greppable in `docker ps`, the random suffix avoids clashes with leftovers // from killed runs. diff --git a/tests/e2e/common/common_test.go b/tests/e2e/common/common_test.go index d9edc2a..5eda6fe 100644 --- a/tests/e2e/common/common_test.go +++ b/tests/e2e/common/common_test.go @@ -398,3 +398,34 @@ func TestSearchEntry(t *testing.T) { t.Error("SearchEntry matched a version prefix") } } + +func TestBareMajorBuilds(t *testing.T) { + // Abridged `vfox search java graal` output, observed on 2026-10-18. + searched := SearchedVersions("Available versions:\r\n" + + " - 25.4.4.1-graal (LTS)\r\n" + + " - 25.1.3-graal (LTS)\r\n" + + " - 25.0.4.1-graal (LTS)\r\n" + + " - 25.0.4-graal (LTS) (installed)\r\n" + + " - 25-graal (LTS)\r\n" + + " - 24.0.2-graal\r\n" + + " - 21.0.12-graal (LTS)\r\n") + if want := []string{"25.4.4.1-graal", "25.1.3-graal", "25.0.4.1-graal", "25.0.4-graal", "25-graal", "24.0.2-graal", "21.0.12-graal"}; !slices.Equal(searched, want) { + t.Fatalf("SearchedVersions = %q, want %q", searched, want) + } + builds, want := BareMajorBuilds(searched, OracleGraalVMSpec) + // GA 25 is named like the spec, so vfox would find it installed before + // asking PreInstall. + if expected := []string{"25.4.4.1-graal", "25.1.3-graal", "25.0.4.1-graal", "25.0.4-graal"}; !slices.Equal(builds, expected) { + t.Errorf("builds = %q, want %q", builds, expected) + } + if want != "25.0.4.1-graal" { + t.Errorf("want = %q, want 25.0.4.1-graal", want) + } + // GraalVM Community's versions are not Oracle GraalVM's. + if builds, want := BareMajorBuilds([]string{"25.4.4.1-graalce", "25.0.2-graalce"}, OracleGraalVMSpec); len(builds) != 0 || want != "" { + t.Errorf("BareMajorBuilds(graalce versions) = %q, %q; want none", builds, want) + } + if !ChecksOracleGraalVM("graalce") || ChecksOracleGraalVM("tem") { + t.Error("only the GraalVM Community combos check Oracle GraalVM's bare major") + } +} diff --git a/tests/e2e/linux/e2e_test.go b/tests/e2e/linux/e2e_test.go index df449c2..45308dd 100644 --- a/tests/e2e/linux/e2e_test.go +++ b/tests/e2e/linux/e2e_test.go @@ -167,6 +167,9 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c setupVfox(ctx, t, ctr, c.Vfox) checkMissingVersionRejected(ctx, t, ctr, c.Distribution, version, arch) + if common.ChecksOracleGraalVM(c.Distribution) { + checkGraalVMBareMajor(ctx, t, ctr) + } installed := installJava(ctx, t, ctr, common.Spec(c.Distribution, version)) verifySearch(ctx, t, ctr, c.Distribution, installed) diff --git a/tests/e2e/linux/setup.go b/tests/e2e/linux/setup.go index dcbee3e..2f09d84 100644 --- a/tests/e2e/linux/setup.go +++ b/tests/e2e/linux/setup.go @@ -75,3 +75,29 @@ func checkMissingVersionRejected(ctx context.Context, t *testing.T, ctr tc.Conta t.Logf("PASS java@%s is rejected on linux/%s", spec, arch) } } + +// checkGraalVMBareMajor proves Oracle GraalVM's bare major installs the +// newest build of its 25.0 line, not one of the monthly feature releases +// Foojay lists before it (see common.OracleGraalVMSpec). The builds of the +// major `vfox search java graal` lists are faked as installed first (see +// common.BareMajorBuilds), so `vfox install` names the one PreInstall picked +// and downloads nothing. +func checkGraalVMBareMajor(ctx context.Context, t *testing.T, ctr tc.Container) { + t.Helper() + spec := common.OracleGraalVMSpec + out := execRetry(ctx, t, ctr, "vfox search java graal", "vfox search java graal") + builds, want := common.BareMajorBuilds(common.SearchedVersions(out), spec) + require.NotEmptyf(t, want, "vfox search java graal lists no %s build but feature releases\n--- output ---\n%s", spec, out) + var dirs, javaDirs []string + for _, build := range builds { + dir := `"${VFOX_HOME:-$HOME/.vfox}"/cache/java/` + shQuote("v-"+build) + dirs = append(dirs, dir) + javaDirs = append(javaDirs, dir+"/"+shQuote("java-"+build)) + } + execOK(ctx, t, ctr, "mkdir --parents "+strings.Join(javaDirs, " ")) + out = execRetry(ctx, t, ctr, "vfox install java@"+spec, "vfox install java@"+spec) + require.Containsf(t, common.StripANSI(out), "java@"+want+" is already installed", + "vfox install java@%s must take %s, the newest build that is no feature release", spec, want) + execOK(ctx, t, ctr, "rm --recursive "+strings.Join(dirs, " ")) + t.Logf("PASS java@%s takes %s out of %s", spec, want, strings.Join(builds, ", ")) +} diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index b0799b1..2d31b7a 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -243,6 +243,9 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c box := setupSlotEnv(ctx, t, ctr, c.Slug()) setupVfox(ctx, t, ctr, box, c.Vfox) checkMissingVersionRejected(ctx, t, ctr, box, c.Distribution, version, arch) + if common.ChecksOracleGraalVM(c.Distribution) { + checkGraalVMBareMajor(ctx, t, ctr, box) + } activation := fetchActivation(ctx, t, ctr, box) installed := installJava(ctx, t, ctr, box, common.Spec(c.Distribution, version)) diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index ee19f93..e017009 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -293,3 +293,30 @@ func checkMissingVersionRejected(ctx context.Context, t *testing.T, ctr tc.Conta t.Logf("PASS java@%s is rejected on windows/%s", spec, arch) } } + +// checkGraalVMBareMajor proves Oracle GraalVM's bare major installs the +// newest build of its 25.0 line, not one of the monthly feature releases +// Foojay lists before it (see common.OracleGraalVMSpec). The builds of the +// major `vfox search java graal` lists are faked as installed first (see +// common.BareMajorBuilds), so `vfox install` names the one PreInstall picked +// and downloads nothing. +func checkGraalVMBareMajor(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox) { + t.Helper() + spec := common.OracleGraalVMSpec + out := execRetry(ctx, t, ctr, "vfox search java graal", "vfox search java graal", box.vars...) + builds, want := common.BareMajorBuilds(common.SearchedVersions(out), spec) + require.NotEmptyf(t, want, "vfox search java graal lists no %s build but feature releases\n--- output ---\n%s", spec, out) + var create, remove []string + for _, build := range builds { + dir := fmt.Sprintf("(Join-Path $env:VFOX_HOME %s)", pwshQuote(`cache\java\v-`+build)) + create = append(create, fmt.Sprintf("New-Item -ItemType Directory -Force -Path (Join-Path %s %s) | Out-Null", + dir, pwshQuote("java-"+build))) + remove = append(remove, fmt.Sprintf("Remove-Item -LiteralPath %s -Recurse -Force", dir)) + } + execOK(ctx, t, ctr, strings.Join(create, "; "), box.vars...) + out = execRetry(ctx, t, ctr, "vfox install java@"+spec, "vfox install "+pwshQuote("java@"+spec), box.vars...) + require.Containsf(t, common.StripANSI(out), "java@"+want+" is already installed", + "vfox install java@%s must take %s, the newest build that is no feature release", spec, want) + execOK(ctx, t, ctr, strings.Join(remove, "; "), box.vars...) + t.Logf("PASS java@%s takes %s out of %s", spec, want, strings.Join(builds, ", ")) +} diff --git a/tests/pre_install_test.lua b/tests/pre_install_test.lua index 8844fe9..d606da4 100644 --- a/tests/pre_install_test.lua +++ b/tests/pre_install_test.lua @@ -38,6 +38,25 @@ local foojay = { ["temurin 21.0.8 available"] = { pkg("21.0.12.1+1") }, ["graalvm 17"] = {}, ["graalvm 17 available"] = { pkg("17.0.12") }, + -- Observed on 2026-10-18: Oracle GraalVM's monthly feature releases come + -- before its 25.0 line, and so do GraalVM Community's, whose 25.0 line + -- ended with 25.0.2. + ["graalvm 25 available"] = { + pkg("25.4.4.1"), + pkg("25.3.4.1"), + pkg("25.2.4"), + pkg("25.1.3"), + pkg("25.0.4.1"), + pkg("25.0.4"), + }, + ["graalvm 25.4.4.1"] = { pkg("25.4.4.1") }, + ["graalvm_community 25 available"] = { + pkg("25.4.4.1"), + pkg("25.3.4.1"), + pkg("25.2.4"), + pkg("25.1.3"), + pkg("25.0.2"), + }, ["zulu 21"] = { pkg("21+35"), pkg("21+35", true) }, ["zulu 21 available"] = { pkg("21.0.12.1+1"), pkg("21.0.12.1+1", true) }, ["openjdk 21"] = { pkg("21+35") }, @@ -56,6 +75,14 @@ local foojay = { ["windows jetbrains 21"] = { pkg("21+2121") }, ["windows jetbrains 21 available"] = { pkg("21.0.11+1163116"), pkg("21.0.11+1163116", false, "zip") }, ["windows jetbrains 21.0.5"] = { pkg("21.0.5+79248"), pkg("21.0.5+76240") }, + ["windows graalvm 25 available"] = { + pkg("25.4.4.1", false, "zip"), + pkg("25.3.4.1", false, "zip"), + pkg("25.2.4", false, "zip"), + pkg("25.1.3", false, "zip"), + pkg("25.0.4.1", false, "zip"), + pkg("25.0.4", false, "zip"), + }, ["windows jetbrains"] = { pkg("21.0.11+1163116"), pkg("21.0.11+1163116", false, "zip"), @@ -158,6 +185,13 @@ check("21-zulu", "21.0.12.1+1-zulu", true) check("21-zulu-fx", "21.0.12.1+1-zulu-fx", true) -- Foojay lists no GraalVM 17 GA build, so this found nothing before. check("17-graal", "17.0.12-graal", true) +-- Oracle GraalVM's bare major takes the newest build of its 25.0 line, not a +-- monthly feature release, which installs when named in full. +check("25-graal", "25.0.4.1-graal", true) +check("graalvm-25", "25.0.4.1-graal", true) +check("25.4.4.1-graal", "25.4.4.1-graal", false) +-- GraalVM Community's feature releases are the only ones it still updates. +check("25-graalce", "25.4.4.1-graalce", true) -- Anything longer than a major still matches exactly. check("21.0.8-tem", "21.0.8+9-tem", false) check("28.ea", "28-ea+19", false) @@ -194,6 +228,7 @@ assert(all["25.0.2+10-tem"] and all["21.0.8+9-tem"], "Available all: Temurin ver RUNTIME.osType = "windows" assert(check("21-jb", "21.0.11+1163116-jb", true, "zip") == 1, "21-jb: expected one lookup") assert(check("21.0.5-jb", "21.0.5+79248-jb", false, "tar.gz") == 1, "21.0.5-jb: expected one lookup") +check("25-graal", "25.0.4.1-graal", true, "zip") local jb = {} for _, item in ipairs(PLUGIN:Available({ args = { "jb" } })) do table.insert(jb, item.version)