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
9 changes: 7 additions & 2 deletions docs/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,13 @@ JDK, so `java` can only come from what vfox installs:
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
`java=25.0.4.fx-librca` (its spelling of JavaFX builds). On Windows the
file is written with CRLF line endings.
`java=25.0.4.fx-librca` (its spelling of JavaFX builds). So does one
holding the installed version (`java=21.0.8+9-tem`). Both run next to an
empty install of another release whose version starts with the pinned
one (`21.0.80+1-tem`): vfox sorts installed versions as strings,
descending, so `vfox list java` must show it first, and
`ParseLegacyFile` used to take it as the first one starting with the
release. On Windows the file is written with CRLF line endings.
5. 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
Expand Down
51 changes: 15 additions & 36 deletions hooks/parse_legacy_file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
local strings = require("vfox.strings")

local distribution_version_parser = require("distribution_version")
local installed_version = require("installed_version")

--- Rewrites an SDKMAN identifier in the plugin's spelling, which parse_version
--- reads. SDKMAN calls JetBrains jbr (the plugin's jb) and puts fx in the
Expand Down Expand Up @@ -64,52 +65,30 @@ function PLUGIN:ParseLegacyFile(ctx)
return {}
end

local versionArr = strings.split(fullVersion, "+")
local version = versionArr[1]
local build = versionArr[2]

local version = strings.split(fullVersion, "+")[1]
local majorVersion = strings.split(version, ".")[1]

--- Installed versions end in -<vendor> (OpenJDK's in nothing), then -fx for
--- JavaFX builds: 21.0.8+9-tem, 21.0.12+7-zulu-fx, 25.0.2+10.
local suffix = (vendor == "open" and "" or "-" .. vendor) .. (distributionVersion.javafx_bundled and "-fx" or "")

--- Whether installed version v is of this distribution and JavaFX flavour,
--- with a Java version starting with prefix.
local function matches(v, prefix)
local java_version = v:sub(1, #v - #suffix)
return java_version .. suffix == v
and (vendor ~= "open" or not strings.contains(java_version, "-"))
and strings.has_prefix(java_version, prefix)
end

--- Get the list of versions of the current plugin installed
--- (a plain call: vfox 1.x rejects the extra ctx argument of ctx:...).
local targetVersion
local versions = ctx.getInstalledVersions()
for _, v in ipairs(versions) do
--- It may be exactly the same, try it first.
if v == javaVersion then
targetVersion = v
break
end

if matches(v, version) then
targetVersion = v
break
end
end

if not targetVersion then
for _, v in ipairs(versions) do
if matches(v, majorVersion .. ".") then
targetVersion = v
break
end
end
--- vfox sorts it as strings, descending (21.0.8+9-tem, 21.0.12+8-tem,
--- 21.0.1+12-tem), so the first match is not the one to take.
local installed = {}
for _, v in ipairs(ctx.getInstalledVersions()) do
installed[v] = true
end

--- The version itself (21.0.5+63130-jb), else the newest build of the
--- release, as SDKMAN's build numbers count its own rebuilds
--- (21.0.12+1.1-tem), else the newest installed release of the major
--- (21.0.12+8-tem for 21.0.3-tem).
local name = fullVersion .. suffix
return {
version = targetVersion,
version = installed[name] and name
or installed_version.find(installed, version, suffix)
or installed_version.newest(installed, majorVersion .. ".", "^[%d.+]*$", suffix),
}
end
63 changes: 5 additions & 58 deletions hooks/pre_use.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,7 @@
-- limitations under the License.

local distribution_version_parser = require("distribution_version")

--- Compares the numbers in two version strings one by one, missing ones as 0.
local function compare(a, b)
local x, y = {}, {}
for number in a:gmatch("%d+") do
table.insert(x, tonumber(number))
end
for number in b:gmatch("%d+") do
table.insert(y, tonumber(number))
end
for i = 1, math.max(#x, #y) do
if (x[i] or 0) ~= (y[i] or 0) then
return (x[i] or 0) > (y[i] or 0) and 1 or -1
end
end
return 0
end

--- Whether Java version a is newer than b: the release decides, then the
--- build (21.0.12.1+1 > 21.0.8+9 > 21.0.8+7 > 21+35).
local function newer(a, b)
local a_release, a_build = a:match("^([^+]*)(.*)$")
local b_release, b_build = b:match("^([^+]*)(.*)$")
local order = compare(a_release, b_release)
return order > 0 or order == 0 and compare(a_build, b_build) > 0
end

--- Returns the installed version that version .. suffix names. vfox uses the
--- returned version as is, while installed names carry the build number
--- PreInstall found, which the input may leave out. So, like PreInstall, take
--- the newest build of a release (21.0.8-tem selects 21.0.8+9-tem) and the
--- newest release of a bare major (21-tem selects 21.0.12.1+1-tem), unless the
--- name itself is installed. Without a match, return the name for vfox to
--- report as not installed.
local function installed_version(installed_sdks, version, suffix)
local name = version .. suffix
if installed_sdks[name] then
return name
end
-- A bare major takes any release of it (21.0.12.1+1 for 21) but no EA build
-- (26-ea+13 for 26); other versions take only a build number (not
-- 21.0.12.1+1 for 21.0.12, nor 21.0.12+8 for 21.0.1).
local more = version:match("^%d+$") and "^[.+][%d.+]*$" or "^%+%d+$"
local best, best_version
for candidate in pairs(installed_sdks) do
local java_version = candidate:sub(1, #candidate - #suffix)
if
java_version .. suffix == candidate
and java_version:sub(1, #version) == version
and java_version:sub(#version + 1):find(more)
and (not best or newer(java_version, best_version))
then
best, best_version = candidate, java_version
end
end
return best or name
end
local installed_version = require("installed_version")

function PLUGIN:PreUse(ctx)
local input_version = ctx.version
Expand All @@ -91,8 +35,11 @@ function PLUGIN:PreUse(ctx)
if distribution_version.distribution.short_name ~= "open" then
suffix = "-" .. distribution_version.distribution.short_name .. suffix
end
-- vfox uses the returned version as is, so return the installed one it
-- names or, without a match, the name for vfox to report as not installed.
local name = distribution_version.version .. suffix
return {
version = installed_version(ctx.installedSdks or {}, distribution_version.version, suffix),
version = installed_version.find(ctx.installedSdks or {}, distribution_version.version, suffix) or name,
}
else
return {
Expand Down
82 changes: 82 additions & 0 deletions lib/installed_version.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- Copyright 2026 Han Li and contributors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

--- Finds installed versions, which PreInstall names after the Java version,
--- then -<short name> (nothing for OpenJDK), then -fx for JavaFX builds:
--- 21.0.8+9-tem, 25.0.4+9-librca-fx, 25.0.2+10. installed is a table keyed by
--- those names.
local installed_version = {}

--- Compares the numbers in two version strings one by one, missing ones as 0.
local function compare(a, b)
local x, y = {}, {}
for number in a:gmatch("%d+") do
table.insert(x, tonumber(number))
end
for number in b:gmatch("%d+") do
table.insert(y, tonumber(number))
end
for i = 1, math.max(#x, #y) do
if (x[i] or 0) ~= (y[i] or 0) then
return (x[i] or 0) > (y[i] or 0) and 1 or -1
end
end
return 0
end

--- Whether Java version a is newer than b: the release decides, then the
--- build (21.0.12.1+1 > 21.0.8+9 > 21.0.8+7 > 21+35).
local function newer(a, b)
local a_release, a_build = a:match("^([^+]*)(.*)$")
local b_release, b_build = b:match("^([^+]*)(.*)$")
local order = compare(a_release, b_release)
return order > 0 or order == 0 and compare(a_build, b_build) > 0
end

--- Returns the newest installed version ending in suffix whose Java version is
--- prefix followed by a rest that the pattern rest matches, or nil.
function installed_version.newest(installed, prefix, rest, suffix)
local best, best_version
for candidate in pairs(installed) do
local java_version = candidate:sub(1, #candidate - #suffix)
if
java_version .. suffix == candidate
and java_version:sub(1, #prefix) == prefix
and java_version:sub(#prefix + 1):find(rest)
and (not best or newer(java_version, best_version))
then
best, best_version = candidate, java_version
end
end
return best
end

--- Returns the installed version that version .. suffix names, or nil. The
--- input may leave out the build number PreInstall found, so, like PreInstall,
--- take the newest build of a release (21.0.8-tem selects 21.0.8+9-tem) and
--- the newest release of a bare major (21-tem selects 21.0.12.1+1-tem), unless
--- the name itself is installed.
function installed_version.find(installed, version, suffix)
local name = version .. suffix
if installed[name] then
return name
end
-- A bare major takes any release of it (21.0.12.1+1 for 21) but no EA build
-- (26-ea+13 for 26); other versions take only a build number (not
-- 21.0.12.1+1 for 21.0.12, nor 21.0.12+8 for 21.0.1).
local rest = version:match("^%d+$") and "^[.+][%d.+]*$" or "^%+%d+$"
return installed_version.newest(installed, version, rest, suffix)
end

return installed_version
25 changes: 25 additions & 0 deletions tests/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ func MissingBuildSpec(distribution, version string) string {
return Spec(distribution, version+"+99")
}

// Decoy renders the installed name of another release whose version starts
// with the pinned one (<version>0+1, e.g. 21.0.80+1-tem). vfox lists the
// installed versions sorted as strings, descending, so it comes before the
// pinned JDK (21.0.8+9-tem), and ParseLegacyFile, which took the first one
// starting with the release, selected it (vfox-java#79).
func Decoy(distribution, version string) string {
return Spec(distribution, version+"0+1")
}

// Major returns the leading version component: "21.0.8" -> "21".
func Major(version string) string {
return strings.SplitN(version, ".", 2)[0]
Expand Down Expand Up @@ -406,6 +415,22 @@ func InstalledVersion(output string) string {
return ""
}

// listedVersion matches one `vfox list java` entry: "-> <version>", followed
// by "<— current" for the version in use.
var listedVersion = regexp.MustCompile(`^-> (\S+)`)

// ListedVersions extracts the installed versions from `vfox list java`
// output, in the order vfox lists them.
func ListedVersions(output string) []string {
var versions []string
for _, line := range strings.Split(StripANSI(output), "\n") {
if m := listedVersion.FindStringSubmatch(strings.TrimRight(line, "\r")); m != nil {
versions = append(versions, m[1])
}
}
return versions
}

// unpackedTarball matches the "Unpacking <archive>..." line vfox prints
// before extracting a .tar.gz.
var unpackedTarball = regexp.MustCompile(`(?m)^Unpacking .*\.tar\.gz\.\.\.\r?$`)
Expand Down
24 changes: 19 additions & 5 deletions tests/e2e/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ func TestRetryAttemptsDefault(t *testing.T) {
}

func TestSpecs(t *testing.T) {
for _, c := range []struct{ dist, version, spec, missing, missingBuild, legacy string }{
{"open", "25.0.2", "25.0.2", "25.0.99", "25.0.2+99", "25.0.2-open"},
{"tem", "21.0.8", "21.0.8-tem", "21.0.99-tem", "21.0.8+99-tem", "21.0.8-tem"},
{"librca-fx", "25.0.4", "25.0.4-librca-fx", "25.0.99-librca-fx", "25.0.4+99-librca-fx", "25.0.4.fx-librca"},
{"jb", "21.0.5", "21.0.5-jb", "21.0.99-jb", "21.0.5+99-jb", "21.0.5-jbr"},
for _, c := range []struct{ dist, version, spec, missing, missingBuild, legacy, decoy string }{
{"open", "25.0.2", "25.0.2", "25.0.99", "25.0.2+99", "25.0.2-open", "25.0.20+1"},
{"tem", "21.0.8", "21.0.8-tem", "21.0.99-tem", "21.0.8+99-tem", "21.0.8-tem", "21.0.80+1-tem"},
{"librca-fx", "25.0.4", "25.0.4-librca-fx", "25.0.99-librca-fx", "25.0.4+99-librca-fx", "25.0.4.fx-librca", "25.0.40+1-librca-fx"},
{"jb", "21.0.5", "21.0.5-jb", "21.0.99-jb", "21.0.5+99-jb", "21.0.5-jbr", "21.0.50+1-jb"},
} {
if got := Spec(c.dist, c.version); got != c.spec {
t.Errorf("Spec(%s, %s) = %q, want %q", c.dist, c.version, got, c.spec)
Expand All @@ -202,6 +202,9 @@ func TestSpecs(t *testing.T) {
if got := LegacyIdentifier(c.dist, c.version); got != c.legacy {
t.Errorf("LegacyIdentifier(%s, %s) = %q, want %q", c.dist, c.version, got, c.legacy)
}
if got := Decoy(c.dist, c.version); got != c.decoy {
t.Errorf("Decoy(%s, %s) = %q, want %q", c.dist, c.version, got, c.decoy)
}
}
}

Expand Down Expand Up @@ -294,6 +297,17 @@ func TestInstalledVersion(t *testing.T) {
}
}

func TestListedVersions(t *testing.T) {
out := "-> 21.0.80+1-tem\r\n" +
"-> 21.0.8+9-tem \x1b[92m<— current\x1b[0m\r\n"
if got := ListedVersions(out); len(got) != 2 || got[0] != "21.0.80+1-tem" || got[1] != "21.0.8+9-tem" {
t.Errorf("ListedVersions = %q, want [21.0.80+1-tem 21.0.8+9-tem]", got)
}
if got := ListedVersions("no available version\n"); len(got) != 0 {
t.Errorf("ListedVersions = %q, want none", got)
}
}

func TestFromTarball(t *testing.T) {
tarball := "Preinstalling java@21.0.5+79248-jb...\r\n" +
"Unpacking C:\\Users\\John Doe\\.vfox\\cache\\java\\v-21.0.5+79248-jb\\jbrsdk_jcef-21.0.5-windows-x64-b792.48.tar.gz...\r\n"
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/linux/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c
installed := installJava(ctx, t, ctr, common.Spec(c.Distribution, version))
verifySearch(ctx, t, ctr, c.Distribution, installed)
// Before any `vfox use`, so java can only come from the legacy file.
verifyLegacyFile(ctx, t, ctr, c.Distribution, version, arch)
verifyLegacyFile(ctx, t, ctr, c.Distribution, version, installed, arch)

useJava(ctx, t, ctr, installed)
verifyToolchain(ctx, t, ctr, c.Distribution, version, installed, arch)
Expand Down
35 changes: 24 additions & 11 deletions tests/e2e/linux/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,33 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, distribut
// verifyLegacyFile proves ParseLegacyFile selects the installed JDK from a
// .sdkmanrc (PLUGIN.legacyFilenames) holding the SDKMAN identifier without
// the build number (e.g. java=21.0.8-tem, java=21.0.5-jbr,
// java=25.0.4.fx-librca). It runs before any `vfox use`, so java can only
// java=25.0.4.fx-librca) or the installed version itself (e.g.
// java=21.0.8+9-tem), while vfox lists an empty install of another release
// first (see common.Decoy). It runs before any `vfox use`, so java can only
// come from the legacy file.
func verifyLegacyFile(ctx context.Context, t *testing.T, ctr tc.Container, distribution, version, arch string) {
func verifyLegacyFile(ctx context.Context, t *testing.T, ctr tc.Container, distribution, version, installed, arch string) {
t.Helper()
const dir = "/tmp/e2e-legacy-sdkmanrc"
identifier := common.LegacyIdentifier(distribution, version)
execOK(ctx, t, ctr, fmt.Sprintf("mkdir --parents %s && printf 'java=%%s\\n' %s > %s/.sdkmanrc",
dir, shQuote(identifier), dir))
// The probe activates inside its own exec, a fresh shell session, so no
// `vfox env` state cached by an earlier call can answer for the file.
out := execRetry(ctx, t, ctr, ".sdkmanrc java="+identifier, fmt.Sprintf(
`cd %s && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && %s`, dir, showSettings))
common.CheckJava(t, out, distribution, version, arch, ".sdkmanrc java="+identifier)
t.Logf("PASS .sdkmanrc java=%s selects java %s", identifier, version)
// vfox counts a version as installed once its directory holds
// java-<version>; the decoy holds nothing more, so selecting it leaves
// no java on PATH.
decoy := common.Decoy(distribution, version)
decoyDir := `"${VFOX_HOME:-$HOME/.vfox}"/cache/java/` + shQuote("v-"+decoy)
execOK(ctx, t, ctr, "mkdir --parents "+decoyDir+"/"+shQuote("java-"+decoy))
listed := common.ListedVersions(execOK(ctx, t, ctr, "vfox list java"))
require.Equalf(t, []string{decoy, installed}, listed, "vfox list java must list the decoy %s first", decoy)

for _, identifier := range []string{common.LegacyIdentifier(distribution, version), installed} {
execOK(ctx, t, ctr, fmt.Sprintf("mkdir --parents %s && printf 'java=%%s\\n' %s > %s/.sdkmanrc",
dir, shQuote(identifier), dir))
// The probe activates inside its own exec, a fresh shell session, so no
// `vfox env` state cached by an earlier call can answer for the file.
out := execRetry(ctx, t, ctr, ".sdkmanrc java="+identifier, fmt.Sprintf(
`cd %s && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && %s`, dir, showSettings))
common.CheckJava(t, out, distribution, version, arch, ".sdkmanrc java="+identifier)
t.Logf("PASS .sdkmanrc java=%s selects java %s, not %s", identifier, version, decoy)
}
execOK(ctx, t, ctr, "rm --recursive "+decoyDir)
}

// verifyToolchain runs java and javac through the activate hook and checks
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/windows/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func runOne(t *testing.T, ctx context.Context, image, platform, arch string, c c
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)
verifyLegacyFile(ctx, t, ctr, box, c.Distribution, version, installed, arch)

useJava(ctx, t, ctr, box, activation, installed)
verifyToolchain(ctx, t, ctr, box, activation, c.Distribution, version, installed, arch)
Expand Down
Loading
Loading