diff --git a/docs/e2e.md b/docs/e2e.md index a579e57..2aa5a97 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -88,8 +88,14 @@ JDK, so `java` can only come from what vfox installs: (`21.0.80+1-tem`): vfox sorts installed versions as strings, descending, so `vfox list java` must show it first. `ParseLegacyFile` used to take it as the first one starting with the release, and, for the padded release, - as the newest release of the major. On Windows the file is written with - CRLF line endings. + as the newest release of the major. Then SDKMAN's identifier of an + early-access build of the pinned major, spelled as `sdk list java` shows + OpenJDK's (`28.0.0.0+ea.20-open`), must select that build: + `java=21.0.0.0+ea.1-tem` selects `21-ea+1-tem`, not the release, which + `ParseLegacyFile` took while it understood only SDKMAN's older + `21.ea.1-tem`. jdk.java.net removes each early-access build within 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 `java.version`, the runner's `os.arch` (`amd64` / `aarch64`, which also catches an x64 JDK emulated on Windows ARM64) and the distribution's diff --git a/hooks/parse_legacy_file.lua b/hooks/parse_legacy_file.lua index 3cdf5c0..90abd4f 100644 --- a/hooks/parse_legacy_file.lua +++ b/hooks/parse_legacy_file.lua @@ -21,6 +21,9 @@ local installed_version = require("installed_version") --- reads. SDKMAN calls JetBrains jbr (the plugin's jb) and puts fx in the --- version of JavaFX builds: 21.0.12.fx-zulu, or 21.0.12-fx+1.1-librca in its --- semverish identifiers (the plugin's 21.0.12-zulu-fx, 21.0.12+1.1-librca-fx). +--- The semverish ones spell early-access builds 28.0.0+ea.19-open, padded or +--- not (28.0.0.0+ea.19-open), where SDKMAN used to write the plugin's +--- 28.ea.19-open (Foojay's 28-ea+19). --- Any other spelling, the plugin's own included, is returned unchanged. local function plugin_spelling(identifier) local version, distribution = identifier:match("^(.+)%-([^-]+)$") @@ -30,11 +33,16 @@ local function plugin_spelling(identifier) if distribution == "jbr" then distribution = "jb" end + local suffix = "-" .. distribution local release, build = version:match("^(%d[%d.]*)[.-]fx(.*)$") if release then - return release .. build .. "-" .. distribution .. "-fx" + version, suffix = release .. build, suffix .. "-fx" end - return version .. "-" .. distribution + local major, ea_build = version:match("^(%d+)[.0]*%+ea%.(%d+)$") + if major then + version = major .. ".ea." .. ea_build + end + return version .. suffix end function PLUGIN:ParseLegacyFile(ctx) diff --git a/tests/e2e/common/common.go b/tests/e2e/common/common.go index 9b2d29d..d8be46b 100644 --- a/tests/e2e/common/common.go +++ b/tests/e2e/common/common.go @@ -308,6 +308,29 @@ func PaddedIdentifier(distribution, version, installed string) string { return version + buildNumber.FindString(installed) + "-" + sdkmanName(distribution) } +// EarlyAccess renders the installed name of an early-access build of the +// pinned major (-ea+1, e.g. 21-ea+1-tem, 25-ea+1 for OpenJDK). +// jdk.java.net removes each early-access build within weeks, so the suite +// fakes the install: a java- directory whose bin holds a stand-in +// java that only prints this name. +func EarlyAccess(distribution, version string) string { + return Spec(distribution, Major(version)+"-ea+1") +} + +// EarlyAccessIdentifier renders SDKMAN's identifier of that build, spelled +// as `sdk list java` shows OpenJDK's (28.0.0.0+ea.20-open): +// ".0.0.0[-fx]+ea.1-" (21.0.0.0+ea.1-tem, +// 25.0.0.0-fx+ea.1-librca). ParseLegacyFile matched only SDKMAN's older +// 21.ea.1-tem to it, so it took the newest release of the major instead +// (vfox-java#86). +func EarlyAccessIdentifier(distribution, version string) string { + version = Major(version) + ".0.0.0" + if JavaFX(distribution) { + version += "-fx" + } + return version + "+ea.1-" + sdkmanName(distribution) +} + // Alias renders the asdf-style spelling "-[-fx]" of an // installed version, which PreUse must rewrite back to installed // (temurin-21.0.8+9 to 21.0.8+9-tem, openjdk-25.0.2+10 to 25.0.2+10). diff --git a/tests/e2e/common/common_test.go b/tests/e2e/common/common_test.go index 04f34b3..f40f476 100644 --- a/tests/e2e/common/common_test.go +++ b/tests/e2e/common/common_test.go @@ -222,6 +222,23 @@ func TestPaddedIdentifier(t *testing.T) { } } +func TestEarlyAccess(t *testing.T) { + for _, c := range []struct{ dist, version, installed, identifier string }{ + {"open", "25.0.2", "25-ea+1", "25.0.0.0+ea.1-open"}, + {"tem", "21.0.8", "21-ea+1-tem", "21.0.0.0+ea.1-tem"}, + {"librca-fx", "25.0.4", "25-ea+1-librca-fx", "25.0.0.0-fx+ea.1-librca"}, + {"jb", "21.0.5", "21-ea+1-jb", "21.0.0.0+ea.1-jbr"}, + {"graalce", "25.0.2", "25-ea+1-graalce", "25.0.0.0+ea.1-graalce"}, + } { + if got := EarlyAccess(c.dist, c.version); got != c.installed { + t.Errorf("EarlyAccess(%s, %s) = %q, want %q", c.dist, c.version, got, c.installed) + } + if got := EarlyAccessIdentifier(c.dist, c.version); got != c.identifier { + t.Errorf("EarlyAccessIdentifier(%s, %s) = %q, want %q", c.dist, c.version, got, c.identifier) + } + } +} + func TestAlias(t *testing.T) { for _, c := range []struct{ dist, installed, want string }{ {"open", "25.0.2+10", "openjdk-25.0.2+10"}, diff --git a/tests/e2e/linux/verify.go b/tests/e2e/linux/verify.go index bb93a6d..11c4826 100644 --- a/tests/e2e/linux/verify.go +++ b/tests/e2e/linux/verify.go @@ -56,11 +56,12 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, distribut // java=25.0.4.fx-librca), the one with a zero padding the release (e.g. // java=21.0.8.0+9-tem, see common.PaddedIdentifier) 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. +// another release first (see common.Decoy). SDKMAN's identifier of an +// early-access build (e.g. java=21.0.0.0+ea.1-tem) must then select a faked +// install of that build (see common.EarlyAccess), not the release. 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, installed, arch string) { t.Helper() - const dir = "/tmp/e2e-legacy-sdkmanrc" // vfox counts a version as installed once its directory holds // java-; the decoy holds nothing more, so selecting it leaves // no java on PATH. @@ -75,16 +76,37 @@ func verifyLegacyFile(ctx context.Context, t *testing.T, ctr tc.Container, distr common.PaddedIdentifier(distribution, version, installed), 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)) + out := legacyJava(ctx, t, ctr, identifier) 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) + + // The early-access build's java only prints its name, which the + // release's java, selected instead, would not. + ea := common.EarlyAccess(distribution, version) + eaDir := `"${VFOX_HOME:-$HOME/.vfox}"/cache/java/` + shQuote("v-"+ea) + bin := eaDir + "/" + shQuote("java-"+ea) + "/bin" + execOK(ctx, t, ctr, fmt.Sprintf("mkdir --parents %s && printf '%%s' %s > %s/java && chmod +x %s/java", + bin, shQuote("#!/bin/sh\necho "+shQuote(ea)+"\n"), bin, bin)) + identifier := common.EarlyAccessIdentifier(distribution, version) + out := legacyJava(ctx, t, ctr, identifier) + require.Containsf(t, out, ea, ".sdkmanrc java=%s must select %s, not %s", identifier, ea, installed) + t.Logf("PASS .sdkmanrc java=%s selects %s, not %s", identifier, ea, installed) + execOK(ctx, t, ctr, "rm --recursive "+eaDir) +} + +// legacyJava writes java= to a .sdkmanrc and returns what java +// prints next to it. 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. +func legacyJava(ctx context.Context, t *testing.T, ctr tc.Container, identifier string) string { + t.Helper() + const dir = "/tmp/e2e-legacy-sdkmanrc" + execOK(ctx, t, ctr, fmt.Sprintf("mkdir --parents %s && printf 'java=%%s\\n' %s > %s/.sdkmanrc", + dir, shQuote(identifier), dir)) + return execRetry(ctx, t, ctr, ".sdkmanrc java="+identifier, fmt.Sprintf( + `cd %s && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && %s`, dir, showSettings)) } // verifyToolchain runs java and javac through the activate hook and checks diff --git a/tests/e2e/windows/verify.go b/tests/e2e/windows/verify.go index 00394c1..7860287 100644 --- a/tests/e2e/windows/verify.go +++ b/tests/e2e/windows/verify.go @@ -59,11 +59,12 @@ func verifySearch(ctx context.Context, t *testing.T, ctr tc.Container, box slotB // java=21.0.8.0+9-tem, see common.PaddedIdentifier) or the installed version // itself (e.g. java=21.0.8+9-tem), written by Set-Content with Windows line // endings, 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. +// common.Decoy). SDKMAN's identifier of an early-access build (e.g. +// java=21.0.0.0+ea.1-tem) must then select a faked install of that build +// (see common.EarlyAccess), not the release. 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, box slotBox, distribution, version, installed, arch string) { t.Helper() - const dir = `C:\e2e-legacy-sdkmanrc` // vfox counts a version as installed once its directory holds // java-; the decoy holds nothing more, so selecting it leaves // no java on PATH. @@ -79,25 +80,46 @@ func verifyLegacyFile(ctx context.Context, t *testing.T, ctr tc.Container, box s common.PaddedIdentifier(distribution, version, installed), installed, } { - execOK(ctx, t, ctr, fmt.Sprintf( - "New-Item -ItemType Directory -Force -Path %s | Out-Null; Set-Content -Path %s -Value %s", - pwshQuote(dir), pwshQuote(dir+`\.sdkmanrc`), pwshQuote("java="+identifier)), box.vars...) - // The probe activates its own PowerShell process. Activation pins - // __VFOX_PID, which scopes the `vfox env` state cache, but Windows may - // reuse a just-exited PID and legacy files are not among the cache's - // tracked inputs, so the cache file is removed before the export. - script := fmt.Sprintf( - "Invoke-Expression -Command (vfox activate pwsh | Out-String); "+ - "Set-Location -LiteralPath %s; "+ - "if ($env:__VFOX_CURTMPPATH) { Remove-Item -LiteralPath (Join-Path $env:__VFOX_CURTMPPATH 'env-state.json') -Force -ErrorAction SilentlyContinue }; "+ - "$__vfoxExport = vfox env -s pwsh | Out-String; "+ - "if ($__vfoxExport.Trim()) { Invoke-Expression -Command $__vfoxExport }; "+ - "java %s", pwshQuote(dir), showSettings) - out := execRetry(ctx, t, ctr, ".sdkmanrc java="+identifier, script, box.vars...) + out := legacyJava(ctx, t, ctr, box, identifier) 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, fmt.Sprintf("Remove-Item -LiteralPath %s -Recurse -Force", decoyDir), box.vars...) + + // The early-access build's java (a java.cmd) only prints its name, which + // the release's java, selected instead, would not. + ea := common.EarlyAccess(distribution, version) + eaDir := fmt.Sprintf("(Join-Path $env:VFOX_HOME %s)", pwshQuote(`cache\java\v-`+ea)) + execOK(ctx, t, ctr, fmt.Sprintf( + "$bin = Join-Path %s %s; New-Item -ItemType Directory -Force -Path $bin | Out-Null; "+ + "Set-Content -Path (Join-Path $bin 'java.cmd') -Value %s", + eaDir, pwshQuote(`java-`+ea+`\bin`), pwshQuote("@echo "+ea)), box.vars...) + identifier := common.EarlyAccessIdentifier(distribution, version) + out := legacyJava(ctx, t, ctr, box, identifier) + require.Containsf(t, out, ea, ".sdkmanrc java=%s must select %s, not %s", identifier, ea, installed) + t.Logf("PASS .sdkmanrc java=%s selects %s, not %s", identifier, ea, installed) + execOK(ctx, t, ctr, fmt.Sprintf("Remove-Item -LiteralPath %s -Recurse -Force", eaDir), box.vars...) +} + +// legacyJava writes java= to a .sdkmanrc and returns what java +// prints next to it. The probe activates its own PowerShell process. +// Activation pins __VFOX_PID, which scopes the `vfox env` state cache, but +// Windows may reuse a just-exited PID and legacy files are not among the +// cache's tracked inputs, so the cache file is removed before the export. +func legacyJava(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, identifier string) string { + t.Helper() + const dir = `C:\e2e-legacy-sdkmanrc` + execOK(ctx, t, ctr, fmt.Sprintf( + "New-Item -ItemType Directory -Force -Path %s | Out-Null; Set-Content -Path %s -Value %s", + pwshQuote(dir), pwshQuote(dir+`\.sdkmanrc`), pwshQuote("java="+identifier)), box.vars...) + script := fmt.Sprintf( + "Invoke-Expression -Command (vfox activate pwsh | Out-String); "+ + "Set-Location -LiteralPath %s; "+ + "if ($env:__VFOX_CURTMPPATH) { Remove-Item -LiteralPath (Join-Path $env:__VFOX_CURTMPPATH 'env-state.json') -Force -ErrorAction SilentlyContinue }; "+ + "$__vfoxExport = vfox env -s pwsh | Out-String; "+ + "if ($__vfoxExport.Trim()) { Invoke-Expression -Command $__vfoxExport }; "+ + "java %s", pwshQuote(dir), showSettings) + return execRetry(ctx, t, ctr, ".sdkmanrc java="+identifier, script, box.vars...) } // verifyToolchain runs java and javac through the activate + env hooks and diff --git a/tests/parse_legacy_file_test.lua b/tests/parse_legacy_file_test.lua index 9403ce0..993d341 100644 --- a/tests/parse_legacy_file_test.lua +++ b/tests/parse_legacy_file_test.lua @@ -98,6 +98,20 @@ local cases = { { "java=25.0.1.0+8-open\n", { "25+36" }, "25+36" }, { "java=26.0.1-oracle\n", { "26-oracle" }, "26-oracle" }, { "java=26.0.2-oracle\n", { "26-oracle", "26.0.1-oracle" }, "26.0.1-oracle" }, + -- SDKMAN's semverish identifiers write early-access builds 28.0.0+ea.19, + -- padded or not, where it used to write 28.ea.19 (vfox-java#86). + { "java=28.ea.19-open\n", { "28-ea+19" }, "28-ea+19" }, + { "java=28.0.0+ea.19-open\n", { "28-ea+19" }, "28-ea+19" }, + { "java=28.0.0.0+ea.19-open\n", { "28-ea+18", "28-ea+19", "27+36" }, "28-ea+19" }, + { "java=26.0.0+ea.13-graal\n", { "26-ea+2-graal", "26-ea+13-graal" }, "26-ea+13-graal" }, + { "java=28.0.0.0+ea.19-jbr\n", { "28-ea+19-jb" }, "28-ea+19-jb" }, + { "java=28.0.0.0-fx+ea.19-zulu\n", { "28-ea+19-zulu", "28-ea+19-zulu-fx" }, "28-ea+19-zulu-fx" }, + -- A build not installed takes the newest one installed, as 28.ea.20 does. + { "java=28.ea.20-open\n", { "28-ea+18", "28-ea+19" }, "28-ea+19" }, + { "java=28.0.0.0+ea.20-open\n", { "28-ea+18", "28-ea+19" }, "28-ea+19" }, + -- Never a release for an early-access build, nor the other way round. + { "java=28.0.0.0+ea.19-open\n", { "28+36", "28.0.1+3" }, nil }, + { "java=28.0.0+36-open\n", { "28-ea+19" }, nil }, -- No java line at all selects nothing. { "maven=3.9.9\n", { "21.0.8+9-tem" }, nil }, }