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
8 changes: 3 additions & 5 deletions docs/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <distribution>` (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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions hooks/pre_install.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 6 additions & 21 deletions lib/foojay.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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

Expand Down
7 changes: 4 additions & 3 deletions tests/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/windows/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
15 changes: 7 additions & 8 deletions tests/e2e/windows/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/windows/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 3 additions & 12 deletions tests/e2e/windows/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
68 changes: 56 additions & 12 deletions tests/pre_install_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<distribution>[ <version>][ <latest>]".
-- by "<distribution>[ <version>][ <latest>]", and for windows/amd64 zip and
-- tar.gz packages, keyed by "windows <distribution>[ <version>][ <latest>]".
local foojay = {
["temurin 21"] = { pkg("21+35") },
["temurin 21 available"] = { pkg("21.0.12.1+1") },
Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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 = {}
Expand All @@ -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")
Loading