diff --git a/docs/e2e.md b/docs/e2e.md index 98a8555..7a3bfee 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -73,8 +73,11 @@ JDK, so `java` can only come from what vfox installs: 3. 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 + lists that same name as installed, noting JavaFX on `-fx` builds. For + GraalVM it must also list the feature releases GraalVM ships every month + since 25.1 (`25.4.4.1-graalce`), none of them noted LTS, though Foojay + marks them LTS, as the JDK they build on is; each lasts about a month. + On Windows the list includes JetBrains Runtime 21.0.5, which ships only as tar.gz there. 4. Before any `vfox use`, a `.sdkmanrc` holding the SDKMAN identifier without the build number selects the JDK through `ParseLegacyFile`: diff --git a/hooks/available.lua b/hooks/available.lua index 22404bf..4f4d2d4 100644 --- a/hooks/available.lua +++ b/hooks/available.lua @@ -16,6 +16,10 @@ local foojay = require("foojay") local loongnix = require("loongnix") local distribution_version_parser = require("distribution_version") +-- is_feature_release reads GraalVM's numbering only: Liberica NIK and Mandrel +-- number every release after themselves (Mandrel 23.1.12 is LTS). +local graalvm = { graal = true, graalce = true } + --- Return all available versions provided by this plugin --- @param ctx table Empty table used as context, for future extension --- @return table Descriptions of available versions and accompanying tool descriptions @@ -73,7 +77,10 @@ function PLUGIN:Available(ctx) if not seen[v] then seen[v] = true -- check if version exists - local note = jdk.term_of_support == "lts" and "LTS" or "" + -- Foojay marks GraalVM's feature releases LTS, as the JDK they build on is. + local lts = jdk.term_of_support == "lts" + and not (graalvm[short] and distribution_version_parser.is_feature_release(jdk.java_version)) + local note = lts and "LTS" or "" if jdk.javafx_bundled == true then note = note == "" and "JavaFX" or note .. ", JavaFX" end 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..da90555 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -55,7 +55,8 @@ var DefaultVersions = map[string]string{ // (vfox-java#72). "jb": "21.0.5", // GraalVM Community: vfox must leave GRAALVM_HOME alone even for - // GraalVM (vfox-java#27). + // GraalVM (vfox-java#27), and searching must not mark its feature + // releases LTS (vfox-java#95). "graalce": "25.0.2", } @@ -368,6 +369,16 @@ func PaddedSpecs(distribution, version, installed string) []string { return specs } +// GraalVM reports whether the distribution is Oracle GraalVM or GraalVM +// Community, whose searches list GraalVM's feature releases. +func GraalVM(distribution string) bool { + switch Base(distribution) { + case "graal", "graalce": + return true + } + return false +} + // Vendor returns the java.vendor property the distribution's builds report, // when the suite knows it. func Vendor(distribution string) (string, bool) { @@ -506,6 +517,27 @@ func CheckInstalled(t *testing.T, output, spec string) string { return installed } +// featureReleaseEntry matches the `vfox search` entry of a GraalVM feature +// release. GraalVM ships one every month since 25.1, numbered with the +// feature release train as the minor version (25.4.4.1-graalce), which its +// other releases keep at zero (25.0.2-graalce). +var featureReleaseEntry = regexp.MustCompile(`^\d+\.[1-9]`) + +// FeatureReleaseEntries returns the entries of GraalVM feature releases in +// a GraalVM `vfox search`, without the leading " - ". Foojay marks them LTS, +// as the JDK they build on is, and the Available hook passed that on, though +// each is superseded within a month (vfox-java#95). +func FeatureReleaseEntries(output string) []string { + var entries []string + for _, line := range strings.Split(StripANSI(output), "\n") { + entry, ok := strings.CutPrefix(strings.TrimRight(line, "\r"), " - ") + if ok && featureReleaseEntry.MatchString(entry) { + entries = append(entries, entry) + } + } + return entries +} + // SearchEntry finds the `vfox search` entry listing exactly version // (" - [ ()][ (installed)]") and returns it without the // leading " - ". diff --git a/tests/e2e/common/common_test.go b/tests/e2e/common/common_test.go index d9edc2a..510f251 100644 --- a/tests/e2e/common/common_test.go +++ b/tests/e2e/common/common_test.go @@ -290,6 +290,9 @@ func TestDistributionFacts(t *testing.T) { if !JavaFX("librca-fx") || JavaFX("tem") || Base("librca-fx") != "librca" { t.Error("JavaFX/Base mishandle the -fx suffix") } + if !GraalVM("graal") || !GraalVM("graalce") || GraalVM("mandrel") || GraalVM("nik") { + t.Error("GraalVM must hold for graal and graalce alone") + } if SearchArgs("open") != "" || SearchArgs("librca-fx") != " librca" { t.Errorf("SearchArgs = %q, %q", SearchArgs("open"), SearchArgs("librca-fx")) } @@ -379,6 +382,19 @@ func TestCheckInstalled(t *testing.T) { } } +func TestFeatureReleaseEntries(t *testing.T) { + out := "Available versions:\r\n" + + " - 25.4.4.1-graalce (LTS)\r\n" + + " - 25.1.3-graalce\r\n" + + " - 25.0.2-graalce (LTS) (installed)\r\n" + + " - 25-graalce (LTS)\r\n" + + " - 24.0.2-graalce\r\n" + want := []string{"25.4.4.1-graalce (LTS)", "25.1.3-graalce"} + if got := FeatureReleaseEntries(out); !slices.Equal(got, want) { + t.Errorf("FeatureReleaseEntries = %q, want %q", got, want) + } +} + func TestSearchEntry(t *testing.T) { out := "Available versions:\n" + " - 25.0.4+9-librca (LTS)\n" + diff --git a/tests/e2e/linux/verify.go b/tests/e2e/linux/verify.go index 2d798f1..53b43ba 100644 --- a/tests/e2e/linux/verify.go +++ b/tests/e2e/linux/verify.go @@ -40,7 +40,9 @@ const userGraalVMHome = "/opt/graalvm-for-native-image" // verifySearch checks `vfox search java` (the plugin's Available hook) lists // the installed version under the name PreInstall gave it, marked -// installed, with the JavaFX note for -fx builds. +// installed, with the JavaFX note for -fx builds. For GraalVM it must also +// list feature releases (e.g. 25.4.4.1-graalce), none of them marked LTS +// (see common.FeatureReleaseEntries). func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, distribution, installed string) { t.Helper() cmd := "vfox search java" + common.SearchArgs(distribution) @@ -52,6 +54,16 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, distribut require.Containsf(t, entry, "JavaFX", "%s must note JavaFX on %s", cmd, installed) } t.Logf("PASS %s lists %s", cmd, entry) + if common.GraalVM(distribution) { + // Foojay lists every feature release since 25.1.3 on each platform + // the suite runs GraalVM on, so the check cannot pass vacuously. + features := common.FeatureReleaseEntries(out) + require.NotEmptyf(t, features, "%s lists no feature release\n--- output ---\n%s", cmd, out) + for _, feature := range features { + require.NotContainsf(t, feature, "LTS", "%s must not mark the feature release %s LTS", cmd, feature) + } + t.Logf("PASS %s marks no feature release LTS: %s", cmd, strings.Join(features, ", ")) + } } // verifyLegacyFile proves ParseLegacyFile selects the installed JDK from a diff --git a/tests/e2e/windows/verify.go b/tests/e2e/windows/verify.go index 9a7e5af..d7c83f2 100644 --- a/tests/e2e/windows/verify.go +++ b/tests/e2e/windows/verify.go @@ -43,6 +43,8 @@ const userGraalVMHome = `C:\graalvm-for-native-image` // the installed version under the name PreInstall gave it, marked // installed, with the JavaFX note for -fx builds. That includes releases // shipped only as tar.gz, such as JetBrains Runtime 21.0.5 (vfox-java#72). +// For GraalVM it must also list feature releases (e.g. 25.4.4.1-graalce), +// none of them marked LTS (see common.FeatureReleaseEntries). func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, distribution, installed string) { t.Helper() cmd := "vfox search java" + common.SearchArgs(distribution) @@ -54,6 +56,16 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, box slotB require.Containsf(t, entry, "JavaFX", "%s must note JavaFX on %s", cmd, installed) } t.Logf("PASS %s lists %s", cmd, entry) + if common.GraalVM(distribution) { + // Foojay lists every feature release since 25.1.3 on each platform + // the suite runs GraalVM on, so the check cannot pass vacuously. + features := common.FeatureReleaseEntries(out) + require.NotEmptyf(t, features, "%s lists no feature release\n--- output ---\n%s", cmd, out) + for _, feature := range features { + require.NotContainsf(t, feature, "LTS", "%s must not mark the feature release %s LTS", cmd, feature) + } + t.Logf("PASS %s marks no feature release LTS: %s", cmd, strings.Join(features, ", ")) + } } // verifyLegacyFile proves ParseLegacyFile selects the installed JDK from a diff --git a/tests/pre_install_test.lua b/tests/pre_install_test.lua index 8844fe9..92118f0 100644 --- a/tests/pre_install_test.lua +++ b/tests/pre_install_test.lua @@ -27,6 +27,11 @@ local function pkg(java_version, fx, archive_type) } end +local function lts(jdk) + jdk.term_of_support = "lts" + return jdk +end + -- Foojay answers observed on 2026-10-17 for linux/amd64 tar.gz packages, keyed -- by "[ ][ ]", and for windows/amd64 zip and -- tar.gz packages, keyed by "windows [ ][ ]". @@ -51,6 +56,11 @@ local foojay = { -- Searching asks for no version; abridged. ["openjdk"] = { pkg("25.0.2+10"), pkg("21.0.2+13") }, ["temurin"] = { pkg("25.0.2+10"), pkg("21.0.8+9") }, + -- Observed on 2026-10-18: Foojay marks GraalVM's feature releases LTS, as + -- 25.0 is, and Mandrel 23.1 LTS, as the JDK 21 it builds on is; abridged. + ["graalvm"] = { lts(pkg("25.4.4.1")), lts(pkg("25.1.3")), lts(pkg("25.0.4.1")), lts(pkg("25")), pkg("24.0.2") }, + ["graalvm_community"] = { lts(pkg("25.4.4.1")), lts(pkg("25.0.2")) }, + ["mandrel"] = { lts(pkg("23.1.12")) }, -- JetBrains Runtime ships Windows zips for its newer builds only, and Foojay -- lists a version's zip and tar.gz in either order; abridged. ["windows jetbrains 21"] = { pkg("21+2121") }, @@ -178,7 +188,7 @@ assert(#requests == 1 and not requests[1]:find("latest=", 1, true), "Available: local function listed(query) local versions = {} for _, item in ipairs(PLUGIN:Available({ args = { query } })) do - versions[item.version] = true + versions[item.version] = item.note end return versions end @@ -188,6 +198,25 @@ end local all = listed("all") assert(all["25.0.2+10-tem"] and all["21.0.8+9-tem"], "Available all: Temurin versions lost their suffix") +-- Searching drops the LTS note from GraalVM's feature releases, each +-- superseded within a month, whether it lists one distribution or all. +-- GraalVM's other releases keep it, and so do Mandrel's, which Mandrel numbers +-- after itself. +for version, note in pairs({ + ["25.4.4.1-graal"] = "", + ["25.1.3-graal"] = "", + ["25.0.4.1-graal"] = "LTS", + ["25-graal"] = "LTS", + ["25.4.4.1-graalce"] = "", + ["25.0.2-graalce"] = "LTS", + ["23.1.12-mandrel"] = "LTS", +}) do + for _, query in ipairs({ version:match("%-(%a+)$"), "all" }) do + local got = listed(query)[version] + assert(got == note, ("Available %s: %s noted %q, want %q"):format(query, version, tostring(got), note)) + end +end + -- Windows asks for zip and tar.gz packages in one lookup: installing takes a -- version's zip where there is one, and searching lists the releases that ship -- only as tar.gz too, newest first and each once.