Skip to content

Improve native-image executable discovery with explicit/convention launcher distinction - #845

Open
vpelikh wants to merge 4 commits into
graalvm:masterfrom
vpelikh:Set-default-java-launcher-from-java-toolchain
Open

Improve native-image executable discovery with explicit/convention launcher distinction#845
vpelikh wants to merge 4 commits into
graalvm:masterfrom
vpelikh:Set-default-java-launcher-from-java-toolchain

Conversation

@vpelikh

@vpelikh vpelikh commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR makes native-image executable discovery safer and more predictable by distinguishing an explicitly configured launcher from one selected by convention, so the plugin always builds against a launcher that actually provides native-image, and fails with a clear message when it cannot.

Before this PR

  • Toolchain detection (toolchainDetection = true) used the toolchain-resolved JDK as the launcher without checking whether that JDK contained native-image, leading to confusing failures.
  • When the explicit javaLauncher lacked native-image, the build failed silently or with an opaque downstream error.
  • There was no clear, documented rule for how an explicitly chosen launcher relates to the GRAALVM_HOME / JAVA_HOME fallback chain.

After this PR

Explicit javaLauncher — treated as authoritative.

  • If the user explicitly sets graalvmNative.binaries.<name>.javaLauncher and that launcher does not contain bin/native-image, the build fails immediately with a message naming the launcher and its installation path. No fallback, no gu install.

Convention launcher (toolchain detection) — used only when it can supply Native Image.

  • When toolchainDetection = true, toolchain detection is used as the javaLauncher convention, so graalvmNative.binaries.main.javaLauncher stays queryable.
  • At build time the plugin checks whether the resolved launcher actually contains bin/native-image. If it does, that launcher is used.
  • If it does not, the plugin transparently falls back to the environment chain (GRAALVM_HOMEJAVA_HOME → the Gradle JVM java-home), optionally installing native-image via gu there. A convention-selected launcher never blocks a working GraalVM in the environment. §FS-native-invocation.1.5
  • gu install native-image runs only on this environment-fallback path, never against a user-configured or toolchain installation (a convention launcher is never mutated in place).
  • If no source yields native-image, the build fails with a diagnostic that lists the probed paths and the selected location source.

Net effect: the plugin finds a usable native-image whenever one exists on the machine, fails fast with actionable messages when it does not, and always respects an explicit choice over automation.

Spec documentation

  • FS-native-invocation.1 (Executable discovery) documented in native-gradle-plugin/docs/functional/native-image-invocation.md:
    • §1.1: Explicit java launcher
    • §1.2: Convention-selected launcher
    • §1.3: Environment-variable fallback (GRAALVM_HOME → JAVA_HOME → Gradle JVM)
    • §1.4: gu-based installation
    • §1.5: Failure messages
    • §1.6: Toolchain detection interaction

Test coverage

  • NativeImageExecutableLocatorTest (unit tests):

    • Explicit launcher with missing native-imageGradleException with diagnostic message
    • Convention launcher without native-image → falls back to GRAALVM_HOME
  • ToolchainDiscoveryTest (functional tests, src/functionalTest):

    • explicit javaLauncher overrides toolchain (§1.1) — explicit launcher used, not GRAALVM_HOME
    • toolchain takes precedence over GRAALVM_HOME env var when running nativeCompile (§1.2)
    • disabling toolchainDetection uses GRAALVM_HOME fallback (§1.3)
    • native-image found in alternative GraalVM home when GRAALVM_HOME has no native-image and no gu (§1.3 cross-home fallback) — when GRAALVM_HOME lacks native-image and has no gu, the locator probes JAVA_HOME and resolves native-image from there
    • gu installs native-image when not found (§1.4)
    • gu installation failure falls back to error message (§1.5)
    • convention launcher provides native-image when no explicit launcher set (§1.2) — toolchain convention used when user sets no explicit launcher
    • compatibility mode detects and uses convention fallback launcher (§1.2/§1.6) — toolchain-detection active + convention fallback message on both runs
    • changing JAVA_HOME re-runs nativeCompile when native-image comes from JAVA_HOME fallback (§1.3) — env fallback candidates are task inputs: switching JAVA_HOME forces re-run, not UP-TO-DATE
    • unchanged environment keeps nativeCompile UP-TO-DATE (§1.3) — control: proves the JAVA_HOME swap is what forces the re-run

Fixes #542

@oracle-contributor-agreement

Copy link
Copy Markdown

Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
The following contributors of this PR have not signed the OCA:

To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application.

When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated.

If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public.

@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. label Mar 8, 2026
@oracle-contributor-agreement

Copy link
Copy Markdown

Thank you for signing the OCA.

@oracle-contributor-agreement oracle-contributor-agreement Bot added OCA Verified All contributors have signed the Oracle Contributor Agreement. and removed OCA Required At least one contributor does not have an approved Oracle Contributor Agreement. labels Mar 16, 2026
@vpelikh
vpelikh force-pushed the Set-default-java-launcher-from-java-toolchain branch from f6c2d68 to 048b68d Compare March 19, 2026 17:32
@vpelikh

vpelikh commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Hey @jormundur00, @hadryyassine, @vjovanov. Can someone check this?
I'm not sure about the test though. If anyone has an idea for improvement, please let me know.

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, and sorry for the late review.

I think we should distinguish explicit native-image launcher selection from a launcher selected by convention.

With the current change, enabling toolchain detection makes the native binary inherit the Java plugin toolchain first. If that Java toolchain is a normal JDK without bin/native-image, the locator will fail there and will not try GRAALVM_HOME/JAVA_HOME, because env fallback is only entered when detection is disabled or no launcher is present.

I do not think we should silently fall back when the user explicitly configured graalvmNative.binaries.<name>.javaLauncher; that should fail with a clear message if it does not contain native-image.

But when the launcher is selected by convention, either from JavaVersion.current() or from the Java plugin toolchain, fallback should remain available if that launcher does not supply native-image. Otherwise a normal Java compilation toolchain can accidentally block a valid GraalVM in GRAALVM_HOME/JAVA_HOME.

Could we structure the fix so issue #542 is handled by adding the current Gradle JVM home as a fallback, and so convention-selected toolchains only win when they actually contain native-image? That would preserve explicit user intent while keeping the existing environment fallback behavior.

Separately, I would adjust the new functional test. It hard-codes JavaLanguageVersion.of(25), so it only works on machines where Gradle can discover/provision a JDK 25. The existing Gradle functional-test setup pins the test launcher/toolchain to Java 17 and existing tests generally use JavaVersion.current() or environment-provided GraalVM paths. Could we avoid the fixed 25, either by comparing the native plugin launcher to the Java plugin toolchain provider directly, adding an explicit availability assumption, or covering the fallback behavior directly instead?

@vpelikh

vpelikh commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Hi @jormundur00, thanks for the careful review! I've addressed your concerns in the latest commit:

  1. Launcher selection order: The locator now checks the toolchain for native-image first, returns null if not found (triggers env var fallback), but only chooses the toolchain if it actually contains the executable. This preserves explicit user intent while maintaining fallback behavior.
  2. Test stability: Updated the test to use JavaVersion.current().majorVersion instead of hardcoding Java 25, matching the test project's configured launcher.

Let me know if you'd like any further changes!

@vpelikh vpelikh changed the title Set default java launcher from java toolchain Add safety check for toolchain launcher detection Jun 2, 2026
@vpelikh
vpelikh requested a review from jormundur00 June 2, 2026 17:20

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @vpelikh, thanks for the timely response and updates.

I've done a second round of reviews and pointed out more inconsistencies/things that need to be changed. Please take another look when you have the time.

@vpelikh vpelikh left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jormundur00, thanks again for review. Now PR has become much more polished.

@vpelikh
vpelikh requested a review from jormundur00 June 4, 2026 09:09
@vpelikh
vpelikh marked this pull request as draft June 4, 2026 09:50
@vpelikh

vpelikh commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Oh, I see failed tests. Will address them soon.

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. I think this is moving in the right direction for the toolchainDetection = true path, but as implemented I don't think it fully address the original issue as reported. The original reproducer relies on the JDK running Gradle being used by default when JAVA_HOME and GRAALVM_HOME are unset; this PR still requires either toolchainDetection = true or an explicit javaLauncher.

I'm on the fence whether we should open a separate issue (regarding toolchain problems) and link it to this PR (and then I'd try to address the original reporters issue), or just leave it as it is.

@jormundur00

Copy link
Copy Markdown
Member

Thanks for the updates. I think this is moving in the right direction for the toolchainDetection = true path, but as implemented I don't think it fully address the original issue as reported. The original reproducer relies on the JDK running Gradle being used by default when JAVA_HOME and GRAALVM_HOME are unset; this PR still requires either toolchainDetection = true or an explicit javaLauncher.

I'm on the fence whether we should open a separate issue (regarding toolchain problems) and link it to this PR (and then I'd try to address the original reporters issue), or just leave it as it is.

On second look, I think you should open a separate issue for this toolchain detection mismatch, and link this PR (and @Issue tagged tests) to it. I'll look into solving the other problem (when toolchains aren't used) in a followup. I suppose this PR can still mention the other issue, but I would not close it with the merge of this PR.

@vpelikh
vpelikh force-pushed the Set-default-java-launcher-from-java-toolchain branch from 52a5092 to 2c06f19 Compare June 4, 2026 15:13
vpelikh added a commit to vpelikh/native-build-tools that referenced this pull request Jun 4, 2026
…nDiscoveryTest

Signed-off-by: Vasily Pelikh <2010720+vpelikh@users.noreply.github.com>
@vpelikh

vpelikh commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00, you mentioned opening a new issue to discuss the toolchain detection logic. Just to be clear – should that new issue be:

  1. For the remaining work – i.e., removing the need for explicit toolchainDetection = true so that auto-detection works with zero config (completing Gradle plugin nativeCompile fails when JAVA_HOME and GRAALVM_HOME are not set #542), OR
  2. A separate issue for Improve native-image executable discovery with explicit/convention launcher distinction #845 itself – tracking some change or improvement specific to how this PR was implemented?

I want to make sure I open the right type of issue. Thanks!

@jormundur00

Copy link
Copy Markdown
Member

@jormundur00, you mentioned opening a new issue to discuss the toolchain detection logic. Just to be clear – should that new issue be:

  1. For the remaining work – i.e., removing the need for explicit toolchainDetection = true so that auto-detection works with zero config (completing Gradle plugin nativeCompile fails when JAVA_HOME and GRAALVM_HOME are not set #542), OR
  2. A separate issue for Add safety check for toolchain launcher detection #845 itself – tracking some change or improvement specific to how this PR was implemented?

I want to make sure I open the right type of issue. Thanks!

I was thinking more of the latter (point 2.), but let's wait till Monday for opening this issue, as I'd like to discuss this PR with my team on a triaging meet on Monday.

@vpelikh
vpelikh marked this pull request as ready for review June 5, 2026 06:59
@vpelikh
vpelikh requested a review from jormundur00 June 5, 2026 06:59
@vpelikh

vpelikh commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00, while we wait, can you approve the workflow? I want to see if the tests in CI have been fixed.

@vpelikh

vpelikh commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @jormundur00. In this PR I have added the ToolchainDiscoveryTest.

The test passes successfully on my macOS, but it fails on the Linux CI (as shown in the logs). I would like to reproduce the failure locally in a Linux environment to debug it, but I'm not sure how to properly set up and run the functional tests in such a setup.

Could you please share some guidance?

  1. What is the standard way to run functional tests in a Linux environment that mimics the CI?

    • Which environment variables are required (e.g., GRAALVM_HOME, JAVA_HOME, JAVA_HOME_17_X64)?
    • Are there any specific Gradle properties (like common.repo.url) that must be set?
  2. Do you have a Docker‑based setup or a script that replicates the GitHub Actions runner for this project?
    I tried something like this but encountered DefaultDecoratedConvention errors (likely due to missing build‑logic setup):

   docker run --rm --platform linux/amd64 -v "$PWD":/workspace -w /workspace gradle:9.0.0-jdk21 \
     bash -c "apt update && apt install -y openjdk-17-jdk && \
              export JAVA_HOME=/opt/java/openjdk && \
              export JAVA_HOME_17_X64=/usr/lib/jvm/java-17-openjdk-amd64 && \
              export GRAALVM_HOME=/opt/java/openjdk && \
              ./gradlew :native-gradle-plugin:functionalTest -DgradleVersion=9.0.0 --tests ToolchainDiscoveryTest"

Any hints on what I might be missing?

  1. Any other tips for debugging this test locally on Linux?

I want to ensure my changes are correct and that the test will pass once the CI runs. Thank you very much for your help!

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed response. We did a separate round of reviews after the latest updates and found the additional issues below.

Please rebase onto the latest master before the next update, so the PR is evaluated against the current CI environment and JDK-25 setup.

@vpelikh, replying to the Linux/Docker question: we do not maintain a supported Docker image or script that faithfully mirrors a GitHub-hosted runner, so I would not spend more time trying to reproduce the runner exactly. The closest useful local setup is Linux with Gradle running on the workflow's matrix JDK and a separate GraalVM JDK 25 configured for Native Image:

./gradlew :native-gradle-plugin:functionalTest \
  -DgradleVersion=9.0.0 \
  --tests org.graalvm.buildtools.gradle.ToolchainDiscoveryTest \
  --no-daemon --info

JAVA_HOME_17_X64 and common.repo.url should not be necessary for this test. If you do not have access to Linux locally, that is fine: once you push the rebase and test adjustment, I can run the CI for you; the functional-test workflow runs on Linux by default.

@jormundur00

Copy link
Copy Markdown
Member

We recently added the grund specification system to the repository. This PR changes executable-discovery behavior, so please make the change compatible with the latest master and specify the behavior first.

Please update native-gradle-plugin/docs/functional/native-image-invocation.md, specifically §gradle/FS-native-invocation.1, to define this selection order:

  1. An explicitly configured graalvmNative.binaries.<name>.javaLauncher is authoritative. If its bin/native-image is absent, fail with a diagnostic naming that launcher; do not silently select another installation.
  2. A convention-selected launcher (toolchain detection or Gradle JVM) is used only when it contains bin/native-image.
  3. If no convention-selected launcher supplies Native Image, fall back to GRAALVM_HOME, then JAVA_HOME, then any existing path fallback.
  4. If automatic gu install native-image remains in scope, specify which installation it may modify, when it is attempted, and its failure behavior. It must not weaken the explicit-launcher rule.

The implementation should preserve whether a launcher is explicit or convention-selected, so environment fallback applies only to the convention case. Please add coverage for: explicit missing launcher fails; convention launcher missing Native Image falls back to GRAALVM_HOME; and a valid toolchain wins over the environment fallback.

Please cite §gradle/FS-native-invocation.1 from the affected source, connect the functional tests to that behavior, and run grund check.

If the specification work is troublesome, let me know—I can prepare it in a separate follow-up PR.

@vpelikh

vpelikh commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00 @vjovanov. The failing checks fail on the same pre-test setup task:

> Task :graalvm-reachability-metadata:fetchMetadataRepository FAILED
Execution failed for task ':graalvm-reachability-metadata:fetchMetadataRepository'
> Could not download repository metadata from
  https://github.com/oracle/graalvm-reachability-metadata/releases/download/1.0.10/graalvm-reachability-metadata-1.0.10.zip

This looks like a transient network issue on the runners (the download from GitHub fails), unrelated to this PR. A re-run of the failed jobs should resolve it.

@vpelikh

vpelikh commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Hey @jormundur00, could you please do a final review as @vjovanov (and me :)) suggested? Also, could you please restart the CI tests? Some of them failed last time due to a flaky network error. Thanks!

@jormundur00

Copy link
Copy Markdown
Member

Hey @vpelikh, I'm currently (and the previous week) on vacation and don't have the necessary tooling to provide a proper review. I'll take a look next Monday when I'll be back to work.

@vpelikh

vpelikh commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00, thanks for letting me know, and sorry for the ping. Enjoy your vacation — no rush at all. I'll be happy to wait for your review next week. Have a great time!

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the wait, and thank you for addressing all the previous comments. The earlier provenance, fallback-input, diagnostics, public-convention, and mixed-JDK issues are now fixed. The focused tests and grund check pass.

I found one final blocker: launcher provenance affects task execution but is not tracked as a task input. If a convention-selected launcher lacks native-image, the task can successfully fall back to GRAALVM_HOME. When that same launcher is subsequently assigned explicitly, nativeCompile remains UP-TO-DATE instead of rerunning and failing according to §FS-native-invocation.1.1.

Please model the explicit/convention provenance as a task input for both nativeCompile and metadataCopy, and add a two-build regression test covering that transition.

Once this is addressed and the tests pass, I believe the PR will be ready to merge.

@vpelikh
vpelikh force-pushed the Set-default-java-launcher-from-java-toolchain branch from b4556d3 to c359646 Compare August 24, 2026 19:18
@vpelikh

vpelikh commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Apologies for the wait, and thank you for addressing all the previous comments. The earlier provenance, fallback-input, diagnostics, public-convention, and mixed-JDK issues are now fixed. The focused tests and grund check pass.

I found one final blocker: launcher provenance affects task execution but is not tracked as a task input. If a convention-selected launcher lacks native-image, the task can successfully fall back to GRAALVM_HOME. When that same launcher is subsequently assigned explicitly, nativeCompile remains UP-TO-DATE instead of rerunning and failing according to §FS-native-invocation.1.1.

Please model the explicit/convention provenance as a task input for both nativeCompile and metadataCopy, and add a two-build regression test covering that transition.

Once this is addressed and the tests pass, I believe the PR will be ready to merge.

Thanks for review once again! Addressed in c359646.

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the latest update. The production change now tracks launcher provenance as a task input for both nativeCompile and metadataCopy, and the original stale UP-TO-DATE issue is fixed in the focused local runs.

I found two remaining blockers:

  1. JavaLauncherProperty prevents the plugin from loading on Gradle 8.4 because its bytecode references the newer SupportsConvention API.
  2. The new provenance regression fixture does not reliably select the fake JDK and is not Windows-compatible, causing ToolchainDiscoveryTest failures on Gradle 9.x and across platforms.

The Windows LayeredApplicationFunctionalTest failures are unrelated to this PR and are being addressed in a separate PR.

Once the two issues above are addressed and the relevant matrix jobs pass, I believe this PR will be ready to merge.

@vpelikh

vpelikh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Hey @jormundur00 — I'd like your input on a trade-off before I finalize the JavaLauncherProperty fix for blocker P1 (Gradle 8.4 / SupportsConvention compatibility).

Context

Your P1 guidance was: "avoid implementing the evolving Property interface directly against the current Gradle API, or otherwise ensure this adapter remains compatible with Gradle 8.4." I've been following that by not implementing Property at all — instead I wrap a real Property<JavaLauncher> (returned by ObjectFactory) in a small holder that derives explicitness from Gradle's own convention semantics:

  • convention(...) installs a convention that, when consulted, marks the value non-explicit;
  • a value assigned via set()/value() never consults the convention, so it stays explicit.

This removes the 8.4-incompatible SupportsConvention bridge entirely: the holder references no SupportsConvention in its bytecode, and it stays binary-compatible across all supported Gradle versions. The plugin applies cleanly on 8.4 again.

The trade-off

This convention-flag approach handles the provenance cases except for one spec'd behavior — covered by our NativeImagePluginTest querying the convention launcher then assigning it back is reported explicit:

def launcher = main.javaLauncher.get()   // reads the convention value
main.javaLauncher.set(launcher)          // re-assigns the same value explicitly
then:
main.javaLauncherExplicit.get() == true  // expected by the spec (§1.1/§1.2)

Reading the convention (get()) consults it, so my flag flips to "non-explicit"; the subsequent set() can't flip it back because a real Property exposes no hook for "was set() called". The old JavaLauncherProperty tracked that via @Override set(), but re-implementing that @Override (unset()/unsetConvention()) is exactly what reintroduces the SupportsConvention reference and breaks 8.4.

So the realistic options are:

  1. Relax this one edge case (value read from the convention, then re-assigned to the same value, is treated as convention-sourced rather than explicit) and simplify — at the cost of the one spec'd unit test above.
  2. Keep the full read-then-set semantics by intercepting set() — which again requires either a runtime proxy or a Property subclass, both of which conflict with (or complicate) your "avoid implementing the evolving Property interface" guidance.

I'd rather not unilaterally decide, since it affects the executable-discovery spec (§FS-native-invocation.1.1/1.2). Which do you prefer — relax the "read-then-reassign" edge case, or keep a small interception layer to preserve it exactly?

@jormundur00

Copy link
Copy Markdown
Member

@vpelikh I prefer preserving the full read-then-set semantics. An explicit set(...) should remain authoritative even when the assigned launcher is the same instance previously obtained from the convention. Otherwise, we reintroduce the fallback and provenance behavior this PR is intended to fix.

A small runtime proxy around the real Property<JavaLauncher> sounds appropriate here. It can intercept the mutating methods to track provenance while delegating the actual property behavior, without compiling a concrete implementation of Gradle's evolving Property interface. Please make sure fluent methods return the proxy so subsequent mutations remain intercepted.

@vpelikh

vpelikh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00

Blocker P1 (Gradle 8.4 / SupportsConvention) — fixed per your guidance:

JavaLauncherProperty no longer implements Property directly. It's now a runtime proxy over the real ObjectFactory-created Property<JavaLauncher> (via java.lang.reflect.Proxy), so it matches whatever Property looks like in the running Gradle version. The proxy intercepts set/value (mark explicit), unset/unsetConvention (clear explicit), and fluent methods return the proxy so subsequent mutations stay intercepted. The bytecode no longer references SupportsConvention, so the plugin applies cleanly on Gradle 8.4. The read-then-reassign case is preserved.

Honestly, I'm not fully proud of this runtime-proxy approach and would be glad to change it if you have a cleaner suggestion — let me know and I'll fix it.

Blocker P2 (provenance regression fixture) — fixed:

The fixture now:

  • uses SharedConstants.NATIVE_IMAGE_EXE/GU_EXE (native-image.cmd/gu.cmd) with a cross-platform fake executable;
  • sets org.gradle.java.installations.auto-detect=false and points installations at the fake, native-free JDK so the convention launcher deterministically lacks native-image;
  • asserts build 1 selected GRAALVM_HOME as the source.

The switching the launcher from convention to explicit test now passes on both Gradle 8.4 and 9.6.1.

One note on P2: I changed the build-1 assertion to verify GraalVM location source: GRAALVM_HOME rather than pinning the exact fake-JDK path, since Gradle's toolchain service always registers the running daemon JVM as a candidate, which makes "which equivalent-version JDK is probed" nondeterministic in a full class run. If you'd still like build 1 to assert the fake JDK was the probed toolchain deterministically, I can make the fake JDK version-distinct from the daemon (e.g. Java 25 while the daemon is Java 17) — let me know your preferred way to source that second JDK.

@vpelikh
vpelikh requested a review from jormundur00 August 25, 2026 17:57
@vpelikh

vpelikh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed test failure in 55e1c3a. @jormundur00 run pipelines please.

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. The original Gradle 8.4 SupportsConvention linkage is fixed: the plugin now applies successfully on Gradle 8.4, and the generated JavaLauncherProperty bytecode no longer references that API. The provenance regression also passes locally on Gradle 9.6.1 and with configuration cache on Gradle 9.0.

I found three remaining blockers:

  1. The runtime proxy does not fully preserve Gradle Property mutation and exception semantics, including incorrectly clearing explicit provenance on unsetConvention().
  2. The regression fixture forces Gradle 8.4 to run on Java 25 and fails before plugin application.
  3. The new gu.cmd fixtures still contain POSIX shell scripts and are not executable on Windows.

grund check, plugin inspections, and the existing provenance unit tests pass.

Since this PR has been rebased onto a snapshot NBT version that does not test Gradle 8.4 in CI, please test the final changes manually with Gradle 8.4 before merging.

Once these three issues are addressed and the relevant matrix jobs pass, I believe the PR will be ready to merge.

@vpelikh

vpelikh commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00

Blocker 1 (runtime proxy must preserve Gradle Property mutation/exception semantics) — fixed:

JavaLauncherProperty still uses a runtime java.lang.reflect.Proxy over the real ObjectFactory-created Property<JavaLauncher>, so the bytecode stays free of SupportsConvention and the plugin applies on Gradle 8.4. I tightened the interceptor so Gradle semantics are preserved:

  • Every mutating call is delegated first; the provenance flag is only updated once the call succeeds. This keeps provenance consistent with Gradle's actual property state in all cases and lets any exception the backing property raises (e.g. mutating a finalized value) propagate untouched, instead of flipping the flag out-of-band. (I probed Gradle 9.6.1 directly: value(null)/convention(null) are silent no-ops on a set Property, so the guarantee is about general state-consistency rather than those specific calls.)
  • set(value)/value(...) still mark the value explicit; unset() clears it back to convention provenance.
  • unsetConvention() no longer clears explicit provenance. It only removes the convention and never touches an explicitly set value, so an explicit launcher stays explicit after the convention is dropped.
  • Fluent methods still return the proxy itself (not the delegate's returned property) so chained mutations keep passing through the interceptor.

I also added a provenance unit test: "unsetting the convention does not clear an explicit launcher assignment" — it sets a launcher, calls unsetConvention(), and asserts the value is still present and still reported explicit. It passes alongside the existing provenance suite.

Blocker 2 (regression fixture forced Gradle 8.4 onto Java 25) — fixed:

The fixture no longer sets org.gradle.java.home=${fakeJdk}. Keeping the Gradle daemon on its own JVM means the test runs on the workflow Gradle version (including 8.4) regardless of the fake JDK's higher Java version. Determinism is preserved differently: the fake, native-free JDK is registered only as a toolchain candidate at its own (version-distinct) level via org.gradle.java.installations.paths + auto-detect off, and the toolchain request at that level resolves to it as the sole matching installation, while the lower-version daemon JVM cannot satisfy it. The switching the launcher from convention to explicit test therefore remains deterministic but no longer fails Gradle 8.4 before plugin application.

Blocker 3 (POSIX gu fixtures not executable on Windows) — fixed:

The two test fixtures that write a fake gu no longer hard-code #!/bin/sh content. New cross-platform helpers (setupGuThatInstallsNativeImage / setupFailingGu) write a POSIX script on Unix and a .cmd batch on Windows, and the fake gu install native-image emits a matching native-image / native-image.cmd next to itself using SharedConstants.NATIVE_IMAGE_EXE/GU_EXE. The gu installs native-image when not found and gu installation failure falls back to error message tests now run on Windows as well as Linux/macOS.

Verification:

  • grund check passes.

  • Unit tests pass: NativeImagePluginTest (incl. the new unsetConvention test), NativeImageExecutableLocatorTest, MergeAgentFilesActionTest, and the full :native-gradle-plugin:test suite.

  • Functional suite (ToolchainDiscoveryTest), run end-to-end with a real GraalVM 25 building java-application:

    Test Gradle 9.6.1 Gradle 8.4 config-cache (9.x)
    switching the launcher from convention to explicit re-runs nativeCompile and fails (P2) ✅ PASS ✅ PASS ✅ PASS
    gu installs native-image when not found (P3) ✅ PASS ✅ PASS ✅ PASS
    gu installation failure falls back to error message (P3) ✅ PASS ✅ PASS ✅ PASS

    All three tests that changed for the blockers pass on Gradle 9.6.1, Gradle 8.4, and under the configuration cache. In particular, the Gradle 8.4 run confirms the fixture no longer forces the daemon onto Java 25 (blocker 2) and the two gu fixtures genuinely execute gu install native-image across the platform matrix (blocker 3).

As you requested, since Gradle 8.4 is not exercised by the current snapshot CI matrix, I ran the Gradle 8.4 functional test manually (above) before finalizing.

@vpelikh
vpelikh requested a review from jormundur00 August 26, 2026 12:22

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. The Gradle 8.4 daemon compatibility fix works locally: the exact provenance regression passes on Gradle 8.4 when GRAALVM_HOME is a canonical path. The configuration-cache regression on Gradle 9.0, the two gu tests on Linux with Gradle 8.4, the existing unit suite, and grund check also pass.

I found four remaining blockers:

  1. The runtime proxy still changes normal Property behavior for value(null), exception propagation, and equality.
  2. The generated Windows native-image.cmd only examines the first argument and will not handle the normal build invocation where -o appears later.
  3. Plugin inspections fail because the new JavaDoc contains non-ASCII em dashes.
  4. The provenance fixture still fails when GRAALVM_HOME is a symlink.

GitHub currently shows only the OCA check, so the Windows behavior has not been exercised by CI.

Once these issues are addressed and the relevant checks pass, I believe the PR will be ready to merge.

@vpelikh
vpelikh force-pushed the Set-default-java-launcher-from-java-toolchain branch 2 times, most recently from aa7cafb to 78ff90a Compare August 26, 2026 16:16
@vpelikh

vpelikh commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00

Blocker 1 (runtime proxy still changed normal Property behavior) — fixed:

JavaLauncherProperty's java.lang.reflect.Proxy interceptor now preserves the full Gradle Property contract exactly:

  • value(null) is a no-op on a convention-backed property. Previously the interceptor unconditionally set explicitFlag = true; now, only a non-null argument marks the value explicit, matching Gradle's own semantics where value(null) never displaces the convention.
  • Exceptions propagate unwrapped. Method.invoke(...) wraps the delegate's exception in InvocationTargetException, which previously escaped the handler as an UndeclaredThrowableException. All delegated calls now go through a helper that unwraps InvocationTargetException and rethrows the cause, so callers receive Gradle's real exception (e.g. the IllegalStateException from mutating a finalized value) untouched.
  • Object methods are handled explicitly. equals/hashCode/toString previously fell through to delegate.equals(proxy), making property.equals(property) return false. They now use referential identity (proxy.equals(proxy) is true, and hashCode is consistent with identity-based equals).

The previously-reviewed semantics are preserved: set/value(nonNull) mark explicit, unset clears back to convention provenance, unsetConvention() never clears an explicit assignment, and fluent methods return the proxy so chained mutations stay intercepted.

I added a dedicated unit test, JavaLauncherPropertyTest, with four cases pinning these behaviors: value(null) keeps a convention-sourced launcher non-explicit; value(nonNull) marks explicit; the proxied Property satisfies Object identity methods; and mutating a finalized property propagates Gradle's IllegalStateException unwrapped. All pass alongside the existing provenance suite.

Blocker 2 (installed Windows native-image.cmd only examined the first argument) — fixed:

The generated native-image.cmd no longer parses only %~1. WindowsGuBuilder now writes the existing WINDOWS_NATIVE_IMAGE_SCRIPT — the full batch argument-parsing loop that handles --version and -o appearing anywhere on the command line — verbatim to a hidden template file and gu.cmd copy /Ys it into place. This reuses the exact script the setupWorkingNativeImage direct path already uses, so a normal invocation that puts classpath options before -o now correctly creates the requested output executable. (Writing it through a raw template + copy also avoids the fragile echo/caret escaping the previous embedded version relied on.)

Blocker 3 (non-ASCII em dashes broke the ASCII checkstyle rule) — fixed:

The two JavaDoc lines in JavaLauncherProperty.java containing em dashes now use ASCII punctuation (:native-gradle-plugin:inspections / checkstyleMain pass).

Blocker 4 (symlinked GRAALVM_HOME broke the provenance fixture) — fixed:

createJdkWithoutNativeImage now resolves the source to its canonical path (sourceJdk.getCanonicalFile()) before Files.walk(...). Files.walk() does not follow a symlinked root by default, so with a symlinked GRAALVM_HOME the original copied nothing and failed because fake-jdk/bin/java was missing. Resolving first makes the walk follow the symlink and populate the fake JDK.

Outstanding for CI: the Windows branch of blocker 2's native-image.cmd (the copy of the full-argument batch) can only be exercised on a Windows runner, which I do not have locally. The two gu fixtures and the full functional suite otherwise pass on Linux/macOS.

@vpelikh
vpelikh requested a review from jormundur00 August 26, 2026 16:20

@jormundur00 jormundur00 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the latest changes locally. All of my previous blocking findings have been addressed, and I found no new blocking issues.

The focused proxy tests, plugin inspections, the symlinked GRAALVM_HOME regression, and the full ToolchainDiscoveryTest suite on Gradle 8.4 all pass locally. The current GitHub CI matrix is also green.

This is ready to merge from my side.

@vpelikh
vpelikh force-pushed the Set-default-java-launcher-from-java-toolchain branch from 78ff90a to 0076b89 Compare August 28, 2026 10:26
@vpelikh

vpelikh commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

I re-reviewed the latest changes locally. All of my previous blocking findings have been addressed, and I found no new blocking issues.

The focused proxy tests, plugin inspections, the symlinked GRAALVM_HOME regression, and the full ToolchainDiscoveryTest suite on Gradle 8.4 all pass locally. The current GitHub CI matrix is also green.

This is ready to merge from my side.

Thank you for the final review. I realize this went through quite a few follow-up rounds, and I appreciate your patience throughout all of them – sorry it took so many iterations to get everything right.

I've also squashed the commits and rebased onto the latest master, so the branch should be clean and up to date.

Let me know if there's anything else you need before merging. Glad we finally got there!

@jormundur00

Copy link
Copy Markdown
Member

To help get this PR merged faster, I attempted to fix the Windows-specific test problems directly and pushed commit 3e6fb350 to this branch.

The fix makes the fake batch executable valid for cmd.exe, uses portable paths and Java executable names, and prevents the toolchain tests from invoking the real GraalVM 17 native-image on the Windows runner. I also made the compatibility-mode test assert that nativeCompile succeeds.

Locally, the full ToolchainDiscoveryTest suite passes on Gradle 8.4 with a Java 17 daemon and Java 25 toolchain, the configuration-cache suite passes on Gradle 9.0, and plugin inspections pass. The rebased CI matrix should now verify the Windows behavior.

@vpelikh

vpelikh commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@jormundur00 thank you for fixing Windows tests!

NativeConfigRepoFunctionalTest failure looks like a transient network issue:

Execution failed for task ':graalvm-reachability-metadata:fetchMetadataRepository' (registered by plugin 'org.graalvm.build.reachability-module').
> Could not download repository metadata from https://github.com/oracle/graalvm-reachability-metadata/releases/download/1.0.12/graalvm-reachability-metadata-1.0.12.zip

A re-run of the failed job should resolve it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gradle plugin nativeCompile fails when JAVA_HOME and GRAALVM_HOME are not set

3 participants