Paul King created GROOVY-12683:
----------------------------------

             Summary: Extension modules whose classes need an optional 
dependency break all compilation when that dependency is missing (groovy-fx in 
the distribution)
                 Key: GROOVY-12683
                 URL: https://issues.apache.org/jira/browse/GROOVY-12683
             Project: Groovy
          Issue Type: Bug
          Components: Extension methods, Grape
            Reporter: Paul King


h3. Problem

The {{groovy-fx}} module added by GROOVY-11434 is an optional module, but it is 
still copied into {{lib}} of the binary distribution and the {{installGroovy}} 
task output. JavaFX is a compile-only dependency of that module and is not 
bundled. On a JDK without JavaFX, that one jar makes *every* compilation fail, 
even a trivial script:

{noformat}
$ groovy -e 'println "x".toUpperCase()'
SEVERE [groovy.ui.GroovyMain] Caught: {0}
java.lang.NoClassDefFoundError: Unable to configure 
org.apache.groovy.groovyfx.extensions.FXExtensions due to missing dependency 
javafx/beans/binding/NumberBinding
{noformat}

Removing {{groovy-fx-*.jar}} from {{lib}} makes the error go away.

h3. Cause

The module descriptor registers {{FXExtensions}}, whose method signatures 
mention JavaFX types. The runtime already tolerates an extension class it 
cannot link: {{SimpleExtensionModule.getMetaMethods}} catches {{LinkageError}} 
per class and logs a warning. The compile-time cache does not. 
{{AbstractExtensionMethodCache.scan}}, which backs both static type checking 
and the macro transform, builds a {{ClassNode}} for every extension class 
visible to the loader. When one class cannot be configured, the error escapes 
and stops the compilation. The macro transform is a global transform, so this 
happens for every script, not just ones that use {{@CompileStatic}}.

GroovyFX is the first module we ship whose descriptor-registered extension 
class depends on a compile-only dependency. A third-party extension module with 
an optional dependency fails in exactly the same way.

h3. Proposed fix

# *Backstop:* make {{AbstractExtensionMethodCache.scan}} catch {{LinkageError}} 
per extension class and log a warning that names the class, matching the 
runtime. A single unusable module can then no longer stop all compilation.
# *Declared prerequisites:* add an optional {{requiredClasses}} key to the 
extension module descriptor. If any listed class cannot be loaded from the 
loader that defines the module's extension classes, the module is deferred 
instead of loaded. It is not registered, a FINE message reports the deferral, 
and its descriptor is remembered per class loader.
# *Late activation:* after each {{@Grab}}, Grape (Ivy and Maven) re-checks the 
deferred modules. It registers any whose required classes are now loadable, 
using the existing GROOVY-5543 metaclass refresh. It then invalidates the 
compile-time extension method caches so static type checking and macros see the 
new methods. Grape does not invalidate those caches today, even for an ordinary 
grabbed extension module.
# groovy-fx declares {{requiredClasses=javafx.beans.binding.NumberBinding}} 
through the build-logic {{moduleDescriptor}} DSL.

Users who supply JavaFX through a build file, a JavaFX-bundling JDK or 
{{--module-path}} are unaffected: the probe succeeds at startup and the module 
registers as before.

h3. Class loader caveat

A jar in {{lib}} is defined by the launcher's {{RootLoader}}, so its classes 
can only link against classes that loader can see. A plain {{@Grab}} adds 
JavaFX to the script's child {{GroovyClassLoader}}, which the module cannot 
use. In that case the module stays deferred and a warning suggests 
{{@GrabConfig(systemClassLoader=true)}}, which works end to end. The same rule 
already applies to {{SceneGraphBuilder}} and every other class in the jar. The 
GroovyFX Grape recipe in the user guide should use {{systemClassLoader=true}}.

h3. Verified with a prototype

* A new forked-JVM test, {{DeferredExtensionModuleTest}}, covers four cases. A 
module is deferred, then activates for dynamic and {{@CompileStatic}} code. A 
module with its required class present loads normally. A module stays deferred 
when the class is visible only to a child loader. A module with an unlinkable 
signature no longer breaks compilation. That last case fails on master with the 
error above.
* On an installed build that has groovy-fx in {{lib}} and no JavaFX, plain and 
{{@CompileStatic}} scripts run.
* With {{@GrabConfig(systemClassLoader=true)}} and {{@Grab}} of 
{{javafx-base}}, the module activates, and {{SimpleIntegerProperty + 
SimpleIntegerProperty}} works both dynamically and under {{@CompileStatic}}.
* The existing extension-module, {{DgmMethodLookupTest}}, Grape, groovy-macro 
and groovy-fx tests still pass.

h3. Notes

* The change adds new public API, all {{@since 7.0.0}}: the descriptor key 
constant, two static methods on {{ExtensionModuleScanner}}, and 
{{AbstractExtensionMethodCache.invalidateAllCaches()}}. The scanner methods 
could be made internal instead.
* The descriptor key is additive. Older Groovy versions ignore it.
* A simpler stopgap for 7.0.0-alpha-1 is to exclude groovy-fx from the 
distribution's {{lib}}. That stops the breakage without the new mechanism.
* Separate cosmetic bug seen above: {{GroovyMain}} logs {{"Caught: {0}"}} 
without formatting, so the literal {{{0}}} is shown.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to