Repository navigation
Uplift the Native Image layer configuration model - #1011
Conversation
vjovanov
left a comment
There was a problem hiding this comment.
I reviewed this with a focus on usability gaps. The architecture is right — shared selection/rendering in common, typed lazy Gradle wiring, Maven-native nil artifact resolution — and the error paths that do exist fail before Native Image is invoked. But there are several gaps worth addressing before merge. File-specific issues are inline; the cross-cutting ones are below.
1. No end-user documentation. The PR updates the internal spec docs and samples, but docs/src/docs/asciidoc/gradle-plugin.adoc, maven-plugin.adoc, and the end-to-end guides contain no mention of layers at all, and changelog.adoc gets no entry. Users cannot discover the layers { } DSL, the layer-create goal, <useLayers>, or the nil dependency type from the published documentation. For a PR whose point is uplifting usability, this is the biggest gap.
2. The happy path was never verified on a supported Native Image release. Per the PR's own testing notes, the consumer image was only run on 25.0.3 — the exact release this PR blocks for consumption. The full produce → consume → run flow should be validated on a newer Native Image before merge.
3. Maven setup silently depends on <extensions>true</extensions> for the nil artifact handler registration, but that is only visible in the sample. Forgetting it degrades confusingly (the nil dependency falls back to the default artifact handler and can leak onto compile classpaths). It should be documented as a requirement with a recognizable failure mode.
4. Empty layers aren't validated. A named Gradle layer with no contents renders an unqualified -H:LayerCreate=<name>.nil with an empty classpath and defers to an obscure Native Image error. A configuration-time "layer 'x' has no contents" would be friendlier.
5. The new model still warns Experimental support for layered images enabled. DSL may change at any time. on every build. If the first-class DSL is the stable uplift, consider scoping or softening that message — it reads oddly against the PR's framing.
Summary of the inline comments: the documented useLayer(layers.base) example doesn't resolve (layers is shadowed inside binary closures), layer = assignment accumulates rather than replaces, fromConfiguration/all silently drop project dependencies (and Gradle/Maven all semantics diverge in multi-module builds), the 25.0.3 guard exists only in Maven, unreferenced nil dependencies are silently ignored, and malformed coordinates surface as raw IllegalArgumentExceptions.
|
In the example from the description: is |
|
PM test reportI reviewed the full diff, published the PR's plugins ( What works (verified by running it)
Bugs (should block merge)1. The legacy Gradle compatibility path is broken end-to-end. Using exactly the DSL from master's shipped sample ( This matches the PR's own disclosure that review cycle 3 flagged "two remaining legacy compatibility blockers" and marked publication blocked. The retained unit tests check argument rendering, not the produced artifact name. Fix: have the legacy 2. 3. Maven modules-only layer leaks the whole project classpath into the layer build. 4. Latent CI failure in the Maven functional test. Its success branch (GraalVM ≥ 25.0.5) executes the layered app without setting UX traps (the first hour of every adopter)
Suggested priorityItems 1–4 are merge blockers (1 and 2 break the PR's explicit compatibility promise; 3 breaks the model's core claim; 4 is a time bomb in CI). The shadowing/ordering/name-validation trio is what I'd fix next — they're cheap, and they're the first three errors any real adopter will hit. Report produced by a hands-on review: static review of all 55 files plus scripted break-it scenarios against locally published |
Follow-up: layer test-coverage auditTwo questions worth answering explicitly for this PR: (1) are all selector types — Selector coverage
Every selector is covered at the string-rendering level, but against a real native-image only
Consumption contextsOnly main executable binaries consume layers in any test, in both plugins:
Suggested additions
|
0c08af9 to
0de6316
Compare
13bc4d7 to
3c7efb8
Compare
Product review: driving the layer feature end-to-endI published the plugins from this branch and used them from real Gradle and Maven projects (not the functional test harness) to answer a few adoption questions: does it work with non-Maven dependencies, can it be used for tests / shared libraries / executables, can several consumers share one layer, and can the result actually be shipped. Short version: the DSL and goal surface are in good shape, and the common cases work. The blocker is deployment — a layer consumed from a Maven repository builds successfully and produces a binary that cannot start. EnvironmentLayer consumption is broken on GraalVM 25.0.x, so everything below was run on GraalVM CE 25.1.3 unless stated otherwise. # publish the branch
git fetch origin pull/1011/head:pr-1011 && git checkout pr-1011
./gradlew publishAllPublicationsToCommonRepository --no-parallel # -> build/common-repo
# GraalVM CE 25.1.3
curl -sLO https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.1.3/graalvm-community-jdk-25i1-25.0.3_linux-x64_bin.tar.gz
tar xzf graalvm-community-jdk-25i1-25.0.3_linux-x64_bin.tar.gz
export GRAALVM_HOME=$PWD/graalvm-community-25.1.3+9.1
export JAVA_HOME=$GRAALVM_HOME1. Blocker: a repository-resolved layer produces an unrunnable binary
Repro cd samples/layered-maven-application
mvn -Dnative.maven.plugin.version=1.1.8-SNAPSHOT install # reactor build, succeeds
ls ~/.m2/repository/org/graalvm/buildtools/samples/base-layer/1.0-SNAPSHOT/
# base-layer-1.0-SNAPSHOT.jar
# base-layer-1.0-SNAPSHOT.nil (916 MB)
# base-layer-1.0-SNAPSHOT.pom
find ~/.m2/repository -name '*.so' # nothing
# now build a consumer OUTSIDE the reactor: copy application/ out, drop the <parent>,
# give it its own groupId/version, and keep the <type>nil</type> dependency + <useLayers>
cd /tmp/standalone-consumer && mvn package # succeeds
./target/standalone-consumerThe same happens for It only works inside a reactor because So the PR's claim that "attached Related sizing concern: the On the Gradle side the same gap exists but is hidden: build/native/nativeCompile/app
# error while loading shared libraries: liblegacy.so: cannot open shared object file2. No version gate on layer consumptionOn GraalVM 25.0.1 — the LTS most users have installed — consuming a layer fails with a raw Native Image internal error: The test suite already knows about this ( Related: the PR description says validation ran on Oracle GraalVM 25.0.2, where 3. Native tests fail unless the layer has the right JDK modulesThe sample ships Adding layers.dependencies.contents.modules('java.sql', 'java.management')
binaries.test { usesLayer('dependencies') }This requirement is not documented anywhere, and 4. Custom shared-library binary + layer crashes without
|
| configuration | result |
|---|---|
create('api') { sharedLibrary = true } (no layer) |
builds |
binaries.main.sharedLibrary = true + layer |
builds |
create('api') { usesLayer(...); sharedLibrary = true } |
crashes |
create('api') { usesLayer(...); sharedLibrary = true; mainClass = '...' } |
builds |
Worth validating in the plugin rather than letting Native Image abort.
5. Non-Maven dependencies
Gradle: works well. I built a jar by hand with javac/jar (no build tool, no coordinates) plus a project(':liblocal') dependency:
dependencies {
implementation files('libs/legacy.jar')
implementation project(':liblocal')
implementation 'org.apache.commons:commons-lang3:3.17.0'
}
graalvmNative {
layers {
legacy { contents { from(files('libs/legacy.jar')) } }
everything { contents { fromConfiguration(configurations.runtimeClasspath) } }
}
binaries { main { usesLayer('legacy') } }
}Both layers build, fromConfiguration correctly wires :liblocal:jar as a producer task dependency, and ./gradlew :app:nativeRun prints legacy-jar | project-dependency | Maven-dependency. No complaints here.
Maven: producing is fine, consuming is coordinate-only. UseLayerConfiguration has a single artifact field parsed as groupId:artifactId[:version], so there is no way to point <useLayers> at a .nil file. A layer produced by Gradle, downloaded, or vendored cannot be consumed without mvn install:install-file. A <useLayer><file>...</file></useLayer> form would close this.
6. Sharing one layer across consumers and across projects
Within one Gradle project: works. Four consumers on one layer configure cleanly, and main + a second executable + test all build against a single nativeDependenciesLayer:
> Task :nativeDependenciesLayer UP-TO-DATE
> Task :nativeCompile
> Task :nativeOtherCompile
> Task :nativeTestCompile
Across Gradle projects: only via an undocumented escape hatch. NativeImageCompileOptions.getLayerFiles() is public, and this does work:
binaries.create('external') {
mainClass = 'org.graalvm.demo.Application'
layerFiles.from(someProject.layout.buildDirectory.file('native/layers/deps/deps.nil'))
}- '-H:LayerUse' (origin(s): command line)
Finished generating 'layered-java-application' in 29.1s.
But there is no task-dependency wiring (a clean build fails), no name validation, and no compatibility-classpath propagation. A supported usesLayer(file) / usesLayerFrom(configuration) — ideally an outgoing variant so project(':base-layer') just works — would make this the answer to "how do several projects share one layer" instead of a trick.
7. pom-packaging producer works, and is probably the pattern to recommend
A module whose only job is declaring dependencies:
<packaging>pom</packaging>
<dependencies><dependency>...commons-lang3...</dependency></dependencies>
...
<execution>
<phase>package</phase>
<goals><goal>layer-create</goal></goals>
<configuration><layer><name>deps</name><all>true</all></layer></configuration>
</execution>mvn install produces a clean pom-only-layer-1.0.0.nil + .pom with no jar. This is the tidiest producer shape and is not mentioned anywhere in the docs.
8. Smaller items
- Configuration cache fails:
15 problems were found storing the configuration cache(DefaultProject,TaskContainer,SourceSetContainernot serializable). Pre-existing —BaseNativeImageOptionson master already holds aTaskContainer— butNativeImageLayerandLayerContentsadd aProjectfield, so "configuration-cache-safe" in the description overstates the current state. - Custom binaries all default to the same image name (
layered-java-applicationappears in bothnativeCompile/andnativeOtherCompile/). Pre-existing, but layers make multi-binary builds much more common. changelog.adocgains a "Release 1.1.6" section holding an unrelated Surefire entry, inserted above "Release 1.0.1".
Documentation gaps
The new adoc sections cover syntax well. What is missing is everything needed to actually ship:
- Requirements — "layers require GraalVM 25.1 or later", at the top of both pages.
- Deployment, with a worked example — currently one paragraph in
maven-plugin.adocand nothing ingradle-plugin.adoc. Needs: which files ship (.so, not.nil), how to place them next to the application, a container/tarball layout, and theLD_LIBRARY_PATH/DYLD_LIBRARY_PATH/PATHsetup. As written, the docs describe a build whose output will not start. .nilvs.so— build-time input vs runtime artifact, and their real sizes, so nobody is surprised by a 900 MB repository artifact.- Module selection for native tests — that
java.basealone is not enough, thatjava.sql/java.management/java.xmlare commonly required, and whatNewly seen boot package ...means. This is the most likely first failure a user hits. - Sharing a layer across modules — an end-to-end Maven example (producer, consumers, reactor vs repository) and, for Gradle, an honest statement of the current limitation.
pom-packaging producer module as the recommended producer shape.- Shared libraries — that a layered shared-library binary needs a
mainClass.
Happy to open follow-up issues for any of these, or send a docs patch for items 1–4.
Correction: the "core type ... not seen as reachable the initial layer" failure is a missing
|
| layer modules | app | result |
|---|---|---|
java.sql, java.management, java.xml |
trivial | ❌ core type ... not seen as reachable the initial layer |
java.base, java.sql, java.management, java.xml |
trivial | ✅ 1 tests successful |
java.sql, java.management, java.xml |
commons-lang3 + file jar + project dep | ❌ core type ... not seen as reachable the initial layer |
java.base, java.sql, java.management, java.xml |
commons-lang3 + file jar + project dep | ✅ 1 tests successful |
Repro
graalvmNative {
metadataRepository.enabled = false
layers { dependencies { contents { modules('java.sql', 'java.management', 'java.xml') } } }
binaries { main { usesLayer('dependencies') }; test { usesLayer('dependencies') } }
}./gradlew :nativeTest # -> core type ... was not seen as reachable the initial layerAdd 'java.base' to the modules(...) list, change nothing else:
./gradlew :nativeTest # -> [ 1 tests successful ]Corrected model of layer module selection
Two distinct failure modes, and they're easy to confuse:
- Missing
java.base→The core type AnalysisType<...> was not seen as reachable the initial layer. Reads like an internal Native Image bug; gives no hint that a module is missing. - Missing some other needed module →
Newly seen boot package <pkg>/Newly seen platform package <pkg>. Nameable: add the module that owns the package.
So java.base is necessary but not sufficient. My original point stands that the sample's modules('java.base') alone is not enough for a native test — that still fails with Newly seen platform package package java.sql, and needs java.sql and java.management on top. The correct guidance is java.base plus whatever the final image touches.
Suggested fix
This is cheap to catch in the plugin. When a layer has a non-empty module selection that doesn't include java.base, fail (or at minimum warn) before invoking Native Image, with a message naming java.base. That converts the least actionable error in the whole feature into a one-line fix for the user. It fits naturally next to the existing empty/duplicate/malformed validation in ArtifactSelection / NativeImageLayerArguments.
Failing that, the docs must state plainly that java.base is effectively mandatory in every module-selecting layer, and that modules(...) appends rather than replaces — the sample's modules("java.base") masking my override is exactly how a user will trip over this.
What this does and doesn't change
It resolves the one failure in my review I couldn't characterize, and it's good news: layered native tests do work for non-trivial applications on GraalVM 25.1, once java.base is present. The other findings are unaffected — in particular the .so publishing gap remains the blocker, since a repository-resolved layer still yields a binary that cannot start.
|
The PR description still doesn't show me all the possibilities of the DSL. |
| contents { | ||
| modules('java.base', 'java.logging') | ||
| packages('org.slf4j') | ||
| fromConfiguration(configurations.runtimeClasspath) |
There was a problem hiding this comment.
Won't this capture the org.slf4j?
There was a problem hiding this comment.
Good catch. Yes, the configuration selector captures the resolved artifact, so combining it with the package selector was misleading. I removed it from this example. Our package-selector functional test already uses the intended form, packages("org.slf4j"), without fromConfiguration.
|
|
||
| binaries { | ||
| test { | ||
| usesLayer('sharedRuntime') |
There was a problem hiding this comment.
It was assuming a project-defined integTest source set and task, which is too project-specific for this guide. I removed that additional-test-suite example.
|
|
||
| [source,groovy] | ||
| ---- | ||
| graalvmNative { |
There was a problem hiding this comment.
Could you clarify what you mean here? We already test the documented layer scenarios in our functional tests. I had intended this as a separate user-facing guide for layered-image workflows, rather than moving the documentation into test fixtures.
There was a problem hiding this comment.
Like the doc snippets. I would like to test all of these verbatim.
| layer and rebuild it. | ||
|
|
||
| [[layer-chaining]] | ||
| == Prepare Multi-Level Layer Stacks |
There was a problem hiding this comment.
The DSL can express it, but current Native Image releases do not support the resulting chained shared-layer topology. I removed the public documentation and support claims, while leaving the API in place for a future Native Image release that supports it. We also retain an ignored functional test for this capability.
There was a problem hiding this comment.
Well, I would not mention it as it doesn't work.
8b38ac5 to
fd7f8cc
Compare
|
We can do followup fixes for this later if needed. It has passed enough reviews that we can merge it and use it in the next release. |
….8 to 1.1.9 [skip ci] Bumps [org.graalvm.buildtools:native-maven-plugin](https://github.com/graalvm/native-build-tools) from 1.1.8 to 1.1.9. Release notes *Sourced from [org.graalvm.buildtools:native-maven-plugin's releases](https://github.com/graalvm/native-build-tools/releases).* > 1.1.9 > ----- > > What's Changed > -------------- > > * Make repository test output concise by default by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1004](https://redirect.github.com/graalvm/native-build-tools/pull/1004) > * Decouple Maven plugin unit tests from remote bootstrap tasks by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1007](https://redirect.github.com/graalvm/native-build-tools/pull/1007) > * Clarify build prerequisites and add a core build mode by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1012](https://redirect.github.com/graalvm/native-build-tools/pull/1012) > * Release 1.1.8 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1015](https://redirect.github.com/graalvm/native-build-tools/pull/1015) > * Bump version to 1.1.9-SNAPSHOT by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1016](https://redirect.github.com/graalvm/native-build-tools/pull/1016) > * Uplift the Native Image layer configuration model by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1011](https://redirect.github.com/graalvm/native-build-tools/pull/1011) > * Update reachability metadata to 1.0.10 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1021](https://redirect.github.com/graalvm/native-build-tools/pull/1021) > > **Full Changelog**: <graalvm/native-build-tools@1.1.8...1.1.9> Commits * [`70a22ad`](graalvm/native-build-tools@70a22ad) Release 1.1.9 * [`226c2f2`](graalvm/native-build-tools@226c2f2) Merge pull request [#1021](https://redirect.github.com/graalvm/native-build-tools/issues/1021) from graalvm/update-metadata-to-1.0.10 * [`750e24d`](graalvm/native-build-tools@750e24d) Update reachability metadata to 1.0.10 * [`f9d090c`](graalvm/native-build-tools@f9d090c) Clarify layer selector and test suite support * [`8a62507`](graalvm/native-build-tools@8a62507) Skip later Gradle layer consumers on GraalVM 25.0 * [`fa9cb64`](graalvm/native-build-tools@fa9cb64) Narrow GraalVM 25.0 layer test skips * [`a7f0ca4`](graalvm/native-build-tools@a7f0ca4) Skip Gradle layer consumers on GraalVM 25.0 * [`cf67f05`](graalvm/native-build-tools@cf67f05) Skip Maven shared layer test on GraalVM 25.0 * [`60da37c`](graalvm/native-build-tools@60da37c) Skip Maven all-selector layer test on GraalVM 25.0 * [`0498739`](graalvm/native-build-tools@0498739) Fix layered image runtime distribution * Additional commits viewable in [compare view](graalvm/native-build-tools@1.1.8...1.1.9) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
….6 to 1.1.9 [skip ci] Bumps [org.graalvm.buildtools:native-maven-plugin](https://github.com/graalvm/native-build-tools) from 1.1.6 to 1.1.9. Release notes *Sourced from [org.graalvm.buildtools:native-maven-plugin's releases](https://github.com/graalvm/native-build-tools/releases).* > 1.1.9 > ----- > > What's Changed > -------------- > > * Make repository test output concise by default by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1004](https://redirect.github.com/graalvm/native-build-tools/pull/1004) > * Decouple Maven plugin unit tests from remote bootstrap tasks by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1007](https://redirect.github.com/graalvm/native-build-tools/pull/1007) > * Clarify build prerequisites and add a core build mode by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1012](https://redirect.github.com/graalvm/native-build-tools/pull/1012) > * Release 1.1.8 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1015](https://redirect.github.com/graalvm/native-build-tools/pull/1015) > * Bump version to 1.1.9-SNAPSHOT by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1016](https://redirect.github.com/graalvm/native-build-tools/pull/1016) > * Uplift the Native Image layer configuration model by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1011](https://redirect.github.com/graalvm/native-build-tools/pull/1011) > * Update reachability metadata to 1.0.10 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1021](https://redirect.github.com/graalvm/native-build-tools/pull/1021) > > **Full Changelog**: <graalvm/native-build-tools@1.1.8...1.1.9> > > 1.1.8 > ----- > > What's Changed > -------------- > > * Release 1.1.7 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1009](https://redirect.github.com/graalvm/native-build-tools/pull/1009) > * Bump version to 1.1.8-SNAPSHOT by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1010](https://redirect.github.com/graalvm/native-build-tools/pull/1010) > * Deprecate fallback plugin options by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#997](https://redirect.github.com/graalvm/native-build-tools/pull/997) > * Update reachability metadata to 1.0.9 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1014](https://redirect.github.com/graalvm/native-build-tools/pull/1014) > > **Full Changelog**: <graalvm/native-build-tools@1.1.7...1.1.8> > > 1.1.7 > ----- > > What's Changed > -------------- > > * Release 1.1.6 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#996](https://redirect.github.com/graalvm/native-build-tools/pull/996) > * Suppress missing Javadoc diagnostics across convention tasks by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#994](https://redirect.github.com/graalvm/native-build-tools/pull/994) > * Replace deprecated Kotlin DSL delegated accessors by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#998](https://redirect.github.com/graalvm/native-build-tools/pull/998) > * Fix Maven plugin unit-test mock maker and SLF4J setup by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#999](https://redirect.github.com/graalvm/native-build-tools/pull/999) > * Bump version to 1.1.7-SNAPSHOT by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1001](https://redirect.github.com/graalvm/native-build-tools/pull/1001) > * Bump io.netty:netty-codec-http from 4.1.135.Final to 4.1.136.Final in /samples/metadata-repo-integration by [`@dependabot`](https://github.com/dependabot)[bot] in [graalvm/native-build-tools#1002](https://redirect.github.com/graalvm/native-build-tools/pull/1002) > * Avoid Maven internal API for console colors by [`@jormundur00`](https://github.com/jormundur00) in [graalvm/native-build-tools#1003](https://redirect.github.com/graalvm/native-build-tools/pull/1003) > * Update reachability metadata to 1.0.8 by [`@graalvmbot`](https://github.com/graalvmbot) in [graalvm/native-build-tools#1008](https://redirect.github.com/graalvm/native-build-tools/pull/1008) > > **Full Changelog**: <graalvm/native-build-tools@1.1.6...1.1.7> Commits * [`70a22ad`](graalvm/native-build-tools@70a22ad) Release 1.1.9 * [`226c2f2`](graalvm/native-build-tools@226c2f2) Merge pull request [#1021](https://redirect.github.com/graalvm/native-build-tools/issues/1021) from graalvm/update-metadata-to-1.0.10 * [`750e24d`](graalvm/native-build-tools@750e24d) Update reachability metadata to 1.0.10 * [`f9d090c`](graalvm/native-build-tools@f9d090c) Clarify layer selector and test suite support * [`8a62507`](graalvm/native-build-tools@8a62507) Skip later Gradle layer consumers on GraalVM 25.0 * [`fa9cb64`](graalvm/native-build-tools@fa9cb64) Narrow GraalVM 25.0 layer test skips * [`a7f0ca4`](graalvm/native-build-tools@a7f0ca4) Skip Gradle layer consumers on GraalVM 25.0 * [`cf67f05`](graalvm/native-build-tools@cf67f05) Skip Maven shared layer test on GraalVM 25.0 * [`60da37c`](graalvm/native-build-tools@60da37c) Skip Maven all-selector layer test on GraalVM 25.0 * [`0498739`](graalvm/native-build-tools@0498739) Fix layered image runtime distribution * Additional commits viewable in [compare view](graalvm/native-build-tools@1.1.6...1.1.9) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
What changed
This PR turns Native Image layers from an experimental, binary-scoped Gradle feature into a shared model with first-class Gradle and Maven support.
A layer produces a build-time
.nilartifact plus runtime shared libraries. Consumers pass the.nilto Native Image and must ship the runtime libraries with the final executable.Gradle DSL: complete surface
A named layer is an entry in
graalvmNative.layers. Its task is namednative<LayerName>Layerand writesbuild/native/layers/<name>/<name>.nil.Declare layer contents
Use any combination of these selectors in a layer's
contentsblock:graalvmNative { layers { selected { contents { // JDK modules; include java.base in a module-selecting base layer. modules('java.base', 'java.logging') // Packages found on the layer classpath. packages('org.slf4j') // Explicit files, FileCollections, directories, or providers. from(files('libs/local.jar')) // Every resolved artifact from a Configuration or Provider<Configuration>. fromConfiguration(configurations.runtimeClasspath) // An individually resolved module; transitive is true by default. dependencies('org.slf4j:slf4j-api:2.0.17') // An individually resolved module without its transitives. dependencies('com.acme:standalone:1.0') { transitive = false } // A version-catalog accessor is also accepted. // dependencies(libs.slf4j.api) } // Producer-specific Native Image settings. buildArgs('-Ob') verbose = true } everything { contents { // The full runtime dependency graph, including project artifacts. all = true // all may also be combined with module/package selectors. modules('java.base') } } } }modules(...),packages(...),from(...),fromConfiguration(...), anddependencies(...)append selections.all = trueselects the runtime graph. Empty, invalid, and duplicate selections fail before Native Image runs.Consume named layers
The preferred consumer form is lazy, name-based
usesLayer('<name>'); it works regardless of declaration order. Repeat it to select multiple layers.graalvmNative { binaries { main { usesLayer('selected') usesLayer('everything') } test { usesLayer('selected') } create('api') { sharedLibrary = true imageName = 'api' usesLayer('selected') } } }Typed alternatives are available when passing the layer object or a provider:
graalvmNative { binaries.main { useLayer(graalvmNative.layers.selected) useLayer(graalvmNative.layers.named('everything')) } // The singular assignment selects one layer and replaces a prior assignment. binaries.test { layer = graalvmNative.layers.selected } }Inside a Groovy binary closure, use the qualified
graalvmNative.layers.<name>form for typed references: the deprecated binary-levellayersproperty shadows the extension container.A layer can itself consume a named layer to construct a stack:
graalvmNative { layers { base { contents.modules('java.base') } framework { usesLayer('base') contents.packages('org.example.framework') } } binaries.main { usesLayer('framework') } }The older binary-scoped forms remain deprecated compatibility adapters:
graalvmNative { binaries { libdependencies { createLayer { modules = ['java.base'] classpath.from(externalDependenciesOf(configurations.runtimeClasspath)) } } main { useLayer('libdependencies') } } }nativeRunsupplies selected layer directories to the platform loader. With the Gradleapplicationplugin,installDist,distZip, anddistTarinclude the executable, staged runtime libraries, and a loader-aware launcher. Named-layer reuse is first-class within a Gradle project; cross-project use currently requires manual file/runtime wiring.Maven DSL: complete surface
Maven creates a layer with the
layer-creategoal. Configure the plugin as an extension so Maven consistently registers thenilartifact type in reactor and repository builds:Declare layer contents
Bind
layer-createin a producer execution. A<layer>supports module, package, path, dependency-coordinate, and full-runtime selectors:Dependency coordinates use
groupId:artifactIdorgroupId:artifactId:version;<transitive>defaults totrue. The goal attaches both<name>.niland a platform-classified runtime ZIP. A POM-packaging module may be a layer-only producer.Consume one or more layers
Declare each producer as a normal dependency of type
nil, then select it by coordinate.<useLayers>can be configured at plugin level or goal-execution level.<useLayers>applies to every goal receiving that configuration: application compilation,native:test, shared-library builds, andlayer-create. Use an execution-specific configuration when those goals need different stacks. Maven consumption is coordinate-only; a local.nilfile path is not a supported<useLayers>form.Repository consumers resolve the matching runtime ZIP and stage it under
target/native/layer-runtime. Package that directory with the final executable and expose it throughLD_LIBRARY_PATH(Linux),DYLD_LIBRARY_PATH(macOS), orPATH(Windows).Shared behavior and deployment
common/utilsrenders-H:LayerCreateand-H:LayerUseconsistently for both plugins..nilfiles are build-time artifacts. Runtime shared libraries are deployment artifacts and must accompany the final executable..nilfiles can be large; plan Maven repository retention and cache capacity before broadly publishing layers.Testing
Validated locally with grounding and formatting checks; shared utility tests; Gradle inspections, unit tests, and layered functional tests; and Maven inspections, unit tests, and layered functional tests.
Functional coverage includes module, package, path, dependency, and all-dependency selection where supported by the Native Image version; main executable, native-test, shared-library, and additional native-test-suite consumers; Maven reactor/
nilresolution; and layered executable launch with the producer directory onLD_LIBRARY_PATH.Closes #977
AI workflow
Generated by Rhei’s
github-issue-fixworkflow using 23 completed AI-assisted steps, 3 resolved models, and 3 focused review cycles.View AI workflow details
Approved proposal ID:
193a8259f600cc11The proposal was approved by an authorized repository member with
/rhei approve 193a8259f600cc11before implementation.State path:
issue-intake → approval-check → approval-apply → implement-fix → validate/review cycle 1 → address-review → validate/review cycle 2 → address-review → validate/review cycle 3 → record-blocked-publication → completedDeterministic approval, dispatch, review-routing, and completion transitions were performed by Rhei and did not consume model tokens.
openai:gpt-5.6-solvia Codex, reasoning effortmedium. Inspected the issue, repository rules, worktree, and specification fit. Tokens: 1,247,127 total; 1,231,844 input including 1,133,568 cached-read; 15,283 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Implemented the shared, Gradle, Maven, specification, sample, and test changes. Tokens: 23,727,061 total; 23,664,080 input including 22,914,688 cached-read; 62,981 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Ran grounding, formatting, focused unit, inspection, functional, and Native Image checks. Tokens: 2,849,937 total; 2,836,313 input including 2,691,712 cached-read; 13,624 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Reviewed issue scope and accepted behavior. Tokens: 523,722 total; 518,684 input including 450,048 cached-read; 5,038 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Reviewed grund alignment, repository rules, and specification coverage. Tokens: 559,568 total; 553,019 input including 482,432 cached-read; 6,549 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Reviewed code quality, API boundaries, maintainability, and coverage. Tokens: 635,412 total; 627,370 input including 540,160 cached-read; 8,042 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Assessed test evidence, failures, and remaining CI risk. Tokens: 202,333 total; 198,525 input including 161,792 cached-read; 3,808 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Reconciled focused findings into the first repair list. Tokens: 137,439 total; 134,751 input including 108,800 cached-read; 2,688 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Addressed the first aggregate review findings. Tokens: 10,152,091 total; 10,136,921 input including 9,917,312 cached-read; 15,170 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Re-ran focused checks after the first repair. Tokens: 3,897,354 total; 3,884,800 input including 3,730,176 cached-read; 12,554 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Rechecked issue requirements after repair. Tokens: 239,926 total; 236,685 input including 191,104 cached-read; 3,241 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Rechecked specifications and grounding after repair. Tokens: 558,130 total; 549,894 input including 473,600 cached-read; 8,236 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Rechecked implementation quality and edge cases after repair. Tokens: 951,066 total; 943,220 input including 846,848 cached-read; 7,846 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Reassessed validation evidence and disclosure. Tokens: 201,903 total; 197,665 input including 158,336 cached-read; 4,238 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Reconciled the second focused review cycle. Tokens: 160,616 total; 158,379 input including 112,256 cached-read; 2,237 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Addressed the second aggregate review findings. Tokens: 604,631 total; 598,878 input including 542,336 cached-read; 5,753 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Re-ran focused validation after the second repair. Tokens: 2,177,923 total; 2,165,805 input including 2,049,536 cached-read; 12,118 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Completed the final requirements-focused review. Tokens: 488,847 total; 483,569 input including 425,088 cached-read; 5,278 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Completed the final specification and grounding review. Tokens: 490,430 total; 485,942 input including 428,544 cached-read; 4,488 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Identified the two remaining legacy compatibility blockers. Tokens: 890,896 total; 881,455 input including 800,896 cached-read; 9,441 output.openai:gpt-5.6-terravia Codex, reasoning effortmedium. Confirmed focused validation passed and recorded broader gaps. Tokens: 239,300 total; 234,312 input including 201,088 cached-read; 4,988 output.openai:gpt-5.6-solvia Codex, reasoning effortmedium. Marked publication blocked because the bounded repair visits were exhausted with two fixable compatibility findings. Tokens: 228,588 total; 225,929 input including 199,424 cached-read; 2,659 output.openai:gpt-5.6-lunavia Codex, reasoning effortmedium. Recorded the local-only result without pushing or opening a PR. Tokens: 135,286 total; 133,502 input including 110,208 cached-read; 1,784 output.Rhei accounting covers all 23 completed model invocations: 51,299,586 total tokens; 51,081,542 input tokens including 48,669,952 cached-read tokens; and 218,044 output tokens. Cached-read tokens are included in input and total counts. Cache-write dimensions were unsupported, and the recorded models were unpriced.
After the bounded Rhei review-repair loop ended, a human explicitly directed a manual continuation through Codex. That continuation added API-level
@Deprecatedmigration documentation, added focused legacy producer/consumer compatibility coverage, reran the relevant grounding, inspection, unit, and functional checks, committed the result, and published this PR. This post-Rhei session is disclosed separately because its token usage was not captured by Rhei accounting.