jdaugherty commented on code in PR #16224:
URL: https://github.com/apache/grails-core/pull/16224#discussion_r3897906295


##########
grails-core/src/main/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformation.groovy:
##########
@@ -148,6 +150,10 @@ class GlobalGrailsClassInjectorTransformation implements 
ASTTransformation, Comp
                 pluginVersion = resolvePluginVersion(classNode, 
projectVersion?.toString())
                 addPluginVersionProperty(classNode, pluginVersion)
                 compileBeansDsl(classNode, source)
+                String generatedAutoConfigurationName = 
classNode.getNodeMetaData(
+                        
GrailsBeansASTTransformation.GENERATED_AUTO_CONFIGURATION_NAME_METADATA)
+                AutoConfigurationImportsWriter.register(

Review Comment:
   `compileBeansDsl` just above deliberately loads 
`GrailsBeansASTTransformation` reflectively and documents the 
`ClassNotFoundException` branch, but this hunk adds direct references - 
`GENERATED_AUTO_CONFIGURATION_NAME_METADATA` is built via `getName()`, so it is 
not inlined as a compile-time constant and the field read loads the class, as 
do the `AutoConfigurationImportsWriter` calls (including `reconcile`, which 
runs for every source unit). If grails-beans-dsl is ever absent from the 
compile classpath, every project source now fails with `NoClassDefFoundError`, 
where previously the beans property was just left alone. Either that situation 
is impossible, in which case the reflective load and its guard comment in 
`compileBeansDsl` are dead ceremony that could go, or it is possible, and these 
calls should take the same reflective route. Worth making the file tell one 
story either way.



##########
grails-beans-dsl/src/main/java/org/grails/compiler/beans/GrailsBeansASTTransformation.java:
##########
@@ -324,9 +330,26 @@ private ClassNode createAutoConfigurationSibling(ClassNode 
pluginClass, Annotati
         sibling.addAnnotations(siblingAnnotations);
         pluginClass.getAnnotations().removeAll(siblingAnnotations);
 
+        // The name is settled here and nowhere else. The global transform 
consumes this metadata
+        // and registers it using its Eclipse-aware compilation target 
resolution.
+        
pluginClass.putNodeMetaData(GENERATED_AUTO_CONFIGURATION_NAME_METADATA, 
siblingName);
+        if (!isEclipseSourceUnit(source)) {
+            AutoConfigurationImportsWriter.register(siblingName, 
targetDirectory(source), source, compilationUnit);

Review Comment:
   In the standard-compiler path this call and the global transform's 
metadata-driven `register` both run for the same descriptor in the same 
compilation: the implicit path reaches here through `compileBeansDsl`, and the 
global transform then registers again from the node metadata. The double write 
is absorbed by the short-circuit in `register`, but the hand-authored branch 
has no such guard, so a module whose hand-authored file is missing the 
generated class gets the warning emitted twice per compile. Reproduced by 
compiling a descriptor with an implicit DSL `beans` closure through a 
`CompilationUnit` with `targetDirectory` set and a hand-authored file listing 
only another class: `errorCollector.warnings` holds the identical message twice.
   
   Consider deduplicating per compilation, e.g. in the hand-authored branch of 
`register`, gate the warning on `registeredBy(compilation).add(className)` so 
the second call stays silent.



##########
grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy:
##########
@@ -336,7 +336,7 @@ class GlobalGrailsClassInjectorTransformationSpec extends 
Specification {
                     sourceFile,
                     '''
                         
@org.springframework.boot.autoconfigure.AutoConfiguration
-                        class DslBeansGrailsPlugin {
+                        class DslBeansGrailsPlugin extends 
grails.plugins.Plugin {

Review Comment:
   Adding `extends grails.plugins.Plugin` retargets this test at the sibling 
path, and the previous fixture's behavior - a descriptor not extending `Plugin` 
whose DSL-shaped `beans` closure is claimed onto the class itself - loses its 
only coverage here. Consider keeping the old fixture as its own test alongside 
this one; the non-Plugin case could also assert that no imports file is 
generated, which pins the 'only the sibling generated for a plugin descriptor 
is registered' scope.



##########
grails-beans-dsl/src/main/java/org/grails/compiler/beans/AutoConfigurationImportsWriter.java:
##########
@@ -0,0 +1,278 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.compiler.beans;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.WarningMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+/**
+ * Registers a generated auto-configuration in
+ * {@code 
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}.
+ *
+ * <p>The class a {@code beans} closure compiles to is created during 
compilation and is not a source
+ * file anyone can open. Leaving its registration to be written by hand made a 
plugin whose beans are
+ * silently never registered the ordinary consequence of not knowing the class 
exists - and the name
+ * to write is one only the compiler knows, since it follows from the 
descriptor's name and package.
+ * Writing it where the class is created is the only point at which that name 
is known for certain.
+ *
+ * <p>A module that keeps the file by hand at {@value 
#SOURCE_IMPORTS_LOCATION} keeps it: generating
+ * a second copy would put the same resource at the same path twice, and 
folding its entries into a
+ * copy under the build directory would lose them the moment anyone deleted 
the file that was, until
+ * then, where they were written down. Such a module is warned when the 
generated class is missing
+ * from it and is otherwise left alone, so nothing that builds today builds 
differently - deleting
+ * the hand-authored file is what opts in, and is safe once it holds nothing 
but what is generated.
+ *
+ * <p>That one conventional location is all a compiler can look in: a source 
set's resource
+ * directories are a build-tool notion and are not among the things the 
compiler is told, so a module
+ * that relocates them keeps a file this cannot see and gets a second copy 
generated, which the build
+ * then reports as two resources at one path. {@code FactoriesFileWriter} reads
+ * {@code META-INF/grails.factories} from the same fixed location for the same 
reason. Handling a
+ * relocated one needs the build to say where it is.
+ *
+ * <p>Hand-authored entries have to remain possible: a module may register a 
class from another jar,
+ * one annotated with a composed annotation, or one carrying no annotation at 
all, the imports file
+ * being the registration and {@code @AutoConfiguration} only supplying 
ordering.
+ *
+ * @since 8.0
+ */
+public final class AutoConfigurationImportsWriter {
+
+    public static final String IMPORTS_LOCATION =
+            
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
+
+    static final String SOURCE_IMPORTS_LOCATION = "src/main/resources/" + 
IMPORTS_LOCATION;
+
+    /** Set by the Grails Gradle plugin on the compiler's fork options; see 
GrailsAppBaseDirProvider. */
+    private static final String BASE_DIR_PROPERTY = "base.dir";
+
+    private static final String COMMENT_START = "#";
+
+    private static final String CLASS_FILE_EXTENSION = ".class";
+
+    /**
+     * What each compilation has registered so far, so an entry survives the 
pruning below before the
+     * class file backing it has been written - class generation runs long 
after this does, and two
+     * descriptors recompiling together would otherwise prune each other. 
Weakly keyed on the
+     * compilation, which is what makes the state per-build rather than 
per-JVM in a reused daemon.
+     */
+    private static final Map<Object, Set<String>> REGISTERED_BY_COMPILATION =
+            Collections.synchronizedMap(new WeakHashMap<>());
+
+    private AutoConfigurationImportsWriter() {
+    }
+
+    /**
+     * Adds {@code className} to the generated imports file under {@code 
targetDirectory}, together
+     * with anything an earlier source unit of the same compilation registered 
there. A module that
+     * keeps the file by hand is warned instead, and its file is left as the 
only one.
+     *
+     * @param className the generated auto-configuration's binary name
+     * @param targetDirectory the compilation output directory, or {@code 
null} when the compiler did
+     *                        not supply one - in which case there is nowhere 
to write and the class
+     *                        stays registerable by hand
+     * @param source the source being compiled, used for warnings and write 
errors
+     * @param compilation what scopes names registered before their class 
files are written
+     * @return {@code true} when the file was written
+     */
+    public static boolean register(String className, File targetDirectory, 
SourceUnit source, Object compilation) {
+        if (className == null || className.isEmpty() || targetDirectory == 
null) {
+            return false;
+        }
+
+        File sourceDirectory = findSourceDirectory(targetDirectory);
+        File handAuthored = sourceDirectory == null ? null : new 
File(sourceDirectory, SOURCE_IMPORTS_LOCATION);
+        if (handAuthored != null && handAuthored.isFile()) {
+            Set<String> handAuthoredEntries = new TreeSet<>();
+            readEntries(handAuthored, handAuthoredEntries);
+            if (!handAuthoredEntries.contains(className)) {
+                warn(source, className + " is generated from a beans closure 
but is not listed in " +
+                        SOURCE_IMPORTS_LOCATION + ", so Spring Boot will not 
read it. Add it there, or delete " +
+                        "that file once it holds nothing that is not generated 
and it will be written for you.");
+            }
+            write(new File(targetDirectory, IMPORTS_LOCATION), 
Collections.emptySet(), source);
+            return false;
+        }
+
+        Set<String> registeredHere = registeredBy(compilation);
+        registeredHere.add(className);
+
+        File importsFile = new File(targetDirectory, IMPORTS_LOCATION);
+        Set<String> entries = new TreeSet<>();
+        readEntries(importsFile, entries);
+
+        // A descriptor that was renamed, deleted, or given a different 
autoConfigurationName leaves
+        // an entry naming a class that is no longer generated, and Spring 
Boot fails to start on an
+        // auto-configuration it cannot load. Anything this compilation 
registered is kept regardless:
+        // its class file is written in a later phase than this one runs in.
+        entries.removeIf(entry -> !registeredHere.contains(entry) && 
!isGeneratedHere(targetDirectory, entry));
+
+        Set<String> written = new TreeSet<>(entries);
+        written.addAll(registeredHere);
+        if (written.equals(entries) && importsFile.isFile() && 
entries.contains(className)) {

Review Comment:
   This compares `written` against the post-prune set, so a prune that adds 
nothing new is never persisted. With the file holding 
`com.example.GreetingAutoConfiguration` and 
`com.example.StaleAutoConfiguration`, and neither class file present, 
`register('com.example.GreetingAutoConfiguration', ...)` drops the stale entry 
from `entries`, then finds `written.equals(entries)`, returns `false`, and 
leaves the stale entry on disk - the startup failure the comment above says 
this pruning prevents. In a Gradle build `reconcile` repairs it moments later, 
but callers that only go through the local transform (this class's own spec 
exercises exactly that path) keep the stale entry.
   
   Snapshot the on-disk contents before the `removeIf` and compare `written` 
against that instead; the `entries.contains(className)` clause then becomes 
redundant.



-- 
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