Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions docs/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,21 @@ JDK, so `java` can only come from what vfox installs:
(`<version>+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 <distribution>` (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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions hooks/pre_install.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions lib/distribution_version.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions tests/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions tests/e2e/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
3 changes: 3 additions & 0 deletions tests/e2e/linux/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions tests/e2e/linux/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ", "))
}
3 changes: 3 additions & 0 deletions tests/e2e/windows/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
27 changes: 27 additions & 0 deletions tests/e2e/windows/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ", "))
}
35 changes: 35 additions & 0 deletions tests/pre_install_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
Expand All @@ -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"),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading