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)