codeconsole commented on code in PR #16094:
URL: https://github.com/apache/grails-core/pull/16094#discussion_r3792529178


##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -1115,6 +1265,146 @@ ${importStatements}
         }
     }
 
+    /**
+     * Wires up the cache the JDK can write for an application, so the next 
start reads what a
+     * training run worked out rather than working it out again.
+     *
+     * <p>Three steps, because the cache is only usable against the layout it 
was trained on: the
+     * archive is extracted, the extracted application is run and asked for 
its pages, and what the
+     * run recorded is left beside it. An application asks for this with
+     * {@code grails.aotCache.enabled}, and says which of its pages matter.</p>
+     */
+    protected void configureAotCache(Project project) {
+        AotCacheExtension extension = ((ExtensionAware) 
project.extensions.getByName('grails'))
+                .extensions.create('aotCache', AotCacheExtension)
+        extension.enabled.convention(false)
+        extension.paths.convention([])
+        extension.jvmArguments.convention(['-Dspring.aot.enabled=true', 
'-Dgrails.env=production'])
+        extension.port.convention(TRAINING_PORT)
+        
extension.startTimeoutSeconds.convention(TRAINING_START_TIMEOUT_SECONDS)
+
+        project.pluginManager.withPlugin(SPRING_BOOT_PLUGIN) {
+            TaskProvider<?> bootJar = project.tasks.named('bootJar')
+            Provider<Directory> application = 
project.layout.buildDirectory.dir('aot-cache/application')
+            Provider<JavaLauncher> launcher = trainingLauncher(project)
+
+            TaskProvider<ExtractApplicationTask> extract = 
project.tasks.register(
+                    'extractAotCacheApplication', ExtractApplicationTask) { 
ExtractApplicationTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Extracts the application, which is the 
form the cache is read against'
+                task.onlyIf { extension.enabled.get() }
+                task.archiveFile.set(archiveFileOf(bootJar))
+                // Mapped rather than read, so the JDK the toolchain resolves 
to is not provisioned
+                // by every build that merely reads this project.
+                task.javaExecutable.set(launcher.map { JavaLauncher java -> 
java.executablePath.asFile.absolutePath })
+                task.destination.set(application)
+            }
+
+            project.tasks.register('trainAotCache', TrainAotCacheTask) { 
TrainAotCacheTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Runs the application once so the JDK can 
write down what starting it needs'
+                task.onlyIf { extension.enabled.get() }
+                task.dependsOn(extract)
+                task.applicationDirectory.set(application)
+                task.archiveFileName.set(archiveFileOf(bootJar).map { 
RegularFile file -> file.asFile.name })
+                // Beside the extracted application rather than inside it. 
Inside, the cache and its
+                // metadata land in the directory this task declares as its 
input, so writing them
+                // changes that input and the task can never be up to date -- 
every build would run
+                // the application again to record what the last one already 
recorded.
+                Provider<Directory> beside = 
project.layout.buildDirectory.dir('aot-cache')
+                task.cacheFile.set(beside.map { Directory dir -> 
dir.file("${project.name}.aot") })
+                task.metadataFile.set(beside.map { Directory dir -> 
dir.file('aot-cache.properties') })
+                task.javaExecutable.set(launcher.map { JavaLauncher java -> 
java.executablePath.asFile.absolutePath })
+                // Recorded from the JDK that will run the training, not the 
one running the build.
+                // A cache is read only by the JDK build that wrote it, and 
this is what a deployment
+                // checks that against -- so naming the wrong one is worse 
than naming none.
+                task.javaVersion.set(launcher.map { JavaLauncher java -> 
java.metadata.jvmVersion })
+                task.javaVendor.set(launcher.map { JavaLauncher java -> 
java.metadata.vendor })
+                task.jvmArguments.set(extension.jvmArguments)
+                task.paths.set(extension.paths)
+                task.port.set(extension.port)
+                task.startTimeoutSeconds.set(extension.startTimeoutSeconds)
+            }
+        }
+    }
+
+    /**
+     * The archive a task should consume, as something to be resolved when it 
runs.
+     *
+     * <p>A provider rather than a file: asking the task for its archive 
resolves the task, and a
+     * task resolved while the build is configured is one every build pays for 
whether or not it
+     * asked for an archive.</p>
+     */
+    private static Provider<RegularFile> archiveFileOf(TaskProvider<?> 
bootJar) {
+        bootJar.flatMap { Task task -> (Provider<RegularFile>) 
task.property('archiveFile') }
+    }
+
+    /**
+     * The JDK the cache will be trained on, which is the project's toolchain 
where it declares one.
+     *
+     * <p>A cache is read only by the JDK build that wrote it, so training has 
to happen on the JDK
+     * the application is compiled for rather than whichever one happens to be 
running Gradle. A
+     * project on the Java 21 baseline with a Java 25 toolchain compiles with 
25 and would otherwise
+     * have trained with 21, where the cache options do not exist -- and the 
failure it reported was
+     * that the training run ended before it started serving.</p>
+     *
+     * <p>Where no toolchain is declared this resolves to the JDK running the 
build, which is also
+     * the one that compiled the application.</p>
+     */
+    private static Provider<JavaLauncher> trainingLauncher(Project project) {
+        JavaToolchainService toolchains = 
project.extensions.getByType(JavaToolchainService)
+        JavaPluginExtension java = 
project.extensions.getByType(JavaPluginExtension)
+        toolchains.launcherFor(java.toolchain)
+    }
+
+    /**
+     * Records the application's own classes and pages so a native image keeps 
them usable. The build
+     * output already names both, so an application does not have to be traced 
to be buildable.
+     */
+    protected void configureNativeMetadata(Project project) {
+        // Only where an image is actually being built. The metadata is read 
by nothing else, and
+        // generating it means reading the compiled classes and the pages 
compiled into every
+        // dependency -- so wiring it into processResources unconditionally 
made a build that only
+        // wanted to write a resource compile its sources and resolve its 
whole runtime classpath
+        // first, which a project that had declared no repositories could not 
do.
+        project.pluginManager.withPlugin(NATIVE_IMAGE_PLUGIN) {
+            configureNativeMetadataTask(project)
+        }
+    }
+
+    private void configureNativeMetadataTask(Project project) {
+        SourceSet sourceSet = SourceSets.findMainSourceSet(project)
+
+        TaskProvider<GenerateNativeMetadataTask> metadataTask = 
project.tasks.register(
+                'generateNativeMetadata', GenerateNativeMetadataTask) { 
GenerateNativeMetadataTask task ->
+            task.group = BasePlugin.BUILD_GROUP
+            task.description = 'Records the application classes and pages a 
native image must keep'
+            // The classes directory is written by more than one task, so the 
dependency has to be
+            // stated. It is stated against the compilation rather than the 
classes task, because
+            // the classes task also runs processResources, which consumes 
this task's output.
+            task.dependsOn(project.tasks.named(sourceSet.compileJavaTaskName))
+            ['compileGroovy', 'copyAstClasses'].each { String name ->
+                if (project.tasks.findByName(name)) {
+                    task.dependsOn(project.tasks.named(name))
+                }
+            }
+            task.classesDirs.from(sourceSet.output.classesDirs)
+            
task.pageClassesDirs.from(project.layout.buildDirectory.dir('gsp-classes/main'))

Review Comment:
   `709989ae37`. Removed `processResources.from(metadataTask)`. The task now 
depends on `compileGroovyPages` via `pluginManager.withPlugin` on the GSP 
plugin id, and its output is consumed by `bootJar` into `BOOT-INF/classes` — 
where `GroovyPagePlugin` already puts the compiled pages — and by the native 
binary classpath via `graalvmNative.binaries.configureEach { classpath(...) }`.
   
   Source-set output does not work: `classes` builds those, which restores the 
cycle from the other side.
   
   Added a `native-metadata-ordering` fixture and two tests in 
`GrailsNativeImageDefaultsSpec`: `compileGroovyPages` precedes 
`generateNativeMetadata` in the `bootJar` graph, and `processResources` no 
longer depends on the task while `bootJar` does. Both fail against the previous 
wiring.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -1115,6 +1265,146 @@ ${importStatements}
         }
     }
 
+    /**
+     * Wires up the cache the JDK can write for an application, so the next 
start reads what a
+     * training run worked out rather than working it out again.
+     *
+     * <p>Three steps, because the cache is only usable against the layout it 
was trained on: the
+     * archive is extracted, the extracted application is run and asked for 
its pages, and what the
+     * run recorded is left beside it. An application asks for this with
+     * {@code grails.aotCache.enabled}, and says which of its pages matter.</p>
+     */
+    protected void configureAotCache(Project project) {
+        AotCacheExtension extension = ((ExtensionAware) 
project.extensions.getByName('grails'))
+                .extensions.create('aotCache', AotCacheExtension)
+        extension.enabled.convention(false)
+        extension.paths.convention([])
+        extension.jvmArguments.convention(['-Dspring.aot.enabled=true', 
'-Dgrails.env=production'])
+        extension.port.convention(TRAINING_PORT)
+        
extension.startTimeoutSeconds.convention(TRAINING_START_TIMEOUT_SECONDS)
+
+        project.pluginManager.withPlugin(SPRING_BOOT_PLUGIN) {
+            TaskProvider<?> bootJar = project.tasks.named('bootJar')
+            Provider<Directory> application = 
project.layout.buildDirectory.dir('aot-cache/application')
+            Provider<JavaLauncher> launcher = trainingLauncher(project)
+
+            TaskProvider<ExtractApplicationTask> extract = 
project.tasks.register(
+                    'extractAotCacheApplication', ExtractApplicationTask) { 
ExtractApplicationTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Extracts the application, which is the 
form the cache is read against'
+                task.onlyIf { extension.enabled.get() }
+                task.archiveFile.set(archiveFileOf(bootJar))
+                // Mapped rather than read, so the JDK the toolchain resolves 
to is not provisioned
+                // by every build that merely reads this project.
+                task.javaExecutable.set(launcher.map { JavaLauncher java -> 
java.executablePath.asFile.absolutePath })
+                task.destination.set(application)
+            }
+
+            project.tasks.register('trainAotCache', TrainAotCacheTask) { 
TrainAotCacheTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Runs the application once so the JDK can 
write down what starting it needs'
+                task.onlyIf { extension.enabled.get() }
+                task.dependsOn(extract)
+                task.applicationDirectory.set(application)
+                task.archiveFileName.set(archiveFileOf(bootJar).map { 
RegularFile file -> file.asFile.name })
+                // Beside the extracted application rather than inside it. 
Inside, the cache and its
+                // metadata land in the directory this task declares as its 
input, so writing them
+                // changes that input and the task can never be up to date -- 
every build would run
+                // the application again to record what the last one already 
recorded.
+                Provider<Directory> beside = 
project.layout.buildDirectory.dir('aot-cache')
+                task.cacheFile.set(beside.map { Directory dir -> 
dir.file("${project.name}.aot") })

Review Comment:
   `0021e820a5`. `String cacheName = project.name` hoisted out of the 
`beside.map { }` closure.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -1115,6 +1265,146 @@ ${importStatements}
         }
     }
 
+    /**
+     * Wires up the cache the JDK can write for an application, so the next 
start reads what a
+     * training run worked out rather than working it out again.
+     *
+     * <p>Three steps, because the cache is only usable against the layout it 
was trained on: the
+     * archive is extracted, the extracted application is run and asked for 
its pages, and what the
+     * run recorded is left beside it. An application asks for this with
+     * {@code grails.aotCache.enabled}, and says which of its pages matter.</p>
+     */
+    protected void configureAotCache(Project project) {
+        AotCacheExtension extension = ((ExtensionAware) 
project.extensions.getByName('grails'))
+                .extensions.create('aotCache', AotCacheExtension)
+        extension.enabled.convention(false)
+        extension.paths.convention([])
+        extension.jvmArguments.convention(['-Dspring.aot.enabled=true', 
'-Dgrails.env=production'])
+        extension.port.convention(TRAINING_PORT)
+        
extension.startTimeoutSeconds.convention(TRAINING_START_TIMEOUT_SECONDS)
+
+        project.pluginManager.withPlugin(SPRING_BOOT_PLUGIN) {
+            TaskProvider<?> bootJar = project.tasks.named('bootJar')
+            Provider<Directory> application = 
project.layout.buildDirectory.dir('aot-cache/application')
+            Provider<JavaLauncher> launcher = trainingLauncher(project)
+
+            TaskProvider<ExtractApplicationTask> extract = 
project.tasks.register(
+                    'extractAotCacheApplication', ExtractApplicationTask) { 
ExtractApplicationTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Extracts the application, which is the 
form the cache is read against'
+                task.onlyIf { extension.enabled.get() }
+                task.archiveFile.set(archiveFileOf(bootJar))
+                // Mapped rather than read, so the JDK the toolchain resolves 
to is not provisioned
+                // by every build that merely reads this project.
+                task.javaExecutable.set(launcher.map { JavaLauncher java -> 
java.executablePath.asFile.absolutePath })
+                task.destination.set(application)
+            }
+
+            project.tasks.register('trainAotCache', TrainAotCacheTask) { 
TrainAotCacheTask task ->
+                task.group = BasePlugin.BUILD_GROUP
+                task.description = 'Runs the application once so the JDK can 
write down what starting it needs'
+                task.onlyIf { extension.enabled.get() }
+                task.dependsOn(extract)
+                task.applicationDirectory.set(application)
+                task.archiveFileName.set(archiveFileOf(bootJar).map { 
RegularFile file -> file.asFile.name })
+                // Beside the extracted application rather than inside it. 
Inside, the cache and its
+                // metadata land in the directory this task declares as its 
input, so writing them
+                // changes that input and the task can never be up to date -- 
every build would run
+                // the application again to record what the last one already 
recorded.
+                Provider<Directory> beside = 
project.layout.buildDirectory.dir('aot-cache')
+                task.cacheFile.set(beside.map { Directory dir -> 
dir.file("${project.name}.aot") })
+                task.metadataFile.set(beside.map { Directory dir -> 
dir.file('aot-cache.properties') })
+                task.javaExecutable.set(launcher.map { JavaLauncher java -> 
java.executablePath.asFile.absolutePath })
+                // Recorded from the JDK that will run the training, not the 
one running the build.
+                // A cache is read only by the JDK build that wrote it, and 
this is what a deployment
+                // checks that against -- so naming the wrong one is worse 
than naming none.
+                task.javaVersion.set(launcher.map { JavaLauncher java -> 
java.metadata.jvmVersion })
+                task.javaVendor.set(launcher.map { JavaLauncher java -> 
java.metadata.vendor })
+                task.jvmArguments.set(extension.jvmArguments)
+                task.paths.set(extension.paths)
+                task.port.set(extension.port)
+                task.startTimeoutSeconds.set(extension.startTimeoutSeconds)
+            }
+        }
+    }
+
+    /**
+     * The archive a task should consume, as something to be resolved when it 
runs.
+     *
+     * <p>A provider rather than a file: asking the task for its archive 
resolves the task, and a
+     * task resolved while the build is configured is one every build pays for 
whether or not it
+     * asked for an archive.</p>
+     */
+    private static Provider<RegularFile> archiveFileOf(TaskProvider<?> 
bootJar) {
+        bootJar.flatMap { Task task -> (Provider<RegularFile>) 
task.property('archiveFile') }
+    }
+
+    /**
+     * The JDK the cache will be trained on, which is the project's toolchain 
where it declares one.
+     *
+     * <p>A cache is read only by the JDK build that wrote it, so training has 
to happen on the JDK
+     * the application is compiled for rather than whichever one happens to be 
running Gradle. A
+     * project on the Java 21 baseline with a Java 25 toolchain compiles with 
25 and would otherwise
+     * have trained with 21, where the cache options do not exist -- and the 
failure it reported was
+     * that the training run ended before it started serving.</p>
+     *
+     * <p>Where no toolchain is declared this resolves to the JDK running the 
build, which is also
+     * the one that compiled the application.</p>
+     */
+    private static Provider<JavaLauncher> trainingLauncher(Project project) {
+        JavaToolchainService toolchains = 
project.extensions.getByType(JavaToolchainService)
+        JavaPluginExtension java = 
project.extensions.getByType(JavaPluginExtension)
+        toolchains.launcherFor(java.toolchain)
+    }
+
+    /**
+     * Records the application's own classes and pages so a native image keeps 
them usable. The build
+     * output already names both, so an application does not have to be traced 
to be buildable.
+     */
+    protected void configureNativeMetadata(Project project) {
+        // Only where an image is actually being built. The metadata is read 
by nothing else, and
+        // generating it means reading the compiled classes and the pages 
compiled into every
+        // dependency -- so wiring it into processResources unconditionally 
made a build that only
+        // wanted to write a resource compile its sources and resolve its 
whole runtime classpath
+        // first, which a project that had declared no repositories could not 
do.
+        project.pluginManager.withPlugin(NATIVE_IMAGE_PLUGIN) {
+            configureNativeMetadataTask(project)
+        }
+    }
+
+    private void configureNativeMetadataTask(Project project) {
+        SourceSet sourceSet = SourceSets.findMainSourceSet(project)
+
+        TaskProvider<GenerateNativeMetadataTask> metadataTask = 
project.tasks.register(
+                'generateNativeMetadata', GenerateNativeMetadataTask) { 
GenerateNativeMetadataTask task ->
+            task.group = BasePlugin.BUILD_GROUP
+            task.description = 'Records the application classes and pages a 
native image must keep'
+            // The classes directory is written by more than one task, so the 
dependency has to be
+            // stated. It is stated against the compilation rather than the 
classes task, because
+            // the classes task also runs processResources, which consumes 
this task's output.
+            task.dependsOn(project.tasks.named(sourceSet.compileJavaTaskName))
+            ['compileGroovy', 'copyAstClasses'].each { String name ->
+                if (project.tasks.findByName(name)) {

Review Comment:
   `0021e820a5`. `project.tasks.names.contains(name)`.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -836,6 +869,36 @@ ${importStatements}
                 it.destinationDirectory = 
project.layout.buildDirectory.dir('assetCompile/assets')
             }
         }
+        configureAssetsOnTheClasspath(project)
+    }
+
+    /**
+     * Packages the compiled assets where an executable jar can read them.
+     *
+     * <p>The asset pipeline plugin puts them at the root of whatever archive 
is built, which is
+     * where a war serves its web content from and is therefore right for a 
war. An executable jar
+     * has no web content: it serves assets by reading them off the classpath, 
and its classpath is
+     * {@code BOOT-INF/classes} -- so the same assets, at the same place, in a 
jar rather than a war,
+     * are packaged but unreachable, and every asset a page asks for is a 404 
while the page itself
+     * renders. Adding them under the classpath directory is what makes them 
found.</p>
+     *
+     * <p>Only for {@code bootJar}. A war already serves them from the root, 
and putting them on its
+     * classpath as well would ship the same bytes twice.</p>
+     */
+    private void configureAssetsOnTheClasspath(Project project) {
+        project.pluginManager.withPlugin(SPRING_BOOT_PLUGIN) {
+            // Matched by the task the pipeline registers rather than by the 
plugin that registers
+            // it: the asset pipeline's plugin id has changed once already, 
and the task name has
+            // not. Matched rather than looked up, so nothing depends on which 
plugin was applied
+            // first and no task is resolved to find out.
+            FileCollection compiledAssets = project.files(
+                    project.tasks.matching { Task task -> task.name == 
ASSET_COMPILE_TASK })

Review Comment:
   `e68da7d3db`. Used the `project.provider { }` over `tasks.names` option. The 
container is no longer in `bootJar`'s input files, and resolving them realizes 
only `assetCompile` rather than running the predicate against every registered 
task.
   
   Not the plugin-id route: `AssetClasspathPackagingSpec` registers a stand-in 
`assetCompile` because the asset-pipeline plugin is not on the test classpath, 
so keying off the id would leave that wiring unexercised. `project` is still 
reachable from the closure.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -83,6 +93,22 @@ import javax.inject.Inject
 @CompileStatic
 class GrailsGradlePlugin implements Plugin<Project> {
 
+    private static final String NATIVE_IMAGE_PLUGIN = 
'org.graalvm.buildtools.native'
+
+    private static final String SPRING_BOOT_PLUGIN = 'org.springframework.boot'
+
+    private static final String ASSET_COMPILE_TASK = 'assetCompile'
+
+    /** Where an executable jar reads its classpath from, and so where assets 
have to be to be found. */
+    private static final String CLASSPATH_ASSETS_PATH = 
'BOOT-INF/classes/assets'
+
+    private static final int TRAINING_PORT = 18080

Review Comment:
   `3890b3466e`. Added `RunPort.refuseWhereTaken(port, what, setting)`, which 
binds the port before the run starts and fails with the port and the setting 
that names it. Called from `TrainAotCacheTask` (`grails.aotCache.port`) and 
`TraceNativeMetadataTask` (`grails.nativeMetadata.port`). Added `RunPortSpec`.
   
   Bind-check rather than `--server.port=0`, which would remove a documented 
setting. It does not close the race between check and launch.
   



##########
grails-data-neo4j/grails-plugin/src/main/groovy/grails/neo4j/bootstrap/Neo4jDataStoreSpringInitializer.groovy:
##########
@@ -75,17 +74,27 @@ class Neo4jDataStoreSpringInitializer extends 
AbstractDatastoreInitializer {
             callable.delegate = delegate
             callable.call()
 
-            ApplicationEventPublisher eventPublisher
-            if (beanDefinitionRegistry instanceof 
ConfigurableApplicationContext) {
-                eventPublisher = new 
ConfigurableApplicationContextEventPublisher((ConfigurableApplicationContext) 
beanDefinitionRegistry)
-            } else {
-                eventPublisher = new DefaultApplicationEventPublisher()
-            }
+            // Registered rather than built here, so what the datastore holds 
is a reference the
+            // container can build. Holding the publisher itself puts a live 
object in the
+            // definition, and generating code for a definition means writing 
out what it holds --
+            // which a publisher bound to a running context is not.
+            //
+            // The choice is made here rather than through 
AbstractDatastoreInitializer, because this
+            // build resolves that class from a released 
grails-datamapping-core rather than from
+            // the source beside it, and a method added there is not one this 
can call.
+            grailsDatastoreEventPublisher(beanDefinitionRegistry instanceof 
ConfigurableApplicationContext
+                    ? ConfigurableApplicationContextEventPublisher
+                    : DefaultApplicationEventPublisher)
             final boolean isRecentGrailsVersion = 
GrailsVersion.isAtLeastMajorMinor(3, 3)
             neo4jConnectionSourceFactory(Neo4jConnectionSourceFactory) { bean 
->
                 bean.autowire = true
             }
-            neo4jDatastore(Neo4jDatastore, configuration, 
ref("neo4jConnectionSourceFactory"), eventPublisher, 
collectMappedClasses(DATASTORE_TYPE))
+            // The configuration is held rather than named. Naming the 
environment instead is what
+            // lets a definition be generated without writing the build 
machine's own settings into
+            // it, but that is asked through a method added to 
AbstractDatastoreInitializer, and this
+            // build resolves that class from a released 
grails-datamapping-core rather than from the
+            // source beside it -- so the call compiles here and goes missing 
at run time.
+            neo4jDatastore(Neo4jDatastore, configuration, 
ref("neo4jConnectionSourceFactory"), ref('grailsDatastoreEventPublisher'), 
collectMappedClasses(DATASTORE_TYPE))

Review Comment:
   `1ece28fbbb`. `getBeanDefinitions` calls 
`refuseWhereTheConfigurationWouldBeWrittenOut()`, which throws 
`IllegalStateException` when `AheadOfTimeProcessing.generatingCode`, naming 
Neo4j as not yet AOT-processable.
   
   Neither Neo4j change is compiled by anything: `grails-data-neo4j` is not in 
the root `settings.gradle`, is not an `includeBuild`, is referenced by no 
workflow, and its own build fails on 
`org.apache.grails:grails-gradle-plugins:6.1.2`, which does not exist. It pins 
`grailsVersion=6.0.0`, `groovyVersion=3.0.25`, `springBootVersion=2.7.18`, 
`springVersion=5.3.31`, `spockVersion=2.1-groovy-3.0`, 
`servletApiVersion=4.0.1`; 7 source files import `javax.*`, none import 
`jakarta.*`. So `processAot` is not currently reachable for Neo4j at all.
   



##########
grails-data-neo4j/grails-plugin/src/main/groovy/grails/neo4j/bootstrap/Neo4jDataStoreSpringInitializer.groovy:
##########
@@ -75,17 +74,27 @@ class Neo4jDataStoreSpringInitializer extends 
AbstractDatastoreInitializer {
             callable.delegate = delegate
             callable.call()
 
-            ApplicationEventPublisher eventPublisher
-            if (beanDefinitionRegistry instanceof 
ConfigurableApplicationContext) {
-                eventPublisher = new 
ConfigurableApplicationContextEventPublisher((ConfigurableApplicationContext) 
beanDefinitionRegistry)
-            } else {
-                eventPublisher = new DefaultApplicationEventPublisher()
-            }
+            // Registered rather than built here, so what the datastore holds 
is a reference the
+            // container can build. Holding the publisher itself puts a live 
object in the
+            // definition, and generating code for a definition means writing 
out what it holds --
+            // which a publisher bound to a running context is not.
+            //
+            // The choice is made here rather than through 
AbstractDatastoreInitializer, because this
+            // build resolves that class from a released 
grails-datamapping-core rather than from
+            // the source beside it, and a method added there is not one this 
can call.
+            grailsDatastoreEventPublisher(beanDefinitionRegistry instanceof 
ConfigurableApplicationContext

Review Comment:
   `1ece28fbbb`. The condition now also tests 
`resourcePatternResolver.resourceLoader instanceof 
ConfigurableApplicationContext`, so it agrees with `findEventPublisherClass`.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to