From c368a2f220506b214b3a704d2d96294d96f934a2 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sat, 17 Oct 2026 15:43:46 +0800 Subject: [PATCH] fix: list releases that ship only as `tar.gz` in `vfox search` on Windows --- docs/e2e.md | 8 ++--- hooks/pre_install.lua | 8 +++++ lib/foojay.lua | 27 ++++---------- tests/e2e/common/common.go | 7 ++-- tests/e2e/windows/e2e_test.go | 4 +-- tests/e2e/windows/install.go | 15 ++++---- tests/e2e/windows/setup.go | 4 +-- tests/e2e/windows/verify.go | 15 ++------ tests/pre_install_test.lua | 68 ++++++++++++++++++++++++++++------- 9 files changed, 91 insertions(+), 65 deletions(-) diff --git a/docs/e2e.md b/docs/e2e.md index 568fae9..342c107 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -69,10 +69,8 @@ JDK, so `java` can only come from what vfox installs: 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, a JDK installed through the tar.gz fallback is exempt (logged as - a SKIP when absent): `Available` lists zip packages and reaches tar.gz - ones only when a distribution has no zip at all, so JetBrains Runtime - 21.0.5 installs but is not listed. + 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 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 @@ -100,7 +98,7 @@ The default matrix runs every distribution against both vfox channels | `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 fallback on Windows, which has no zip for it | ✓ | ✓ | +| `jb` | 21.0.5 | the tar.gz path on Windows, which has no zip for it | ✓ | ✓ | | `graalce` | 25.0.2 | `GRAALVM_HOME` | ✓ | | Every pinned release is published through Foojay for each platform its row diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index ddb2920..f399025 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -70,6 +70,14 @@ function PLUGIN:PreInstall(ctx) 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. + for _, candidate in ipairs(filtered_jdks) do + if candidate.java_version == jdk.java_version and candidate.archive_type == "zip" then + jdk = candidate + break + end + end if not jdk.links or not jdk.links.pkg_info_uri then error("Invalid JDK package info: missing download links for " .. ctx.version) end diff --git a/lib/foojay.lua b/lib/foojay.lua index 5c959b1..c0c37c1 100644 --- a/lib/foojay.lua +++ b/lib/foojay.lua @@ -18,7 +18,7 @@ local json = require("json") local foojay = {} local URL = - "https://api.foojay.io/disco/v3.0/packages/jdks?version=%s&distribution=%s&architecture=%s&archive_type=%s&operating_system=%s&lib_c_type=%s&directly_downloadable=true" + "https://api.foojay.io/disco/v3.0/packages/jdks?version=%s&distribution=%s&architecture=%s&%s&operating_system=%s&lib_c_type=%s&directly_downloadable=true" --- Detects the libc type on Linux systems (glibc or musl) --- @return string "glibc" or "musl" @@ -46,9 +46,11 @@ foojay.fetchtJdkList = function(distribution, version) os = "macos" end - local archive_type = "tar.gz" + local archive_types = "archive_type=tar.gz" if os == "windows" then - archive_type = "zip" + -- JetBrains Runtime ships Windows zips for its newer builds only. Foojay + -- lists both archive types in one answer, still newest version first. + archive_types = "archive_type=zip&archive_type=tar.gz" if arch == "amd64" then arch = "x64" end @@ -64,7 +66,7 @@ foojay.fetchtJdkList = function(distribution, version) -- latest=available would look at the major alone. local latest = version:match("^%d+$") and "&latest=available" or "" - local reqUrl = URL:format(version, distribution, arch, archive_type, os, lib_c_type) .. latest + local reqUrl = URL:format(version, distribution, arch, archive_types, os, lib_c_type) .. latest local resp, err = http.get({ url = reqUrl, @@ -79,23 +81,6 @@ foojay.fetchtJdkList = function(distribution, version) local respBody = json.decode(resp.body) local result = respBody.result - -- Fallback to tar.gz on Windows if no zip packages found (e.g. JetBrains JDK) - if os == "windows" and (not result or #result == 0) then - archive_type = "tar.gz" - reqUrl = URL:format(version, distribution, arch, archive_type, os, lib_c_type) .. latest - resp, err = http.get({ - url = reqUrl, - }) - if err ~= nil then - error("Failed to fetch jdk info from foojay: " .. err) - end - if resp.status_code ~= 200 then - error("Failed to fetch jdk info from foojay: status_code =>" .. resp.status_code) - end - respBody = json.decode(resp.body) - result = respBody.result - end - return result end diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go index d939e03..e576a62 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -50,8 +50,9 @@ var DefaultVersions = map[string]string{ "tem": "21.0.8", // Liberica with JavaFX: the -fx selection in PreInstall and PreUse. "librca-fx": "25.0.4", - // JetBrains Runtime 21.0.5 ships no Windows zip, so on Windows it takes - // the tar.gz fallback in lib/foojay.lua (vfox-java#58). + // JetBrains Runtime 21.0.5 ships no Windows zip, so on Windows it + // installs from a tar.gz (vfox-java#58), and searching must list it too + // (vfox-java#72). "jb": "21.0.5", // GraalVM Community: the GRAALVM_HOME branch of hooks/env_keys.lua. "graalce": "25.0.2", @@ -401,7 +402,7 @@ func InstalledVersion(output string) string { var unpackedTarball = regexp.MustCompile(`(?m)^Unpacking .*\.tar\.gz\.\.\.\r?$`) // FromTarball reports whether `vfox install` output unpacked a .tar.gz, i.e. -// on Windows the JDK came from lib/foojay.lua's tar.gz fallback. +// on Windows the JDK came from a release Foojay lists with no zip. func FromTarball(output string) bool { return unpackedTarball.MatchString(StripANSI(output)) } diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index cfac072..6db27b9 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -186,8 +186,8 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c checkMissingVersionRejected(ctx, t, ctr, box, c.Distribution, version, arch) activation := fetchActivation(ctx, t, ctr, box) - installed, tarball := installJava(ctx, t, ctr, box, common.Spec(c.Distribution, version)) - verifySearch(ctx, t, ctr, box, c.Distribution, installed, tarball) + installed := installJava(ctx, t, ctr, box, common.Spec(c.Distribution, version)) + verifySearch(ctx, t, ctr, box, c.Distribution, installed) // Before any `vfox use`, so java can only come from the legacy file. verifyLegacyFile(ctx, t, ctr, box, c.Distribution, version, arch) diff --git a/tests/e2e/windows/install.go b/tests/e2e/windows/install.go index 0d5b93e..809c506 100644 --- a/tests/e2e/windows/install.go +++ b/tests/e2e/windows/install.go @@ -24,21 +24,20 @@ import ( ) // installJava installs one JDK and returns the version vfox recorded for it -// (see common.CheckInstalled), plus whether it came from the tar.gz fallback -// rather than a zip. Install runs plain: vfox.exe resolves through the slot -// PATH override. -func installJava(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, spec string) (string, bool) { +// (see common.CheckInstalled), logging whether it came from a zip or a +// tar.gz. Install runs plain: vfox.exe resolves through the slot PATH +// override. +func installJava(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, spec string) string { t.Helper() out := execRetry(ctx, t, ctr, "vfox install java@"+spec, "vfox install "+pwshQuote("java@"+spec), box.vars...) installed := common.CheckInstalled(t, out, spec) - tarball := common.FromTarball(out) archive := "zip" - if tarball { - archive = "tar.gz fallback" + if common.FromTarball(out) { + archive = "tar.gz" } t.Logf("PASS java@%s installed as %s (%s)", spec, installed, archive) - return installed, tarball + return installed } // useJava selects the installed JDK globally; `vfox use` needs the diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index 22e3d5d..53f3de0 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -221,8 +221,8 @@ func activatedEnv(activation, script string) string { // checkMissingVersionRejected proves an unpublished release fails loudly // instead of installing something else: PreInstall finds no Foojay package -// (zip, then the tar.gz fallback) and names the platform it searched, which -// must be the runner's (RUNTIME.osType/archType). +// (zip or tar.gz) and names the platform it searched, which must be the +// runner's (RUNTIME.osType/archType). func checkMissingVersionRejected(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, distribution, version, arch string) { t.Helper() spec := common.MissingSpec(distribution, version) diff --git a/tests/e2e/windows/verify.go b/tests/e2e/windows/verify.go index 1ea6490..2483108 100644 --- a/tests/e2e/windows/verify.go +++ b/tests/e2e/windows/verify.go @@ -37,22 +37,13 @@ const showSettings = "-XshowSettings:properties -version" // 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. -// -// A JDK from the tar.gz fallback (tarball) may be missing from the list: -// lib/foojay.lua falls back per request, so Available, which lists zip -// packages, only reaches tar.gz-only releases when the distribution has no -// zip at all. JetBrains Runtime 21.0.5 is such a release; it is logged and -// skipped rather than failed. -func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, distribution, installed string, tarball bool) { +// 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). +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) out := execRetry(ctx, t, ctr, cmd, cmd, box.vars...) entry, ok := common.SearchEntry(out, installed) - if !ok && tarball { - t.Logf("SKIP %s omits %s: installed from the tar.gz fallback, which Available does not list while zip packages exist", cmd, installed) - return - } require.Truef(t, ok, "%s does not list %s\n--- output ---\n%s", cmd, installed, out) require.Containsf(t, entry, "(installed)", "%s must mark %s installed", cmd, installed) if common.JavaFX(distribution) { diff --git a/tests/pre_install_test.lua b/tests/pre_install_test.lua index 1ffa766..29b6fc8 100644 --- a/tests/pre_install_test.lua +++ b/tests/pre_install_test.lua @@ -16,17 +16,20 @@ dofile("tests/support.lua") local JDKS = "https://api.foojay.io/disco/v3.0/packages/jdks?" local IDS = "https://api.foojay.io/disco/v3.0/ids/" -local function pkg(java_version, fx) +local function pkg(java_version, fx, archive_type) + archive_type = archive_type or "tar.gz" return { java_version = java_version, major_version = tonumber(java_version:match("^%d+")), javafx_bundled = fx == true, - links = { pkg_info_uri = IDS .. java_version .. (fx and "-fx" or "") }, + archive_type = archive_type, + links = { pkg_info_uri = IDS .. java_version .. (fx and "-fx" or "") .. "." .. archive_type }, } end -- Foojay answers observed on 2026-10-17 for linux/amd64 tar.gz packages, keyed --- by "[ ][ ]". +-- by "[ ][ ]", and for windows/amd64 zip and +-- tar.gz packages, keyed by "windows [ ][ ]". local foojay = { ["temurin 21"] = { pkg("21+35") }, ["temurin 21 available"] = { pkg("21.0.12.1+1") }, @@ -41,11 +44,21 @@ local foojay = { ["openjdk 21 available"] = { pkg("21.0.2+13") }, ["openjdk 25.0.2"] = { pkg("25.0.2+10") }, ["openjdk 28-ea"] = { pkg("28-ea+19"), pkg("28-ea+18") }, - ["jetbrains 21"] = { pkg("21+2121") }, - ["jetbrains 21 available"] = { pkg("21.0.11+1163116") }, -- 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") }, + -- 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") }, + ["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 jetbrains"] = { + pkg("21.0.11+1163116"), + pkg("21.0.11+1163116", false, "zip"), + pkg("21.0.8+895146", false, "zip"), + pkg("21.0.8+895146"), + pkg("21.0.5+79248"), + }, } local requests = {} @@ -57,8 +70,12 @@ package.preload.http = function() local body if request.url:sub(1, #JDKS) == JDKS then local params = {} + local archive_types = {} for key, value in request.url:sub(#JDKS + 1):gmatch("([^&=]+)=([^&]*)") do params[key] = value + if key == "archive_type" then + archive_types[value] = true + end end local key = params.distribution if params.version ~= "" then @@ -67,8 +84,17 @@ package.preload.http = function() if params.latest then key = key .. " " .. params.latest end - -- Only tar.gz packages are listed, so Windows retries every zip lookup with tar.gz. - body = { result = params.archive_type == "tar.gz" and foojay[key] or {} } + if params.operating_system == "windows" then + key = "windows " .. key + end + -- Only the packages of the archive types asked for are listed. + local result = {} + for _, package in ipairs(foojay[key] or {}) do + if archive_types[package.archive_type] then + table.insert(result, package) + end + end + body = { result = result } else assert(request.url:sub(1, #IDS) == IDS, "unexpected request: " .. request.url) local id = request.url:sub(#IDS + 1) @@ -92,10 +118,16 @@ dofile("hooks/available.lua") dofile("hooks/pre_install.lua") local cases = 0 -local function check(version, expected, latest) +local function check(version, expected, latest, archive_type) requests = {} local result = PLUGIN:PreInstall({ version = version }) assert(result.version == expected, version .. ": expected " .. expected .. ", got " .. tostring(result.version)) + if archive_type then + assert( + result.url:sub(-#archive_type - 1) == "." .. archive_type, + version .. ": expected the " .. archive_type .. " package, got " .. result.url + ) + end local lookups = 0 for _, url in ipairs(requests) do if url:sub(1, #JDKS) == JDKS then @@ -122,10 +154,6 @@ check("28.ea", "28-ea+19", false) -- OpenJDK versions carry no suffix, however the input spells the distribution. check("25.0.2-open", "25.0.2+10", false) check("openjdk-25.0.2", "25.0.2+10", false) --- On Windows the tar.gz retry keeps asking for the newest release. -RUNTIME.osType = "windows" -assert(check("21-jb", "21.0.11+1163116-jb", true) == 2, "21-jb: expected a zip lookup and a tar.gz retry") -RUNTIME.osType = "linux" -- Searching lists every release, not only the newest one. requests = {} @@ -147,4 +175,20 @@ 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") +-- 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. +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") +local jb = {} +for _, item in ipairs(PLUGIN:Available({ args = { "jb" } })) do + table.insert(jb, item.version) +end +assert( + table.concat(jb, " ") == "21.0.11+1163116-jb 21.0.8+895146-jb 21.0.5+79248-jb", + "Available jb on Windows: unexpected " .. table.concat(jb, " ") +) +RUNTIME.osType = "linux" + print("PASS: " .. cases .. " PreInstall Foojay lookup cases")