diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 96f22bb..b1429a0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -17,14 +17,15 @@ # amd64/arm64 through Docker Desktop's Linux-container mode. It carries the # pinned Go toolchain the E2E suites and the local checks need, matching the # setup-go version CI uses, plus the Lua and license tooling pinned in the -# lint and license workflows. +# lint and license workflows, and the vfox release that runs the hook tests +# (vfox plugin test) in the test workflow. # # The suites themselves run testcontainers against the host daemon through # the socket mounted by devcontainer.json (Docker-out-of-Docker), so the # target containers are spawned exactly as a native CI runner spawns them. # -# The image is multi-arch (linux/amd64 and linux/arm64); the Lua downloads -# below select their asset per target arch. +# The image is multi-arch (linux/amd64 and linux/arm64); the Lua and vfox +# downloads below select their asset per target arch. FROM golang:1.27.1 @@ -34,9 +35,10 @@ FROM golang:1.27.1 COPY --from=docker:29-cli /usr/local/bin/docker /usr/local/bin/docker # Keep the tool versions below in step with .github/workflows/lint.yml -# (stylua-action's version and the `mise x aqua:...` line). +# (stylua-action's version and the `mise x aqua:...` line) and test.yml (vfox). ARG STYLUA_VERSION=2.5.2 ARG LUA_LANGUAGE_SERVER_VERSION=3.19.1 +ARG VFOX_VERSION=1.0.13 # Heredocs need BuildKit, which the dev container tooling uses by default. The # E2E target Dockerfiles stay on the chained-`;` form: testcontainers-go's @@ -53,8 +55,8 @@ apt-get install --assume-yes --no-install-recommends \ zip rm --recursive --force /var/lib/apt/lists/* case "$(dpkg --print-architecture)" in - amd64) stylua_asset=stylua-linux-x86_64.zip; luals_asset="lua-language-server-${LUA_LANGUAGE_SERVER_VERSION}-linux-x64.tar.gz" ;; - arm64) stylua_asset=stylua-linux-aarch64.zip; luals_asset="lua-language-server-${LUA_LANGUAGE_SERVER_VERSION}-linux-arm64.tar.gz" ;; + amd64) stylua_asset=stylua-linux-x86_64.zip; luals_asset="lua-language-server-${LUA_LANGUAGE_SERVER_VERSION}-linux-x64.tar.gz"; vfox_dir="vfox_${VFOX_VERSION}_linux_x86_64" ;; + arm64) stylua_asset=stylua-linux-aarch64.zip; luals_asset="lua-language-server-${LUA_LANGUAGE_SERVER_VERSION}-linux-arm64.tar.gz"; vfox_dir="vfox_${VFOX_VERSION}_linux_aarch64" ;; *) echo "FAIL unsupported architecture $(dpkg --print-architecture)" >&2; exit 1 ;; esac curl --fail --silent --show-error --location --output /tmp/stylua.zip \ @@ -67,8 +69,13 @@ mkdir --parents /opt/lua-language-server tar --extract --gzip --file /tmp/luals.tar.gz --directory /opt/lua-language-server rm --force /tmp/luals.tar.gz ln --symbolic /opt/lua-language-server/bin/lua-language-server /usr/local/bin/lua-language-server +curl --fail --silent --show-error --location --output /tmp/vfox.tar.gz \ + "https://github.com/version-fox/vfox/releases/download/v${VFOX_VERSION}/${vfox_dir}.tar.gz" +tar --extract --gzip --file /tmp/vfox.tar.gz --directory /usr/local/bin --strip-components=1 "${vfox_dir}/vfox" +rm --force /tmp/vfox.tar.gz stylua --version lua-language-server --version +vfox --version EOF # License-header tooling at the versions pinned by .github/workflows/license.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c3bbcaa..384ed21 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,8 +23,12 @@ permissions: contents: read concurrency: - group: e2e-${{ github.ref }} - cancel-in-progress: true + # A new push to a pull request replaces the run of the push before it. Every + # commit on main gets a group of its own instead: in one shared group a newer + # commit cancels the run of the older, and re-running the older cancels the + # newer in turn, so one of the two never ends with a result. + group: e2e-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # The Linux job runs the suite natively, the way the dev container runs it # locally: Go at the dev container's version (.devcontainer/Dockerfile) plus diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a64c92e..1ec1f43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,16 +34,32 @@ jobs: with: go-version: '1.24.0' cache: false - - name: Test Java version resolution with vfox's Lua interpreter + # vfox plugin test came with vfox 1.0.13. The release is pinned and its + # checksum checked, as the actions are pinned by their commits; change the + # two together to move to another release. + - name: Install vfox + env: + VFOX_VERSION: 1.0.13 + VFOX_SHA256: 7ad0a4c82c8173f09393e09abecc3e27468253bab5fe53ffbf58f2b12b3fc768 run: | - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_install_test.lua - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_use_test.lua - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/parse_legacy_file_test.lua - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/loongnix_test.lua - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/github_mirror_test.lua - go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/checksum_test.lua - - name: Test macOS Java home normalization - run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua + archive="$RUNNER_TEMP/vfox.tar.gz" + curl --fail --silent --show-error --location --output "$archive" \ + "https://github.com/version-fox/vfox/releases/download/v${VFOX_VERSION}/vfox_${VFOX_VERSION}_linux_x86_64.tar.gz" + echo "$VFOX_SHA256 $archive" | sha256sum --check + mkdir "$RUNNER_TEMP/vfox" + tar --extract --gzip --file "$archive" --directory "$RUNNER_TEMP/vfox" --strip-components=1 \ + "vfox_${VFOX_VERSION}_linux_x86_64/vfox" + echo "$RUNNER_TEMP/vfox" >> "$GITHUB_PATH" + # It finds tests/**/*_test.lua and runs each file in a VM of its own. A file + # that does not call test.load() from vfox.test fails, and so does a run + # that finds no test file. + - name: Run the offline hook tests with vfox + run: vfox plugin test . + # PostInstall's shell script runs for real here, on directories the test + # makes, and vfox plugin test disables os.execute. So this file is not a + # *_test.lua, and it runs under glua, as it does on macOS below. + - name: Test the macOS Java home layouts with vfox's Lua interpreter + run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua macos-layout: runs-on: macos-latest steps: @@ -55,4 +71,4 @@ jobs: go-version: '1.24.0' cache: false - name: Test actual macOS filesystem layouts - run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua + run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua diff --git a/README.md b/README.md index 53a9e30..341b172 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ Not every distribution has builds for arm64, Alpine or Windows on ARM. Only Temu Zulu, Liberica, Liberica NIK and Corretto (Java 8 only) have builds with JavaFX bundled. Add `-fx` after the distribution, as in `java@21-zulu-fx`. See [docs/javafx.md](docs/javafx.md) for details. +## Native Image + +A GraalVM installs and runs like any other JDK. Only `native-image`, which builds native executables, needs a C compiler and zlib's headers on top, and a minimal Ubuntu, such as its container image, has neither. Install them once, before the first native build: + +```bash +sudo apt install --assume-yes build-essential zlib1g-dev +``` + +GraalVM CE 8 to 19 come without `native-image` and need it added first. See [docs/distributions.md](docs/distributions.md#native-image-and-the-c-toolchain) for what `native-image` prints without these packages, for other systems, and for adding it. + ## `.sdkmanrc` vfox reads the `java=` line of SDKMAN's `.sdkmanrc` through this plugin and selects a JDK that is already installed. See [docs/sdkmanrc.md](docs/sdkmanrc.md) for details. @@ -92,7 +102,7 @@ To see which JDKs exist for your platform, ask Foojay yourself. [docs/foojay.md] ## Troubleshooting -See [docs/troubleshooting.md](docs/troubleshooting.md) for the messages the plugin prints, proxies and where the JDK is. +See [docs/troubleshooting.md](docs/troubleshooting.md) for the messages the plugin and `native-image` print, proxies and where the JDK is. ## Install from source diff --git a/README_CN.md b/README_CN.md index 5a7d4bf..af23067 100644 --- a/README_CN.md +++ b/README_CN.md @@ -78,6 +78,16 @@ java -version Zulu、Liberica、Liberica NIK 和 Corretto(只有 Java 8)提供内置 JavaFX 的构建,在发行版后面加 `-fx` 即可,比如 `java@21-zulu-fx`。详见 [docs/zh-CN/javafx.md](docs/zh-CN/javafx.md)。 +## Native Image + +GraalVM 的安装和使用与别的 JDK 没有两样。只有用来构建原生可执行文件的 `native-image` 还需要 C 编译器和 zlib 的头文件,而最小化的 Ubuntu(比如它的容器镜像)两样都没有。第一次构建原生镜像之前装一次: + +```bash +sudo apt install --assume-yes build-essential zlib1g-dev +``` + +GraalVM CE 8 到 19 不带 `native-image`,要先添加。没有这些包时 `native-image` 的报错、其他系统的做法以及怎样添加,见 [docs/zh-CN/distributions.md](docs/zh-CN/distributions.md#native-image-与-c-工具链)。 + ## `.sdkmanrc` vfox 通过这个插件读取 SDKMAN 的 `.sdkmanrc` 里的 `java=` 一行,并选中已经安装的 JDK。详见 [docs/zh-CN/sdkmanrc.md](docs/zh-CN/sdkmanrc.md)。 @@ -92,7 +102,7 @@ vfox 通过这个插件读取 SDKMAN 的 `.sdkmanrc` 里的 `java=` 一行,并 ## 问题排查 -插件的各种提示、代理设置和 JDK 的位置,见 [docs/zh-CN/troubleshooting.md](docs/zh-CN/troubleshooting.md)。 +插件和 `native-image` 的各种提示、代理设置和 JDK 的位置,见 [docs/zh-CN/troubleshooting.md](docs/zh-CN/troubleshooting.md)。 ## 从源码安装 diff --git a/docs/development.md b/docs/development.md index 7541eab..bf54c31 100644 --- a/docs/development.md +++ b/docs/development.md @@ -18,7 +18,8 @@ lib/foojay.lua the Foojay request, and fixes to what it answers lib/github_mirror.lua the mirror of the downloads from GitHub lib/installed_version.lua matching against installed names lib/loongnix.lua the Loongnix archive -tests/*_test.lua offline hook tests +tests/*_test.lua offline hook tests, run by vfox plugin test +tests/post_install_layouts.lua the macOS layouts PostInstall handles, with its real shell script tests/fixtures/loongnix/ a saved copy of the Loongnix archive listing tests/e2e/ container tests, see e2e.md Injection.lua type hints for what vfox injects, never loaded @@ -69,19 +70,26 @@ SDKMAN spells some things differently (`jbr`, `21.0.12.fx-zulu`, `21.0.12-fx+1.1 ## Tests -The hook tests run offline under `gopher-lua`, the Lua interpreter vfox embeds: +The hook tests run offline under `vfox plugin test`, which vfox 1.0.13 and later has. It finds every `tests/**/*_test.lua` and runs each file in a VM of its own, so a new test file needs no entry anywhere: ```bash -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_install_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_use_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/parse_legacy_file_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/loongnix_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/github_mirror_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/checksum_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua +vfox plugin test . +vfox plugin test . --file tests/pre_use_test.lua ``` -Run them from the repository root. All seven load `tests/support.lua`, which stubs `vfox.strings`, and `tests/pre_install_test.lua` and `tests/loongnix_test.lua` stub `http` and `json` themselves. `tests/pre_install_test.lua` keeps Foojay's answers in a table at the top, keyed by ` []`, with `windows` or `macos` in front for those systems. Add the answer a new case needs there, and note the day you observed it. Lint, license headers and the container tests are in [e2e.md](e2e.md#local-checks). +Run it from the repository root. The Test Plugin workflow installs the release pinned in `.github/workflows/test.yml`, and the dev container carries the same one. + +A test file calls `test.load()` from `vfox.test` once, and calls the real hooks through what it returns, as in `plugin:PreInstall({ version = "21-tem" })`. `test.load` takes the platform (`os`, `arch`), the environment variables (`env`) and a function that answers every HTTP request (`http`). The modules in `lib` can be required after it, and `RUNTIME.osType` and `RUNTIME.archType` can be changed to try another platform. See vfox's [Test and debug a plugin](https://vfox.dev/plugins/create/testing.html). The context and the result of a hook go through the decoding vfox does, so a hook that returns a bare string where vfox wants a table fails the test, and a string the result lacks comes back as `""`, not `nil`. + +The VM disables `os.execute` and `io.popen`, so `tests/pre_install_test.lua` answers the `ldd --version` that decides between glibc and musl itself. It keeps Foojay's answers in a table at the top, keyed by ` []`, with `windows` or `macos` in front for those systems, and its `http` function sends them as JSON. Add the answer a new case needs there, and note the day you observed it. + +`tests/post_install_layouts.lua` is the one test that is not a `*_test.lua`: it runs the shell script of `PostInstall` on directories it makes, which the VM does not allow. It runs under `gopher-lua`, the Lua interpreter vfox embeds: + +```bash +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua +``` + +Lint, license headers and the container tests are in [e2e.md](e2e.md#local-checks). To call a single hook against the real Foojay, vfox 1.0.13 and later has `vfox plugin run`. It does not register the plugin, and `--os` and `--arch` answer for another platform: diff --git a/docs/distributions.md b/docs/distributions.md index 6513d7b..bd379bc 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -171,6 +171,26 @@ GraalVM CE 8 to 19, Gluon GraalVM before 23, and Mandrel and Liberica NIK before - The plugin sets `JAVA_HOME` and `PATH`, and no `GRAALVM_HOME`. A `GRAALVM_HOME` that points at a GraalVM of your own stays as it is, and the native-image build plugins fall back to `JAVA_HOME` when it is unset. - There is no Windows ARM64 and no Alpine build, and Intel Macs get nothing after 25.0.1. See [platforms.md](platforms.md#graalvm). +### Native Image and the C toolchain + +A GraalVM installs and runs like any other JDK. After `vfox install java@25.0.2-graalce` and `vfox use`, `java` and `javac` work on a system with no compiler and no zlib headers. `sudo apt install --assume-yes build-essential zlib1g-dev` is for `native-image` alone, the tool that compiles a program into a native executable. It links the executable with the system's `gcc` against the C library and zlib, so it needs them, and so does whatever calls it, such as the Maven and Gradle plugins for Native Image. The plugin installs the JDK and nothing else. + +On Ubuntu 26.04, execute in Bash, + +```bash +sudo apt install --assume-yes build-essential zlib1g-dev +``` + +- `native-image` is in the `bin` directory of Oracle GraalVM, GraalVM Community, Liberica NIK, Mandrel and Gluon GraalVM. GraalVM CE 8 to 19 are the exception, see below. +- Without `gcc` the build stops within seconds with `Error: Default native-compiler executable 'gcc' not found via environment variable PATH`, and `zlib1g-dev` alone does not change that. +- With `gcc` but without `zlib1g-dev`, which is what `build-essential` alone gives, the build goes through the analysis and the compilation first, which took between 20 seconds and two minutes for a hello world, and fails when it links: `There was an error linking the native image: Linker command exited with 1`, GraalVM's hint `It appears as though libz:.a is missing. Please install it.`, and `cannot find -lz: No such file or directory`. +- `gcc`, `libc6-dev` and `zlib1g-dev` were enough to build a hello world, without `g++` and `make`. `build-essential` brings both, and is what GraalVM's documentation lists. +- Other systems need other packages. GraalVM's [prerequisites](https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites) list `sudo dnf install gcc glibc-devel zlib-devel libstdc++-static` for Fedora and the other systems that use dnf, `xcode-select --install` for macOS, and the Visual Studio Build Tools with the Windows SDK for Windows. On Fedora 44, `gcc`, `glibc-devel` and `zlib-devel` built a hello world without `libstdc++-static`. + +GraalVM CE 8 to 19 (`grl8`, `grl11`, `grl16`, `grl17`, `grl19`) come without `native-image`. Their `bin` directory holds `gu`, the GraalVM Updater, and `native-image` is `command not found` until `gu install native-image` has added it from GitHub. That worked for `22.3.3-grl11`, `21.2-grl16`, `22.3.3-grl17` and `22.3.1-grl19` on 2026-11-02, and the packages above are needed after it as before. It failed for `21.3.1-grl8` with `Internal error occurred: A JSONObject text must begin with '{'`, because the release index that this `gu` downloads, `https://oca.opensource.oracle.com/gds/meta-data.json`, is a web page now. For that release, download `native-image-installable-svm-java8-linux-amd64-21.3.1.jar` from the [`vm-21.3.1` release](https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.1) and run `gu -L install` on the file. + +Checked on 2026-11-02 with vfox 1.0.13 in Ubuntu 26.04 container images (x64) that had no compiler. GraalVM Community 25.0.2 and 25.4.4.1.1, Oracle GraalVM 25.0.5, Mandrel 25.0.5, Liberica NIK 25.0.4.1 and Gluon GraalVM 22.1.0.1 stopped as described without the packages, and with them built a hello world that ran. So did `22.3.3-grl17` after `gu install native-image`, and GraalVM Community 25.0.2 in a Fedora 44 image. + ## Others ### JetBrains Runtime (`jb`) diff --git a/docs/e2e.md b/docs/e2e.md index 0400e43..62184c7 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -2,7 +2,8 @@ [中文](zh-CN/e2e.md) | [English](e2e.md) -The offline Lua hook tests (`tests/*_test.lua`) run in the **Test Plugin** +The offline Lua hook tests (`tests/*_test.lua` under `vfox plugin test`, and +`tests/post_install_layouts.lua` under `glua`) run in the **Test Plugin** workflow. The end-to-end suite runs in containers against real vfox on Linux and Windows; each `vfox` x `distribution` combination gets its own throwaway container. CI runs the suite natively on the stock runners (Docker plus Go @@ -406,13 +407,8 @@ lua-language-server --check . --checklevel=Warning ```bash # Offline hook tests (.github/workflows/test.yml). -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_install_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_use_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/parse_legacy_file_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/loongnix_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/github_mirror_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/checksum_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua +vfox plugin test . +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua ``` ```bash diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9930a5e..892dfed 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -80,6 +80,8 @@ While `proxy.enable` is `false`, the usual `HTTP_PROXY`, `HTTPS_PROXY` and `NO_P - **A `GRAALVM_HOME` you set is left alone.** The plugin sets `JAVA_HOME` and `PATH` only. - **`vfox use java@21` selects `21.3.1-grl11`, which runs Java 11.** GraalVM CE 8 to 19, Gluon GraalVM before 23, and Mandrel and Liberica NIK before 25 carry GraalVM's version numbers, and vfox matches a bare major against those, see [distributions.md](distributions.md#versions-numbered-after-graalvm). Write `java@21-tem`. - **`java@21-mandrel` installs a Mandrel that runs Java 17.** Mandrel's and Liberica NIK's majors are GraalVM's: `21.3.6-mandrel` is the newest Mandrel 21.x. Their releases for Java 21 are 23.1.x, so write `java@23-mandrel`, see [distributions.md](distributions.md#behavior-of-mandrel-and-liberica-nik). +- **`native-image` stops with `Default native-compiler executable 'gcc' not found via environment variable PATH`, or at its last step with `cannot find -lz`.** The plugin installs the JDK and nothing else, and `native-image` needs the system's C toolchain and zlib. On Ubuntu, `sudo apt install --assume-yes build-essential zlib1g-dev` installs them, see [distributions.md](distributions.md#native-image-and-the-c-toolchain). +- **`native-image: command not found` with a GraalVM CE 8 to 19, such as `22.3.3-grl17`.** These come without `native-image`, and `gu install native-image` adds it. It fails for `21.3.1-grl8`, see [distributions.md](distributions.md#native-image-and-the-c-toolchain). ## Where the JDK is diff --git a/docs/zh-CN/development.md b/docs/zh-CN/development.md index c5c52c5..a618e64 100644 --- a/docs/zh-CN/development.md +++ b/docs/zh-CN/development.md @@ -18,7 +18,8 @@ lib/foojay.lua 向 Foojay 发出的请求,以及对返回结 lib/github_mirror.lua 从 GitHub 下载时用的镜像 lib/installed_version.lua 与已安装的版本名做匹配 lib/loongnix.lua Loongnix 归档 -tests/*_test.lua 离线的钩子测试 +tests/*_test.lua 离线的钩子测试,由 vfox plugin test 运行 +tests/post_install_layouts.lua PostInstall 处理的 macOS 目录结构,会真实执行它的 shell 脚本 tests/fixtures/loongnix/ Loongnix 归档目录页的一份快照 tests/e2e/ 容器测试,见 e2e.md Injection.lua vfox 注入对象的类型提示,不会被加载 @@ -69,19 +70,26 @@ SDKMAN 有些写法不同(`jbr`、`21.0.12.fx-zulu`、`21.0.12-fx+1.1-librca` ## 测试 -钩子测试在离线状态下用 `gopher-lua` 运行,它就是 vfox 内嵌的 Lua 解释器: +钩子测试在离线状态下用 `vfox plugin test` 运行,vfox 1.0.13 及以后版本才有这个命令。它会找出所有 `tests/**/*_test.lua`,每个文件在自己的虚拟机里运行,所以新增一个测试文件不需要在别处登记: ```bash -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_install_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_use_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/parse_legacy_file_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/loongnix_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/github_mirror_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/checksum_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua +vfox plugin test . +vfox plugin test . --file tests/pre_use_test.lua ``` -请在仓库根目录运行。七个文件都会加载 `tests/support.lua`,它模拟 `vfox.strings`;`tests/pre_install_test.lua` 和 `tests/loongnix_test.lua` 还自己模拟了 `http` 和 `json`。`tests/pre_install_test.lua` 把 Foojay 的返回结果放在文件开头的一张表里,键是 `<发行版> [<版本>]`,Windows 和 macOS 的前面再分别加 `windows` 和 `macos`。新增用例需要的返回结果就加在那里,并注明观察到的日期。Lint、许可证头和容器测试见 [e2e.md](e2e.md#本地检查)。 +请在仓库根目录运行。Test Plugin 工作流安装的是 `.github/workflows/test.yml` 里固定的那个版本,开发容器里装的也是它。 + +测试文件调用一次 `vfox.test` 的 `test.load()`,再通过它返回的对象调用真实的钩子,比如 `plugin:PreInstall({ version = "21-tem" })`。`test.load` 接受平台(`os`、`arch`)、环境变量(`env`)和一个回答所有 HTTP 请求的函数(`http`)。`lib` 里的模块要在它之后才能 `require`,修改 `RUNTIME.osType` 和 `RUNTIME.archType` 可以换一个平台来试。详见 vfox 的[测试与调试插件](https://vfox.dev/zh-hans/plugins/create/testing.html)。钩子的上下文和返回值会经过 vfox 自己的编解码,所以钩子在 vfox 要表的地方返回一个裸字符串,测试就会失败;返回值里缺的字符串会变成 `""`,而不是 `nil`。 + +测试用的虚拟机默认禁用 `os.execute` 和 `io.popen`,所以 `tests/pre_install_test.lua` 自己回答判断 glibc 还是 musl 的 `ldd --version`。它把 Foojay 的返回结果放在文件开头的一张表里,键是 `<发行版> [<版本>]`,Windows 和 macOS 的前面再分别加 `windows` 和 `macos`,它的 `http` 函数把这些结果以 JSON 发出去。新增用例需要的返回结果就加在那里,并注明观察到的日期。 + +`tests/post_install_layouts.lua` 是唯一不叫 `*_test.lua` 的测试:它在自己创建的目录上运行 `PostInstall` 的 shell 脚本,而测试虚拟机不允许这样做。它用 `gopher-lua` 运行,也就是 vfox 内嵌的 Lua 解释器: + +```bash +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua +``` + +Lint、许可证头和容器测试见 [e2e.md](e2e.md#本地检查)。 想对着真实的 Foojay 调用单个钩子,可以用 vfox 1.0.13 及以后版本的 `vfox plugin run`。它不会注册插件,`--os` 和 `--arch` 可以模拟其他平台: diff --git a/docs/zh-CN/distributions.md b/docs/zh-CN/distributions.md index 72ae3f5..4d40763 100644 --- a/docs/zh-CN/distributions.md +++ b/docs/zh-CN/distributions.md @@ -171,6 +171,26 @@ GraalVM CE 8 到 19、23 以前的 Gluon GraalVM,以及 25 以前的 Mandrel - 插件只设置 `JAVA_HOME` 和 `PATH`,不设置 `GRAALVM_HOME`。你设置的、指向自己的 GraalVM 的 `GRAALVM_HOME` 保持原样;没有设置时,native-image 的构建插件会回退到 `JAVA_HOME`。 - 没有 Windows ARM64 和 Alpine 的构建,Intel Mac 上 25.0.1 之后的版本也没有。见 [platforms.md](platforms.md#graalvm)。 +### Native Image 与 C 工具链 + +GraalVM 的安装和使用与别的 JDK 没有两样:`vfox install java@25.0.2-graalce` 和 `vfox use` 之后,`java` 和 `javac` 在没有编译器、也没有 zlib 头文件的系统上照常工作。`sudo apt install --assume-yes build-essential zlib1g-dev` 只为 `native-image` 而装,它是把程序编译成原生可执行文件的工具。它用系统的 `gcc` 把可执行文件与 C 库和 zlib 链接起来,所以需要它们;调用它的程序同样需要,比如 Native Image 的 Maven 和 Gradle 插件。插件只安装 JDK,不装别的。 + +在 Ubuntu 26.04 的 Bash 中执行: + +```bash +sudo apt install --assume-yes build-essential zlib1g-dev +``` + +- `native-image` 在 Oracle GraalVM、GraalVM 社区版、Liberica NIK、Mandrel 和 Gluon GraalVM 的 `bin` 目录里。GraalVM CE 8 到 19 是例外,见下文。 +- 没有 `gcc` 时,构建几秒内就停下,报 `Error: Default native-compiler executable 'gcc' not found via environment variable PATH`,只装 `zlib1g-dev` 也没用。 +- 有 `gcc` 却没有 `zlib1g-dev` 时(只装了 `build-essential` 也是这种情况),构建会先走完分析和编译(hello world 用了 20 秒到两分钟),到链接时才失败:`There was an error linking the native image: Linker command exited with 1`,GraalVM 自己的提示 `It appears as though libz:.a is missing. Please install it.`,以及 `cannot find -lz: No such file or directory`。 +- 构建一个 hello world,`gcc`、`libc6-dev` 和 `zlib1g-dev` 就够了,用不着 `g++` 和 `make`。`build-essential` 把这两个也装上,GraalVM 的文档列出的正是它。 +- 其他系统需要的包不一样。GraalVM 的[前置条件](https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites)里,Fedora 和其他使用 dnf 的系统是 `sudo dnf install gcc glibc-devel zlib-devel libstdc++-static`,macOS 是 `xcode-select --install`,Windows 是带 Windows SDK 的 Visual Studio Build Tools。在 Fedora 44 上,只装 `gcc`、`glibc-devel` 和 `zlib-devel`、不装 `libstdc++-static`,也构建出了 hello world。 + +GraalVM CE 8 到 19(`grl8`、`grl11`、`grl16`、`grl17`、`grl19`)不带 `native-image`。它们的 `bin` 目录里只有 GraalVM 更新器 `gu`,`native-image` 会报 `command not found`,要先用 `gu install native-image` 从 GitHub 添加。2026-11-02 这在 `22.3.3-grl11`、`21.2-grl16`、`22.3.3-grl17` 和 `22.3.1-grl19` 上成功了,添加之后仍然需要上面的包。`21.3.1-grl8` 上失败了,报 `Internal error occurred: A JSONObject text must begin with '{'`:这个 `gu` 下载的版本索引 `https://oca.opensource.oracle.com/gds/meta-data.json` 现在返回的是一个网页。对这个版本,请从 [`vm-21.3.1` 发布页](https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.1)下载 `native-image-installable-svm-java8-linux-amd64-21.3.1.jar`,再对这个文件执行 `gu -L install`。 + +2026-11-02 用 vfox 1.0.13 在没有编译器的 Ubuntu 26.04 容器镜像(x64)里检查过:GraalVM 社区版 25.0.2 和 25.4.4.1.1、Oracle GraalVM 25.0.5、Mandrel 25.0.5、Liberica NIK 25.0.4.1 和 Gluon GraalVM 22.1.0.1,没有这些包时都如上所述停下,装上之后都构建出了能运行的 hello world。`22.3.3-grl17` 在执行 `gu install native-image` 之后也是如此,GraalVM 社区版 25.0.2 在 Fedora 44 的镜像里也一样。 + ## 其他 ### JetBrains Runtime(`jb`) diff --git a/docs/zh-CN/e2e.md b/docs/zh-CN/e2e.md index af13191..d083eca 100644 --- a/docs/zh-CN/e2e.md +++ b/docs/zh-CN/e2e.md @@ -2,7 +2,7 @@ [中文](e2e.md) | [English](../e2e.md) -离线的 Lua 钩子测试(`tests/*_test.lua`)在 **Test Plugin** 工作流里运行。端到端套件在容器里针对真实的 vfox 运行,覆盖 Linux 和 Windows;每个 `vfox` x `distribution` 组合都有自己的一次性容器。CI 直接在标准 runner 上运行这套测试(Docker 加 `actions/setup-go` 提供的 Go)。本地则由仓库的开发容器在 Linux 上复现这一环境;Windows 上改用 Docker Compose runner 镜像,因为开发容器无法承载 Windows 容器镜像(见下文)。 +离线的 Lua 钩子测试(`tests/*_test.lua` 由 `vfox plugin test` 运行,`tests/post_install_layouts.lua` 由 `glua` 运行)在 **Test Plugin** 工作流里运行。端到端套件在容器里针对真实的 vfox 运行,覆盖 Linux 和 Windows;每个 `vfox` x `distribution` 组合都有自己的一次性容器。CI 直接在标准 runner 上运行这套测试(Docker 加 `actions/setup-go` 提供的 Go)。本地则由仓库的开发容器在 Linux 上复现这一环境;Windows 上改用 Docker Compose runner 镜像,因为开发容器无法承载 Windows 容器镜像(见下文)。 ## 开发容器 @@ -195,13 +195,8 @@ lua-language-server --check . --checklevel=Warning ```bash # 离线的钩子测试(.github/workflows/test.yml)。 -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_install_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/pre_use_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/parse_legacy_file_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/loongnix_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/github_mirror_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/checksum_test.lua -go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_test.lua +vfox plugin test . +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua ``` ```bash diff --git a/docs/zh-CN/troubleshooting.md b/docs/zh-CN/troubleshooting.md index 0b539e9..9d35440 100644 --- a/docs/zh-CN/troubleshooting.md +++ b/docs/zh-CN/troubleshooting.md @@ -80,6 +80,8 @@ vfox config proxy.url http://127.0.0.1:7890 - **你设置的 `GRAALVM_HOME` 不会被改动。** 插件只设置 `JAVA_HOME` 和 `PATH`。 - **`vfox use java@21` 选中了 `21.3.1-grl11`,而它运行的是 Java 11。** GraalVM CE 8 到 19、23 以前的 Gluon GraalVM,以及 25 以前的 Mandrel 和 Liberica NIK,用的都是 GraalVM 的版本号;对只写大版本号的写法,vfox 只按版本号匹配,不分发行版,见 [distributions.md](distributions.md#按-graalvm-编号的版本)。请写成 `java@21-tem`。 - **`java@21-mandrel` 装上的 Mandrel 运行的是 Java 17。** Mandrel 和 Liberica NIK 的大版本号是 GraalVM 的:`21.3.6-mandrel` 是最新的 Mandrel 21.x。它们为 Java 21 发布的是 23.1.x,所以要写 `java@23-mandrel`,见 [distributions.md](distributions.md#mandrel-与-liberica-nik-的行为)。 +- **`native-image` 停在 `Default native-compiler executable 'gcc' not found via environment variable PATH`,或者在最后一步报 `cannot find -lz`。** 插件只安装 JDK,而 `native-image` 需要系统里的 C 工具链和 zlib。Ubuntu 上执行 `sudo apt install --assume-yes build-essential zlib1g-dev` 即可装上,见 [distributions.md](distributions.md#native-image-与-c-工具链)。 +- **GraalVM CE 8 到 19(比如 `22.3.3-grl17`)里 `native-image: command not found`。** 这些版本不带 `native-image`,用 `gu install native-image` 添加。`21.3.1-grl8` 上这条命令会失败,见 [distributions.md](distributions.md#native-image-与-c-工具链)。 ## JDK 在哪里 diff --git a/tests/checksum_test.lua b/tests/checksum_test.lua index 7040c2e..8978213 100644 --- a/tests/checksum_test.lua +++ b/tests/checksum_test.lua @@ -12,7 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") +local test = require("vfox.test") +test.load() local checksum = require("checksum") local cases = 0 diff --git a/tests/github_mirror_test.lua b/tests/github_mirror_test.lua index c384991..6dbc86d 100644 --- a/tests/github_mirror_test.lua +++ b/tests/github_mirror_test.lua @@ -12,7 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") +local test = require("vfox.test") +test.load() local github_mirror = require("github_mirror") -- The mirror comes from the environment: os.getenv answers for it from this diff --git a/tests/loongnix_test.lua b/tests/loongnix_test.lua index ffca722..b79b9d3 100644 --- a/tests/loongnix_test.lua +++ b/tests/loongnix_test.lua @@ -12,57 +12,47 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") +local test = require("vfox.test") local calls = {} local state = {} -package.preload.http = function() - return { - get = function(request) - table.insert(calls, request.url) - assert(request.url:match("^https://ftp%.loongnix%.cn/Java/"), "unexpected provider: " .. request.url) - if state.failure then - return state.failure.response, state.failure.err - end - local path = request.url:gsub("https://ftp%.loongnix%.cn/Java/", "") - if path == "" then - path = "index" - else - path = path:gsub("/$", "") - end - local f = assert(io.open("tests/fixtures/loongnix/" .. path .. ".html")) - local body = f:read("*a") - f:close() - return { status_code = 200, body = body }, nil - end, - } -end -package.preload.json = function() - return { - decode = function() - error("Foojay must not be called") - end, - } -end -RUNTIME = { osType = "linux", archType = "loong64" } -PLUGIN = {} -dofile("hooks/available.lua") -dofile("hooks/pre_install.lua") -dofile("hooks/pre_use.lua") -local versions = PLUGIN:Available({ args = {} }) +-- Loongnix is the only provider: a request to Foojay, or to anything else, fails +-- the call that made it. +local plugin = test.load({ + os = "linux", + arch = "loong64", + http = function(request) + table.insert(calls, request.url) + assert(request.url:match("^https://ftp%.loongnix%.cn/Java/"), "unexpected provider: " .. request.url) + if state.failure then + return state.failure.response, state.failure.err + end + local path = request.url:gsub("https://ftp%.loongnix%.cn/Java/", "") + if path == "" then + path = "index" + else + path = path:gsub("/$", "") + end + local f = assert(io.open(RUNTIME.pluginDirPath .. "/tests/fixtures/loongnix/" .. path .. ".html")) + local body = f:read("*a") + f:close() + return { status_code = 200, body = body } + end, +}) +local versions = plugin:Available({ args = {} }) assert(#versions > 0, "default loong64 search must return versions") local found = {} for _, item in ipairs(versions) do assert(item.version:match("%-lnxabi[12]"), item.version) assert(not found[item.version], "duplicate version " .. item.version) found[item.version] = true - local result = PLUGIN:PreUse({ version = item.version }) + local result = plugin:PreUse({ version = item.version }) assert(result and result.version == item.version, "PreUse changed ABI or JavaFX suffix") end assert(found["21.0.12+8-lnxabi1-fx"]) assert(found["21.0.12+8-lnxabi2-fx"]) assert(found["8.0.502+7-lnxabi2-fx"]) local function check(version, expected, tail) - local result = PLUGIN:PreInstall({ version = version }) + local result = plugin:PreInstall({ version = version }) assert(result.version == expected, result.version) assert(result.url:sub(-#tail) == tail, result.url) end @@ -77,48 +67,48 @@ local function rejects(fn, message) assert(not ok and tostring(err):find(message, 1, true), tostring(err)) end rejects(function() - PLUGIN:PreInstall({ version = "999-lnxabi2-fx" }) + plugin:PreInstall({ version = "999-lnxabi2-fx" }) end, "No Loongnix JDK") rejects(function() - PLUGIN:PreInstall({ version = "21" }) + plugin:PreInstall({ version = "21" }) end, "ABI") rejects(function() - PLUGIN:PreInstall({ version = "lnxabi2-fx" }) + plugin:PreInstall({ version = "lnxabi2-fx" }) end, "No version in lnxabi2-fx; add one, such as 21-lnxabi2-fx.") rejects(function() - PLUGIN:Available({ args = { "tem" } }) + plugin:Available({ args = { "tem" } }) end, "Foojay") for _, abi in ipairs({ "lnxabi1", "lnxabi2" }) do - for _, item in ipairs(PLUGIN:Available({ args = { abi } })) do + for _, item in ipairs(plugin:Available({ args = { abi } })) do assert(item.version:find("-" .. abi, 1, true)) end end -assert(#PLUGIN:Available({ args = { "all" } }) == #versions) +assert(#plugin:Available({ args = { "all" } }) == #versions) RUNTIME.archType = "arm64" rejects(function() - PLUGIN:Available({ args = { "loongnix" } }) + plugin:Available({ args = { "loongnix" } }) end, "linux/loong64") RUNTIME.archType = "loong64" RUNTIME.osType = "darwin" rejects(function() - PLUGIN:PreInstall({ version = "21-lnxabi2-fx" }) + plugin:PreInstall({ version = "21-lnxabi2-fx" }) end, "linux/loong64") RUNTIME.osType = "linux" state.failure = { err = "network unavailable" } rejects(function() - PLUGIN:Available({ args = {} }) + plugin:Available({ args = {} }) end, "network unavailable") state.failure = { response = { status_code = 503, body = "unavailable" } } rejects(function() - PLUGIN:Available({ args = {} }) + plugin:Available({ args = {} }) end, "503") state.failure = { response = { status_code = 200, body = "unexpected upstream page" } } rejects(function() - PLUGIN:Available({ args = {} }) + plugin:Available({ args = {} }) end, "No Loongnix JDK") state.failure = nil rejects(function() - PLUGIN:PreInstall({ version = "21.0.1-lnxabi2-fx" }) + plugin:PreInstall({ version = "21.0.1-lnxabi2-fx" }) end, "No Loongnix JDK") RUNTIME.archType = "arm64" local foojay_calls = {} @@ -127,8 +117,8 @@ require("foojay").fetchtJdkList = function(distribution, version) table.insert(foojay_calls, distribution) return { { java_version = "21.0.1", term_of_support = "lts" } } end -assert(PLUGIN:Available({ args = {} })[1].version == "21.0.1") -assert(PLUGIN:Available({ args = { "tem" } })[1].version == "21.0.1-tem") -assert(#PLUGIN:Available({ args = { "all" } }) > 0) +assert(plugin:Available({ args = {} })[1].version == "21.0.1") +assert(plugin:Available({ args = { "tem" } })[1].version == "21.0.1-tem") +assert(#plugin:Available({ args = { "all" } }) > 0) assert(#foojay_calls > 2) print("PASS: Loongnix hook regression tests (" .. #versions .. " fixture versions)") diff --git a/tests/parse_legacy_file_test.lua b/tests/parse_legacy_file_test.lua index 9987c9e..fe7ca29 100644 --- a/tests/parse_legacy_file_test.lua +++ b/tests/parse_legacy_file_test.lua @@ -12,9 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") -PLUGIN = {} -dofile("hooks/parse_legacy_file.lua") +local test = require("vfox.test") +local plugin = test.load() local function parse(content, installed) local path = os.tmpname() @@ -27,20 +26,19 @@ local function parse(content, installed) return a > b end) local ok, result = pcall(function() - return PLUGIN:ParseLegacyFile({ + -- The hook gets getInstalledVersions as vfox binds it, a Go + -- func() []string that rejects any argument, including the ctx a + -- ctx:getInstalledVersions() call adds. + return plugin:ParseLegacyFile({ filepath = path, filename = ".sdkmanrc", - -- vfox 1.x binds this as a Go func() []string and rejects any - -- argument, including the ctx a ctx:getInstalledVersions() call adds. - getInstalledVersions = function(...) - assert(select("#", ...) == 0, "getInstalledVersions takes no arguments") - return installed - end, + installedVersions = installed, }) end) os.remove(path) assert(ok, result) - return result.version + -- vfox decodes the result into a Go string, so no version comes back empty. + return result.version ~= "" and result.version or nil end -- The JDKs installed in vfox-java#71. diff --git a/tests/post_install_test.lua b/tests/post_install_layouts.lua similarity index 90% rename from tests/post_install_test.lua rename to tests/post_install_layouts.lua index 63be26e..736d49d 100644 --- a/tests/post_install_test.lua +++ b/tests/post_install_layouts.lua @@ -12,7 +12,11 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") +-- This file is not named *_test.lua on purpose. It runs the shell script of +-- PostInstall for real, on directories it makes with mkdir, chmod and ln, and +-- `vfox plugin test` disables os.execute. It runs under glua instead, in the +-- Test Plugin workflow on Ubuntu and on macOS, whose BSD tools the script is for: +-- go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/post_install_layouts.lua PLUGIN = {} RUNTIME = { osType = "darwin" } dofile("hooks/post_install.lua") diff --git a/tests/pre_install_test.lua b/tests/pre_install_test.lua index 9a69c58..ec98f77 100644 --- a/tests/pre_install_test.lua +++ b/tests/pre_install_test.lua @@ -12,7 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") +local test = require("vfox.test") local JDKS = "https://api.foojay.io/disco/v3.0/packages/jdks?" local IDS = "https://api.foojay.io/disco/v3.0/ids/" local GITHUB = "https://github.com/graalvm/graalvm-ce-builds/releases/download/" @@ -291,7 +291,6 @@ for distribution, packages in pairs(own_numbers) do end local requests = {} -local responses = {} -- What Foojay's record of a package says besides the download, by id: the -- fields a case sets override those of the stub, which gives the download only -- (see the checksum cases). @@ -300,89 +299,86 @@ local records = {} -- file that is not there, or { error = "..." } for a request that fails. Any -- other address is not expected. local files = {} -package.preload.http = function() - return { - get = function(request) - table.insert(requests, request.url) - local file = files[request.url] - if type(file) == "table" then - return nil, file.error - elseif file == false then - return { status_code = 404, body = "Not Found" }, nil - elseif file ~= nil then - return { status_code = 200, body = file }, nil +-- Answers as Foojay and the sites of the checksum files do. Foojay's answers +-- are JSON, which the plugin reads with json.decode. +local function answer(request) + table.insert(requests, request.url) + local file = files[request.url] + if type(file) == "table" then + return nil, file.error + elseif file == false then + return { status_code = 404, body = "Not Found" } + elseif file ~= nil then + return { status_code = 200, body = file } + end + 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 + -- Foojay decodes the query string, reading a bare + as a space. + value = value:gsub("%+", " "):gsub("%%(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end) + params[key] = value + if key == "archive_type" then + archive_types[value] = true end - 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 - -- Foojay decodes the query string, reading a bare + as a space. - value = value:gsub("%+", " "):gsub("%%(%x%x)", function(hex) - return string.char(tonumber(hex, 16)) - end) - params[key] = value - if key == "archive_type" then - archive_types[value] = true - end - end - -- A version stops at a space: 21.0.5 63130 matches every 21.0.5 build. - local version = params.version:match("^[^ ]*") - local key = params.distribution - if version ~= "" then - key = key .. " " .. version - end - if params.operating_system == "windows" or params.operating_system == "macos" then - key = params.operating_system .. " " .. key - end - local packages = foojay[key] or {} - -- Only the packages of the archive types asked for are listed, - -- each a copy, as a decoded answer is. - local result = {} - for _, package in ipairs(packages) do - if archive_types[package.archive_type] then - local copy = {} - for field, value in pairs(package) do - copy[field] = value - end - table.insert(result, copy) - end - end - body = { result = result } - else - assert(request.url:sub(1, #IDS) == IDS, "unexpected request: " .. request.url) - local id = request.url:sub(#IDS + 1) - -- The downloads of GraalVM Community that graal() gives the - -- host github are on GitHub, where the id puts the file after - -- it. All others are somewhere else. - local site = id:sub(1, 7) == "github/" and GITHUB or "https://download.invalid/" - local record = { direct_download_uri = site .. id } - for field, value in pairs(records[id] or {}) do - record[field] = value - end - body = { result = { record } } + end + -- A version stops at a space: 21.0.5 63130 matches every 21.0.5 build. + local version = params.version:match("^[^ ]*") + local key = params.distribution + if version ~= "" then + key = key .. " " .. version + end + if params.operating_system == "windows" or params.operating_system == "macos" then + key = params.operating_system .. " " .. key + end + local packages = foojay[key] or {} + -- Only the packages of the archive types asked for are listed. + local result = {} + for _, package in ipairs(packages) do + if archive_types[package.archive_type] then + table.insert(result, package) end - table.insert(responses, body) - return { status_code = 200, body = tostring(#responses) }, nil - end, - } + end + body = { result = result } + else + assert(request.url:sub(1, #IDS) == IDS, "unexpected request: " .. request.url) + local id = request.url:sub(#IDS + 1) + -- The downloads of GraalVM Community that graal() gives the + -- host github are on GitHub, where the id puts the file after + -- it. All others are somewhere else. + local site = id:sub(1, 7) == "github/" and GITHUB or "https://download.invalid/" + local record = { direct_download_uri = site .. id } + for field, value in pairs(records[id] or {}) do + record[field] = value + end + body = { result = { record } } + end + return { status_code = 200, body = require("json").encode(body) } end -package.preload.json = function() + +-- The libc of the machine is found with ldd (lib/foojay.lua), and the test VM +-- disables io.popen. Answer as glibc's ldd does, so that the lookups do not +-- depend on the machine the tests run on. +io.popen = function(command) + assert(command == "ldd --version 2>&1", "unexpected command: " .. command) return { - decode = function(body) - return responses[tonumber(body)] + read = function() + return "ldd (Ubuntu GLIBC 2.43-0ubuntu1) 2.43\n" + end, + close = function() + return true end, } end -RUNTIME = { osType = "linux", archType = "amd64" } -PLUGIN = {} -dofile("hooks/available.lua") -dofile("hooks/pre_install.lua") +local plugin = test.load({ os = "linux", arch = "amd64", http = answer }) -- What searching asks Foojay for the distribution of version. local function searched(version) requests = {} - PLUGIN:Available({ args = { require("distribution_version").parse_version(version).distribution.short_name } }) + plugin:Available({ args = { require("distribution_version").parse_version(version).distribution.short_name } }) return requests[1] end @@ -394,7 +390,7 @@ local cases = 0 local function check(version, expected, bare, file_end) local search = searched(version) requests = {} - local result = PLUGIN:PreInstall({ version = version }) + local result = plugin:PreInstall({ version = version }) assert(result.version == expected, version .. ": expected " .. expected .. ", got " .. tostring(result.version)) if file_end then assert( @@ -424,7 +420,7 @@ end local function check_graalvm(version, expected, filename) local search = searched(version) requests = {} - local result = PLUGIN:PreInstall({ version = version }) + local result = plugin:PreInstall({ version = version }) assert(result.version == expected, version .. ": expected " .. expected .. ", got " .. tostring(result.version)) assert( result.url:sub(-#filename - 1) == "/" .. filename, @@ -501,7 +497,7 @@ os.getenv = function(name) return getenv(name) end local function download(version) - return PLUGIN:PreInstall({ version = version }).url + return plugin:PreInstall({ version = version }).url end local github = GITHUB .. "github/graalvm-community-jdk-25i4-25.0.4.1.1_linux-x64_bin.tar.gz" local oracle = "https://download.invalid/gds/graalvm-community-jdk-25i4-25.0.4.1.1_linux-x64_bin.tar.gz" @@ -611,7 +607,7 @@ local function install(version) print = function(...) table.insert(printed, table.concat({ ... }, " ")) end - local result = PLUGIN:PreInstall({ version = version }) + local result = plugin:PreInstall({ version = version }) print = real_print fetched = {} for _, url in ipairs(requests) do @@ -624,6 +620,12 @@ end local ALGORITHMS = { "sha256", "sha512", "sha1", "md5" } +-- The digest the result carries under the name of an algorithm. vfox decodes the +-- result into Go strings, so one that is not there comes back empty. +local function digest_of(result, key) + return result[key] ~= "" and result[key] or nil +end + -- Installs version and checks that the result carries digest under the name of -- algorithm and under no other (nil for no checksum), that nothing is printed, -- and that the files fetched are the ones given. @@ -632,8 +634,8 @@ local function check_checksum(version, algorithm, digest, asked) for _, key in ipairs(ALGORITHMS) do local want = key == algorithm and digest or nil assert( - result[key] == want, - ("%s: %s expected %s, got %s"):format(version, key, tostring(want), tostring(result[key])) + digest_of(result, key) == want, + ("%s: %s expected %s, got %s"):format(version, key, tostring(want), tostring(digest_of(result, key))) ) end assert(#printed == 0, version .. ": printed " .. table.concat(printed, "; ")) @@ -703,13 +705,22 @@ local OPENJDK_SHA = OPENJDK_URL .. ".sha256" local function check_unverified(version, reason, asked) local result = install(version) for _, key in ipairs(ALGORITHMS) do - assert(result[key] == nil, ("%s: %s expected none, got %s"):format(version, key, tostring(result[key]))) + assert( + digest_of(result, key) == nil, + ("%s: %s expected none, got %s"):format(version, key, tostring(result[key])) + ) end assert(result.version == "25.0.2+10" and result.url == OPENJDK_URL, version .. ": the install changed") - assert( - #printed == 1 and printed[1] == "Cannot verify the download: " .. reason, - version .. ": printed " .. table.concat(printed, "; ") - ) + -- The reason is the whole text, or, as { start, ending }, only its two ends: + -- the middle is what Go and the test runner add around a failed request. + local line, head = printed[1] or "", "Cannot verify the download: " + local told + if type(reason) == "table" then + told = line:sub(1, #head + #reason[1]) == head .. reason[1] and line:sub(-#reason[2]) == reason[2] + else + told = line == head .. reason + end + assert(#printed == 1 and told, version .. ": printed " .. table.concat(printed, "; ")) assert( table.concat(fetched, " ") == table.concat(asked, " "), version .. ": fetched " .. table.concat(fetched, " ") .. ", expected " .. table.concat(asked, " ") @@ -718,10 +729,10 @@ local function check_unverified(version, reason, asked) end files[OPENJDK_SHA] = false check_unverified("25.0.2-open", "failed to fetch " .. OPENJDK_SHA .. ": status_code => 404", { OPENJDK_SHA }) -files[OPENJDK_SHA] = { error = 'Get "' .. OPENJDK_SHA .. '": context deadline exceeded' } +files[OPENJDK_SHA] = { error = "context deadline exceeded" } check_unverified( "25.0.2-open", - "failed to fetch " .. OPENJDK_SHA .. ': Get "' .. OPENJDK_SHA .. '": context deadline exceeded', + { "failed to fetch " .. OPENJDK_SHA .. ": ", "context deadline exceeded" }, { OPENJDK_SHA } ) files[OPENJDK_SHA] = "Blocked by the firewall" @@ -752,14 +763,14 @@ check("openjdk-25.0.2", "25.0.2+10", false) -- Searching lists every release, not only the newest one. requests = {} -PLUGIN:Available({ args = { "tem" } }) +plugin:Available({ args = { "tem" } }) assert(#requests == 1 and not requests[1]:find("latest=", 1, true), "Available: unexpected lookup " .. requests[1]) -- Searching lists OpenJDK versions without a suffix, the way PreInstall names -- them, so vfox marks them installed whether the query is open, openjdk or all. local function listed(query) local versions = {} - for _, item in ipairs(PLUGIN:Available({ args = { query } })) do + for _, item in ipairs(plugin:Available({ args = { query } })) do versions[item.version] = item.note end return versions @@ -811,7 +822,7 @@ for short, want in pairs({ }) do for _, query in ipairs({ short, "all" }) do local entries = {} - for _, item in ipairs(PLUGIN:Available({ args = { query } })) do + for _, item in ipairs(plugin:Available({ args = { query } })) do if item.version:sub(-#short - 1) == "-" .. short and item.version:match("^25") then table.insert(entries, item.version .. (item.note == "" and "" or " (" .. item.note .. ")")) end @@ -821,7 +832,7 @@ for short, want in pairs({ end end local jb = {} -for _, item in ipairs(PLUGIN:Available({ args = { "jb" } })) do +for _, item in ipairs(plugin:Available({ args = { "jb" } })) do table.insert(jb, item.version) end assert( @@ -845,20 +856,20 @@ for _, other in ipairs(others) do end -- Only these distributions ask for another archive type besides tar.gz. requests = {} -PLUGIN:PreInstall({ version = "21-rdht" }) +plugin:PreInstall({ version = "21-rdht" }) assert(requests[1]:find("&archive_type=tar.gz&archive_type=tar.xz&", 1, true), "21-rdht: " .. requests[1]) requests = {} -PLUGIN:PreInstall({ version = "21-tem" }) +plugin:PreInstall({ version = "21-tem" }) assert(requests[1]:find("&archive_type=tar.gz&operating_system=", 1, true), "21-tem: " .. requests[1]) -- Gluon GraalVM's 22.x releases install their JDK 17 build, which they share a -- version with the JDK 11 one, listed once by searching. check("22-gln", "22.1.0.1-gln", true, "/graalvm-svm-java17-linux-gluon-22.1.0.1-Final.tar.gz") check("21-gln", "21.2-gln", true, "/graalvm-svm-linux-gluon-21.2.0-dev.zip") requests = {} -local gluon = PLUGIN:PreInstall({ version = "22.1.0.1-gln" }) +local gluon = plugin:PreInstall({ version = "22.1.0.1-gln" }) assert(gluon.url:find("/graalvm-svm-java17-", 1, true) and #requests == 2, "22.1.0.1-gln: " .. gluon.url) local gln = {} -for _, item in ipairs(PLUGIN:Available({ args = { "gln" } })) do +for _, item in ipairs(plugin:Available({ args = { "gln" } })) do table.insert(gln, item.version) end assert( @@ -875,7 +886,7 @@ RUNTIME.osType = "linux" local function check_own_number(version, expected, file_end) local search = searched(version) requests = {} - local result = PLUGIN:PreInstall({ version = version }) + local result = plugin:PreInstall({ version = version }) assert(result.version == expected, version .. ": expected " .. expected .. ", got " .. tostring(result.version)) assert(result.url:sub(-#file_end) == file_end, version .. ": expected ..." .. file_end .. ", got " .. result.url) local lookups = {} @@ -929,7 +940,7 @@ for short, want in pairs({ }) do for _, query in ipairs({ short, "all" }) do local entries = {} - for _, item in ipairs(PLUGIN:Available({ args = { query } })) do + for _, item in ipairs(plugin:Available({ args = { query } })) do local entry = item.version .. (item.note == "" and "" or " (" .. item.note .. ")") if item.version:sub(-#short - 1) == "-" .. short then table.insert(entries, entry) @@ -946,8 +957,7 @@ assert(listed("nik")["23.1.8-nik-fx"] == "LTS, JavaFX", "Available nik: 23.1.8-n -- A distribution without a version, alone or after an empty one, asks for a -- version before any lookup (vfox-java#98). Alone it failed with a Lua error; -- the empty version reached Foojay, which listed every release, and the newest --- one installed. Once pcall has caught an error, gopher-lua's http stub keeps --- the requests table it saw, so it is emptied in place. +-- one installed. for _, case in ipairs({ { "tem", @@ -959,10 +969,8 @@ for _, case in ipairs({ { "zulu-fx", "such as 21-zulu-fx. Please check available versions with 'vfox search java zulu'" }, { "openjdk", "such as 21-open. Please check available versions with 'vfox search java open'" }, }) do - for i = #requests, 1, -1 do - requests[i] = nil - end - local ok, err = pcall(PLUGIN.PreInstall, PLUGIN, { version = case[1] }) + requests = {} + local ok, err = pcall(plugin.PreInstall, plugin, { version = case[1] }) assert(not ok and tostring(err):find(case[2], 1, true), case[1] .. ": unexpected " .. tostring(err)) assert(#requests == 0, case[1] .. ": unexpected lookup " .. tostring(requests[1])) end @@ -971,10 +979,8 @@ end -- names, which has no build in its name: there is a Mandrel 23.1.3.1 but no -- 23.1.3, and Liberica NIK 23.1.8 is named 23.1.8-nik. for _, version in ipairs({ "23.1.3-mandrel", "23.1.8+1-nik", "26-mandrel" }) do - for i = #requests, 1, -1 do - requests[i] = nil - end - local ok, err = pcall(PLUGIN.PreInstall, PLUGIN, { version = version }) + requests = {} + local ok, err = pcall(plugin.PreInstall, plugin, { version = version }) local want = "No JDK found for " .. version .. " on linux/amd64" assert(not ok and tostring(err):find(want, 1, true), version .. ": unexpected " .. tostring(err)) assert(#requests == 1, version .. ": expected one lookup, got " .. #requests) @@ -983,7 +989,7 @@ end -- A mirror that vfox would not download from stops a download from GitHub, with -- a message that names the variable, and leaves the others alone. mirror.value = "gh-proxy.org" -local ok, err = pcall(PLUGIN.PreInstall, PLUGIN, { version = "25.4.4.1-graalce" }) +local ok, err = pcall(plugin.PreInstall, plugin, { version = "25.4.4.1-graalce" }) assert( not ok and tostring(err):find("VFOX_JAVA_GITHUB_MIRROR must start with http:// or https://", 1, true), "a mirror without a scheme: unexpected " .. tostring(err) diff --git a/tests/pre_use_test.lua b/tests/pre_use_test.lua index 7fd58ff..c7434e4 100644 --- a/tests/pre_use_test.lua +++ b/tests/pre_use_test.lua @@ -12,9 +12,8 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -dofile("tests/support.lua") -PLUGIN = {} -dofile("hooks/pre_use.lua") +local test = require("vfox.test") +local plugin = test.load() -- Installed versions as PreInstall names them, keyed the way vfox passes them. local function sdks(versions) @@ -143,7 +142,7 @@ local cases = { { "semeru_certified-25.0.4", "25.0.4-semcrt" }, } for _, case in ipairs(cases) do - local result = PLUGIN:PreUse({ version = case[1], installedSdks = installed }) + local result = plugin:PreUse({ version = case[1], installedSdks = installed }) -- vfox decodes the result as a table; releases after 1.0.12 reject a bare string. assert(result == nil or type(result) == "table", case[1] .. ": expected a table or nil, got " .. type(result)) local version = result and result.version @@ -182,7 +181,7 @@ local graalvm_cases = { { { "23.1.8-nik", "23.0.1-nik" }, "23-nik", "23.1.8-nik" }, } for _, case in ipairs(graalvm_cases) do - local result = PLUGIN:PreUse({ version = case[2], installedSdks = sdks(case[1]) }) + local result = plugin:PreUse({ version = case[2], installedSdks = sdks(case[1]) }) local version = result and result.version local message = table.concat(case[1], ", ") .. " with " .. case[2] assert(version == case[3], message .. ": expected " .. tostring(case[3]) .. ", got " .. tostring(version)) @@ -193,7 +192,7 @@ end -- after an empty version, vfox reported -tem not installed. local rejected = { "tem", "temurin", "-tem", "temurin-", "zulu-fx", "open", "lnxabi2-fx" } for _, version in ipairs(rejected) do - local ok, err = pcall(PLUGIN.PreUse, PLUGIN, { version = version, installedSdks = installed }) + local ok, err = pcall(plugin.PreUse, plugin, { version = version, installedSdks = installed }) local message = "No version in " .. version .. "; add one" assert(not ok and tostring(err):find(message, 1, true), version .. ": unexpected " .. tostring(err)) end diff --git a/tests/support.lua b/tests/support.lua deleted file mode 100644 index ab79f24..0000000 --- a/tests/support.lua +++ /dev/null @@ -1,39 +0,0 @@ --- 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. - -package.path = "./lib/?.lua;" .. package.path -package.preload["vfox.strings"] = function() - local function has_prefix(value, prefix) - return value:sub(1, #prefix) == prefix - end - return { - split = function(value, separator) - local result = {} - local start = 1 - while true do - local position = value:find(separator, start, true) - if not position then - table.insert(result, value:sub(start)) - return result - end - table.insert(result, value:sub(start, position - 1)) - start = position + #separator - end - end, - has_prefix = has_prefix, - trim_prefix = function(value, prefix) - return has_prefix(value, prefix) and value:sub(#prefix + 1) or value - end, - } -end