Skip to content

Commit 7ef178a

Browse files
authored
fix: install the newest 25.0 build of Oracle GraalVM for a bare major version instead of a feature release (#101)
1 parent b3238ec commit 7ef178a

9 files changed

Lines changed: 212 additions & 11 deletions

File tree

‎docs/e2e.md‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,15 @@ JDK, so `java` can only come from what vfox installs:
7575
(`<version>+99`, e.g. `21.0.8+99-tem`), proving the lookup keeps the
7676
build number: Foojay reads an unencoded `+` as a space and drops the
7777
build, and the release's newest build would install instead.
78-
3. The pinned release installs under the name `PreInstall` gives it: the
78+
3. On the GraalVM Community rows, Oracle GraalVM's bare major `25-graal`
79+
installs the newest build of its 25.0 line (`25.0.4.1-graal`), the line
80+
Oracle's own `25/latest` download follows, not one of the monthly feature
81+
releases Foojay lists before it (`25.4.4.1-graal`). The builds of 25 that
82+
`vfox search java graal` lists are faked as installed first, so
83+
`vfox install` names the one `PreInstall` picked and nothing downloads.
84+
GA 25 is left out, as vfox would find `25-graal` itself installed without
85+
asking `PreInstall`.
86+
4. The pinned release installs under the name `PreInstall` gives it: the
7987
requested version plus the build number (`21.0.8-tem` installs as
8088
`21.0.8+9-tem`). `vfox search java <distribution>` (the `Available` hook)
8189
lists that same name as installed, noting JavaFX on `-fx` builds. For
@@ -84,7 +92,7 @@ JDK, so `java` can only come from what vfox installs:
8492
marks them LTS, as the JDK they build on is; each lasts about a month.
8593
On Windows the list includes JetBrains Runtime 21.0.5, which ships only as
8694
tar.gz there.
87-
4. Before any `vfox use`, a `.sdkmanrc` holding the SDKMAN identifier
95+
5. Before any `vfox use`, a `.sdkmanrc` holding the SDKMAN identifier
8896
without the build number selects the JDK through `ParseLegacyFile`:
8997
`java=21.0.8-tem`, `java=21.0.5-jbr` (SDKMAN's name for JetBrains) or
9098
`java=25.0.4.fx-librca` (its spelling of JavaFX builds). So does one
@@ -107,7 +115,7 @@ JDK, so `java` can only come from what vfox installs:
107115
weeks, so that install is faked: its `bin` holds a stand-in `java` that
108116
only prints the name. On Windows the file is written with CRLF line
109117
endings.
110-
5. After `vfox use --global`, the JDK on `PATH` reports the requested
118+
6. After `vfox use --global`, the JDK on `PATH` reports the requested
111119
`java.version`, the runner's `os.arch` (`amd64` / `aarch64`, which also
112120
catches an x64 JDK emulated on Windows ARM64) and the distribution's
113121
`java.vendor`. `JAVA_HOME` holds that same JDK, `javac` compiles a
@@ -116,7 +124,7 @@ JDK, so `java` can only come from what vfox installs:
116124
`GRAALVM_HOME` set beforehand keeps its value, as users point it at a
117125
GraalVM of their own; for GraalVM, the plugin used to replace it with
118126
the JDK in use.
119-
6. `PreUse` maps the spellings users type to the installed version without
127+
7. `PreUse` maps the spellings users type to the installed version without
120128
another download: the bare major (`java@21`, left to vfox core's fuzzy
121129
matching), the spec the JDK was installed with (`java@21.0.8-tem`, whose
122130
build number `PreUse` finds among the installed versions) and the
@@ -131,13 +139,13 @@ JDK, so `java` can only come from what vfox installs:
131139
The default matrix runs every distribution against both vfox channels
132140
(`latest`, `main`):
133141

134-
| Distribution | Release | Covers | amd64 | arm64 |
135-
| ------------ | ------- | -------------------------------------------------------- | ----- | ----- |
136-
| `open` | 25.0.2 | the default distribution, versions without a suffix | ✓ | |
137-
| `tem` | 21.0.8 | the common path (tar.gz on Linux, zip on Windows) | ✓ | ✓ |
138-
| `librca-fx` | 25.0.4 | JavaFX bundled builds (`-fx`) | ✓ | ✓ |
139-
| `jb` | 21.0.5 | the tar.gz path on Windows, which has no zip for it | ✓ | ✓ |
140-
| `graalce` | 25.0.2 | a `GRAALVM_HOME` set beforehand keeps its value | ✓ | |
142+
| Distribution | Release | Covers | amd64 | arm64 |
143+
| ------------ | ------- | ---------------------------------------------------------------------------- | ----- | ----- |
144+
| `open` | 25.0.2 | the default distribution, versions without a suffix | ✓ | |
145+
| `tem` | 21.0.8 | the common path (tar.gz on Linux, zip on Windows) | ✓ | ✓ |
146+
| `librca-fx` | 25.0.4 | JavaFX bundled builds (`-fx`) | ✓ | ✓ |
147+
| `jb` | 21.0.5 | the tar.gz path on Windows, which has no zip for it | ✓ | ✓ |
148+
| `graalce` | 25.0.2 | a `GRAALVM_HOME` set beforehand keeps its value; Oracle GraalVM's `25-graal` | ✓ | |
141149

142150
Every pinned release is published through Foojay for each platform its row
143151
runs on. OpenJDK and GraalVM Community ship no `windows-aarch64` build, so

‎hooks/pre_install.lua‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ function PLUGIN:PreInstall(ctx)
7272
)
7373
end
7474

75+
-- For Oracle GraalVM, latest=available lists the monthly feature releases
76+
-- (25.4.4.1) before the 25.0 line, which Oracle's own 25/latest download
77+
-- tracks; a bare major takes the newest 25.0 build instead.
78+
if distribution_version.distribution.name == "graalvm" and distribution_version.version:match("^%d+$") then
79+
local releases = {}
80+
for _, candidate in ipairs(filtered_jdks) do
81+
if not distribution_version_parser.is_feature_release(candidate.java_version) then
82+
table.insert(releases, candidate)
83+
end
84+
end
85+
-- A major with feature releases only keeps them.
86+
if #releases > 0 then
87+
filtered_jdks = releases
88+
end
89+
end
90+
7591
local jdk = filtered_jdks[1]
7692
-- Windows lists a version's zip and tar.gz packages in either order;
7793
-- install the zip where there is one.

‎tests/e2e/common/common.go‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@ var distributions = map[string]distribution{
7878
"graalce": {longName: "graalvm_community", vendor: "GraalVM Community"},
7979
}
8080

81+
// OracleGraalVMSpec is Oracle GraalVM's bare major 25. Since 25.1, Oracle
82+
// GraalVM puts out a feature release every month on top of its 25.0 line
83+
// (25.1.3 ... 25.4.4.1) and Foojay lists them first, but the bare major must
84+
// install the newest build of the 25.0 line, the one Oracle's own 25/latest
85+
// download follows (vfox-java#97).
86+
const OracleGraalVMSpec = "25-graal"
87+
88+
// ChecksOracleGraalVM reports whether a combo also checks Oracle GraalVM's
89+
// bare major (see OracleGraalVMSpec), which downloads nothing. GraalVM
90+
// Community's combos do: Foojay publishes both for the same platforms.
91+
func ChecksOracleGraalVM(distribution string) bool {
92+
return Base(distribution) == "graalce"
93+
}
94+
8195
// PassThroughEnvs are host env knobs recorded in each target container's
8296
// environment, so `docker inspect` shows what a combo ran with. Forwarded
8397
// only when set, so unset stays default.
@@ -561,6 +575,44 @@ func SearchEntry(output, version string) (string, bool) {
561575
return "", false
562576
}
563577

578+
// SearchedVersions extracts the versions `vfox search` lists, in its order.
579+
func SearchedVersions(output string) []string {
580+
var versions []string
581+
for _, line := range strings.Split(StripANSI(output), "\n") {
582+
if entry, ok := strings.CutPrefix(strings.TrimRight(line, "\r"), " - "); ok {
583+
version, _, _ := strings.Cut(entry, " ")
584+
versions = append(versions, version)
585+
}
586+
}
587+
return versions
588+
}
589+
590+
// featureRelease matches a GraalVM feature release, numbered in the second
591+
// component (25.4.4.1), which the 25.0 line keeps at 0 or leaves out
592+
// (25.0.4.1, 25).
593+
var featureRelease = regexp.MustCompile(`^\d+\.[1-9]`)
594+
595+
// BareMajorBuilds picks out of the versions `vfox search` lists, newest
596+
// first, the builds of a bare major spec's major and distribution
597+
// (25.4.4.1-graal ... 25.0.1-graal for 25-graal), and the one the spec must
598+
// install: the newest that is no feature release (25.0.4.1-graal). The
599+
// build named like the spec (25-graal, GA 25) is left out: vfox reports a
600+
// spec naming an installed version as installed without calling PreInstall.
601+
func BareMajorBuilds(searched []string, spec string) (builds []string, want string) {
602+
major, distribution, _ := strings.Cut(spec, "-")
603+
for _, version := range searched {
604+
core, ok := strings.CutSuffix(version, "-"+distribution)
605+
if !ok || Major(core) != major || version == spec {
606+
continue
607+
}
608+
builds = append(builds, version)
609+
if want == "" && !featureRelease.MatchString(core) {
610+
want = version
611+
}
612+
}
613+
return builds, want
614+
}
615+
564616
// ContainerName returns a unique container name per run: the combo slug stays
565617
// greppable in `docker ps`, the random suffix avoids clashes with leftovers
566618
// from killed runs.

‎tests/e2e/common/common_test.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,34 @@ func TestSearchEntry(t *testing.T) {
429429
t.Error("SearchEntry matched a version prefix")
430430
}
431431
}
432+
433+
func TestBareMajorBuilds(t *testing.T) {
434+
// Abridged `vfox search java graal` output, observed on 2026-10-18.
435+
searched := SearchedVersions("Available versions:\r\n" +
436+
" - 25.4.4.1-graal (LTS)\r\n" +
437+
" - 25.1.3-graal (LTS)\r\n" +
438+
" - 25.0.4.1-graal (LTS)\r\n" +
439+
" - 25.0.4-graal (LTS) (installed)\r\n" +
440+
" - 25-graal (LTS)\r\n" +
441+
" - 24.0.2-graal\r\n" +
442+
" - 21.0.12-graal (LTS)\r\n")
443+
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) {
444+
t.Fatalf("SearchedVersions = %q, want %q", searched, want)
445+
}
446+
builds, want := BareMajorBuilds(searched, OracleGraalVMSpec)
447+
// GA 25 is named like the spec, so vfox would find it installed before
448+
// asking PreInstall.
449+
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) {
450+
t.Errorf("builds = %q, want %q", builds, expected)
451+
}
452+
if want != "25.0.4.1-graal" {
453+
t.Errorf("want = %q, want 25.0.4.1-graal", want)
454+
}
455+
// GraalVM Community's versions are not Oracle GraalVM's.
456+
if builds, want := BareMajorBuilds([]string{"25.4.4.1-graalce", "25.0.2-graalce"}, OracleGraalVMSpec); len(builds) != 0 || want != "" {
457+
t.Errorf("BareMajorBuilds(graalce versions) = %q, %q; want none", builds, want)
458+
}
459+
if !ChecksOracleGraalVM("graalce") || ChecksOracleGraalVM("tem") {
460+
t.Error("only the GraalVM Community combos check Oracle GraalVM's bare major")
461+
}
462+
}

‎tests/e2e/linux/e2e_test.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c
168168
setupVfox(ctx, t, ctr, c.Vfox)
169169
checkVersionlessRejected(ctx, t, ctr, c.Distribution)
170170
checkMissingVersionRejected(ctx, t, ctr, c.Distribution, version, arch)
171+
if common.ChecksOracleGraalVM(c.Distribution) {
172+
checkGraalVMBareMajor(ctx, t, ctr)
173+
}
171174

172175
installed := installJava(ctx, t, ctr, common.Spec(c.Distribution, version))
173176
verifySearch(ctx, t, ctr, c.Distribution, installed)

‎tests/e2e/linux/setup.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,29 @@ func checkMissingVersionRejected(ctx context.Context, t *testing.T, ctr tc.Conta
9595
t.Logf("PASS java@%s is rejected on linux/%s", spec, arch)
9696
}
9797
}
98+
99+
// checkGraalVMBareMajor proves Oracle GraalVM's bare major installs the
100+
// newest build of its 25.0 line, not one of the monthly feature releases
101+
// Foojay lists before it (see common.OracleGraalVMSpec). The builds of the
102+
// major `vfox search java graal` lists are faked as installed first (see
103+
// common.BareMajorBuilds), so `vfox install` names the one PreInstall picked
104+
// and downloads nothing.
105+
func checkGraalVMBareMajor(ctx context.Context, t *testing.T, ctr tc.Container) {
106+
t.Helper()
107+
spec := common.OracleGraalVMSpec
108+
out := execRetry(ctx, t, ctr, "vfox search java graal", "vfox search java graal")
109+
builds, want := common.BareMajorBuilds(common.SearchedVersions(out), spec)
110+
require.NotEmptyf(t, want, "vfox search java graal lists no %s build but feature releases\n--- output ---\n%s", spec, out)
111+
var dirs, javaDirs []string
112+
for _, build := range builds {
113+
dir := `"${VFOX_HOME:-$HOME/.vfox}"/cache/java/` + shQuote("v-"+build)
114+
dirs = append(dirs, dir)
115+
javaDirs = append(javaDirs, dir+"/"+shQuote("java-"+build))
116+
}
117+
execOK(ctx, t, ctr, "mkdir --parents "+strings.Join(javaDirs, " "))
118+
out = execRetry(ctx, t, ctr, "vfox install java@"+spec, "vfox install java@"+spec)
119+
require.Containsf(t, common.StripANSI(out), "java@"+want+" is already installed",
120+
"vfox install java@%s must take %s, the newest build that is no feature release", spec, want)
121+
execOK(ctx, t, ctr, "rm --recursive "+strings.Join(dirs, " "))
122+
t.Logf("PASS java@%s takes %s out of %s", spec, want, strings.Join(builds, ", "))
123+
}

‎tests/e2e/windows/e2e_test.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c
245245
activation := fetchActivation(ctx, t, ctr, box)
246246
checkVersionlessRejected(ctx, t, ctr, box, activation, c.Distribution)
247247
checkMissingVersionRejected(ctx, t, ctr, box, c.Distribution, version, arch)
248+
if common.ChecksOracleGraalVM(c.Distribution) {
249+
checkGraalVMBareMajor(ctx, t, ctr, box)
250+
}
248251

249252
installed := installJava(ctx, t, ctr, box, common.Spec(c.Distribution, version))
250253
verifySearch(ctx, t, ctr, box, c.Distribution, installed)

‎tests/e2e/windows/setup.go‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,30 @@ func checkMissingVersionRejected(ctx context.Context, t *testing.T, ctr tc.Conta
314314
t.Logf("PASS java@%s is rejected on windows/%s", spec, arch)
315315
}
316316
}
317+
318+
// checkGraalVMBareMajor proves Oracle GraalVM's bare major installs the
319+
// newest build of its 25.0 line, not one of the monthly feature releases
320+
// Foojay lists before it (see common.OracleGraalVMSpec). The builds of the
321+
// major `vfox search java graal` lists are faked as installed first (see
322+
// common.BareMajorBuilds), so `vfox install` names the one PreInstall picked
323+
// and downloads nothing.
324+
func checkGraalVMBareMajor(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox) {
325+
t.Helper()
326+
spec := common.OracleGraalVMSpec
327+
out := execRetry(ctx, t, ctr, "vfox search java graal", "vfox search java graal", box.vars...)
328+
builds, want := common.BareMajorBuilds(common.SearchedVersions(out), spec)
329+
require.NotEmptyf(t, want, "vfox search java graal lists no %s build but feature releases\n--- output ---\n%s", spec, out)
330+
var create, remove []string
331+
for _, build := range builds {
332+
dir := fmt.Sprintf("(Join-Path $env:VFOX_HOME %s)", pwshQuote(`cache\java\v-`+build))
333+
create = append(create, fmt.Sprintf("New-Item -ItemType Directory -Force -Path (Join-Path %s %s) | Out-Null",
334+
dir, pwshQuote("java-"+build)))
335+
remove = append(remove, fmt.Sprintf("Remove-Item -LiteralPath %s -Recurse -Force", dir))
336+
}
337+
execOK(ctx, t, ctr, strings.Join(create, "; "), box.vars...)
338+
out = execRetry(ctx, t, ctr, "vfox install java@"+spec, "vfox install "+pwshQuote("java@"+spec), box.vars...)
339+
require.Containsf(t, common.StripANSI(out), "java@"+want+" is already installed",
340+
"vfox install java@%s must take %s, the newest build that is no feature release", spec, want)
341+
execOK(ctx, t, ctr, strings.Join(remove, "; "), box.vars...)
342+
t.Logf("PASS java@%s takes %s out of %s", spec, want, strings.Join(builds, ", "))
343+
}

‎tests/pre_install_test.lua‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ local foojay = {
4343
["temurin 21.0.8 available"] = { pkg("21.0.12.1+1") },
4444
["graalvm 17"] = {},
4545
["graalvm 17 available"] = { pkg("17.0.12") },
46+
-- Observed on 2026-10-18: Oracle GraalVM's monthly feature releases come
47+
-- before its 25.0 line, and so do GraalVM Community's, whose 25.0 line
48+
-- ended with 25.0.2.
49+
["graalvm 25 available"] = {
50+
pkg("25.4.4.1"),
51+
pkg("25.3.4.1"),
52+
pkg("25.2.4"),
53+
pkg("25.1.3"),
54+
pkg("25.0.4.1"),
55+
pkg("25.0.4"),
56+
},
57+
["graalvm 25.4.4.1"] = { pkg("25.4.4.1") },
58+
["graalvm_community 25 available"] = {
59+
pkg("25.4.4.1"),
60+
pkg("25.3.4.1"),
61+
pkg("25.2.4"),
62+
pkg("25.1.3"),
63+
pkg("25.0.2"),
64+
},
4665
["zulu 21"] = { pkg("21+35"), pkg("21+35", true) },
4766
["zulu 21 available"] = { pkg("21.0.12.1+1"), pkg("21.0.12.1+1", true) },
4867
["openjdk 21"] = { pkg("21+35") },
@@ -66,6 +85,14 @@ local foojay = {
6685
["windows jetbrains 21"] = { pkg("21+2121") },
6786
["windows jetbrains 21 available"] = { pkg("21.0.11+1163116"), pkg("21.0.11+1163116", false, "zip") },
6887
["windows jetbrains 21.0.5"] = { pkg("21.0.5+79248"), pkg("21.0.5+76240") },
88+
["windows graalvm 25 available"] = {
89+
pkg("25.4.4.1", false, "zip"),
90+
pkg("25.3.4.1", false, "zip"),
91+
pkg("25.2.4", false, "zip"),
92+
pkg("25.1.3", false, "zip"),
93+
pkg("25.0.4.1", false, "zip"),
94+
pkg("25.0.4", false, "zip"),
95+
},
6996
["windows jetbrains"] = {
7097
pkg("21.0.11+1163116"),
7198
pkg("21.0.11+1163116", false, "zip"),
@@ -168,6 +195,13 @@ check("21-zulu", "21.0.12.1+1-zulu", true)
168195
check("21-zulu-fx", "21.0.12.1+1-zulu-fx", true)
169196
-- Foojay lists no GraalVM 17 GA build, so this found nothing before.
170197
check("17-graal", "17.0.12-graal", true)
198+
-- Oracle GraalVM's bare major takes the newest build of its 25.0 line, not a
199+
-- monthly feature release, which installs when named in full.
200+
check("25-graal", "25.0.4.1-graal", true)
201+
check("graalvm-25", "25.0.4.1-graal", true)
202+
check("25.4.4.1-graal", "25.4.4.1-graal", false)
203+
-- GraalVM Community's feature releases are the only ones it still updates.
204+
check("25-graalce", "25.4.4.1-graalce", true)
171205
-- Anything longer than a major still matches exactly.
172206
check("21.0.8-tem", "21.0.8+9-tem", false)
173207
check("28.ea", "28-ea+19", false)
@@ -223,6 +257,7 @@ end
223257
RUNTIME.osType = "windows"
224258
assert(check("21-jb", "21.0.11+1163116-jb", true, "zip") == 1, "21-jb: expected one lookup")
225259
assert(check("21.0.5-jb", "21.0.5+79248-jb", false, "tar.gz") == 1, "21.0.5-jb: expected one lookup")
260+
check("25-graal", "25.0.4.1-graal", true, "zip")
226261
local jb = {}
227262
for _, item in ipairs(PLUGIN:Available({ args = { "jb" } })) do
228263
table.insert(jb, item.version)

0 commit comments

Comments
 (0)