Bnd plugin broken in 2.14?

biz.aQute.bnd plugin stopped working with 2.14 releases. I’m not sure if it’s a problem with Gradle or with a plugin itself; I’m not plugins developer and skimming through the release notes for 2.14 doesn’t lead me to any hint.

I’d be grateful for a look at the situation.

Quoting from the issue filled against the plugin (https://github.com/bndtools/bnd/issues/1495):

The error appears during applying a plugin in a script (a copy of enRoute’s Gradle workspace):

subprojects {
  def bndProject = bndWorkspace.getProject(name)
  if (bndProject != null) {
    plugins.apply 'biz.aQute.bnd'
  }
}

The exception thrown:

org.gradle.api.GradleScriptException: A problem occurred evaluating root project 'osgi-bnd'.
        at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:93)
        at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl$2.run(DefaultScriptPluginFactory.java:177)
        at org.gradle.configuration.ProjectScriptTarget.addConfiguration(ProjectScriptTarget.java:77)
        at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:182)
        at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:38)
        at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:25)
        at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:34)
        at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:55)
        at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:540)
        at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:93)
        at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:42)
        at org.gradle.configuration.DefaultBuildConfigurer.configure(DefaultBuildConfigurer.java:35)
        at org.gradle.initialization.DefaultGradleLauncher$2.run(DefaultGradleLauncher.java:124)
        at org.gradle.internal.Factories$1.create(Factories.java:22)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:53)
        at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:121)
        at org.gradle.initialization.DefaultGradleLauncher.access$200(DefaultGradleLauncher.java:32)
        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:98)
        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:92)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:63)
        at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:92)
        at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:83)
        at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:99)
        at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)
        at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:48)
        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:30)
        at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:81)
        at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:46)
        at org.gradle.launcher.exec.DaemonUsageSuggestingBuildActionExecuter.execute(DaemonUsageSuggestingBuildActionExecuter.java:51)
        at org.gradle.launcher.exec.DaemonUsageSuggestingBuildActionExecuter.execute(DaemonUsageSuggestingBuildActionExecuter.java:28)
        at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:43)
        at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:173)
        at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:239)
        at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:212)
        at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:35)
        at org.gradle.launcher.cli.JavaRuntimeValidationAction.execute(JavaRuntimeValidationAction.java:24)
        at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
        at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
        at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:205)
        at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:169)
        at org.gradle.launcher.Main.doAction(Main.java:33)
        at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
        at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:55)
        at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:36)
        at org.gradle.launcher.GradleMain.main(GradleMain.java:23)
        at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:30)
        at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:129)
        at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)
Caused by: org.gradle.api.internal.plugins.PluginApplicationException: Failed to apply plugin [id 'biz.aQute.bnd']
        at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:153)
        at org.gradle.api.internal.plugins.DefaultPluginManager.addImperativePlugin(DefaultPluginManager.java:67)
        at org.gradle.api.internal.plugins.DefaultPluginContainer.apply(DefaultPluginContainer.java:55)
        at org.gradle.api.plugins.PluginContainer$apply.call(Unknown Source)
        at build_a5j7xbbue2r2xgv7zs7ymuj6p$_run_closure1.doCall(/home/jest/work/opti/r/core/osgi-bnd/build.gradle:32)
        at org.gradle.api.internal.ClosureBackedAction.execute(ClosureBackedAction.java:67)
        at org.gradle.util.ConfigureUtil.configure(ConfigureUtil.java:107)
        at org.gradle.api.internal.project.AbstractProject.configure(AbstractProject.java:888)
        at org.gradle.api.internal.project.AbstractProject.configure(AbstractProject.java:893)
        at org.gradle.api.internal.project.AbstractProject.subprojects(AbstractProject.java:876)
        at org.gradle.internal.metaobject.BeanDynamicObject$MetaClassAdapter.invokeMethod(BeanDynamicObject.java:374)
        at org.gradle.internal.metaobject.BeanDynamicObject.invokeMethod(BeanDynamicObject.java:169)
        at org.gradle.internal.metaobject.CompositeDynamicObject.invokeMethod(CompositeDynamicObject.java:96)
        at org.gradle.internal.metaobject.MixInClosurePropertiesAsMethodsDynamicObject.invokeMethod(MixInClosurePropertiesAsMethodsDynamicObject.java:30)
        at org.gradle.internal.metaobject.AbstractDynamicObject.invokeMethod(AbstractDynamicObject.java:163)
        at org.gradle.groovy.scripts.BasicScript.methodMissing(BasicScript.java:79)
        at build_a5j7xbbue2r2xgv7zs7ymuj6p.run(/home/jest/work/opti/r/core/osgi-bnd/build.gradle:29)
        at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:91)
        ... 50 more
Caused by: java.lang.NullPointerException: Cannot set property 'project' on null object
        at aQute.bnd.gradle.BndPlugin$_apply_closure1.doCall(BndPlugin.groovy:64)
        at org.gradle.api.internal.ClosureBackedAction.execute(ClosureBackedAction.java:67)
        at org.gradle.util.ConfigureUtil.configure(ConfigureUtil.java:107)
        at org.gradle.api.internal.project.AbstractProject.configure(AbstractProject.java:888)
        at org.gradle.api.Project$configure.call(Unknown Source)
        at aQute.bnd.gradle.BndPlugin.apply(BndPlugin.groovy:42)
        at aQute.bnd.gradle.BndPlugin.apply(BndPlugin.groovy)
        at org.gradle.api.internal.plugins.ImperativeOnlyPluginApplicator.applyImperative(ImperativeOnlyPluginApplicator.java:35)
        at org.gradle.api.internal.plugins.RuleBasedPluginApplicator.applyImperative(RuleBasedPluginApplicator.java:43)
        at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:137)
        ... 67 more

The offending line: https://github.com/bndtools/bnd/blob/master/biz.aQute.bnd.gradle/src/aQute/bnd/gradle/BndPlugin.groovy#L64

Gradle Version: 2.14-rc-1 to 2.14-rc-5
Operating System: Linux
Is this a regression? If yes, which version of Gradle do you know it last worked for?
2.13

Perhaps this is related to https://docs.gradle.org/2.14-rc-5/release-notes#changes-to-internal-apis which mentions ConfigureUtil and ConfigureUtil appears in the stack trace as called from Project.configure.

The bnd plugin adds an extension to the Project:

  extensions.create('bnd', BndProperties, bndProject)

and per https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtensionAware.html:

// Extensions added with the extension container's create method are themselves extensible
assert project.custom instanceof ExtensionAware

So the failing line should work since the bnd extension is ExtensionAware:

bnd.ext.project = bndProject

It would appear that Gradle 2.14 is failing to make the created extension ExtensionAware.

Thanks. We’re looking into it.

The BndProperties extension is getting decorated with the appropriate ExtraPropertiesExtension, but there have been some changes to how dynamic properties are looked-up (to make the look-up faster). What I’m seeing is that we’re checking BndProperties for dynamic properties (calling propertyMissing) before we check if ext is available from somewhere else. Since BndProperties#propertyMissing always returns something, bnd.ext is returning null instead of the ExtraPropertiesExtension instance.

I just debugged that far myself! The presence of the propertyMissing method results in the problem since it is consulted for ext.

Unfortunately we aren’t going to be able to fix this for 2.14. Luckily, there’s a pretty simple fix.

The BndProperties extension should be fixed so that it doesn’t return null for an unknown property.

So this code: https://github.com/bndtools/bnd/blob/master/biz.aQute.bnd.gradle/src/aQute/bnd/gradle/BndProperties.groovy#L33-L39

Should be amended to be:

  String propertyMissing(String name) {
    String value = get(name)
    if (value == null) {
      value = get(name.replace('_', '.'))
    }
    if (value == null) {
        throw new MissingPropertyException(name, String)
    }
    return value
  }

So I tried that fix. However it does not work properly.

bnd.ext.project = bndProject
assert bnd.ext.project == bndProject
assert bnd.project == bndProject

fails the 2nd assertion

assert bnd.project == bndProject
         |   |       |  |
         |   |       |  aQute.libg
         |   |       false
         |   /Users/hargrave/git/bnd/aQute.libg
         aQute.bnd.gradle.BndProperties_Decorated@7dcb362

It seems in 2.13, the values in ext were consulted first. Now they are consulted after.

Adding the following to the start of propertyMissing works:

  def propertyMissing(String name) {
    if (name == 'ext') {
      throw new MissingPropertyException(name, String)
    }
    ExtraPropertiesExtension ext = extensions.extraProperties
    if (ext.has(name)) {
      return ext.get(name)
    }
    ...

It throws the exception when looking for ext and otherwise consults the extra properties before the bnd properties.

One problem I think you had before (and still have) is something like:

bnd.noSuchProperty gives you a null vs an error.

Another way to fix the problem is to add the properties that were added to ext directly to BndProperties instead. That way you’re not relying on the ordering of dynamic properties (ext) for properties you want to always exist (it’ll be faster too). If you do as Daz suggested, you should get errors when you reference undefined properties too.

The bnd extension is primarily a way to get access to the bnd properties which are in a Map object. Normally in a Map object, calling get on a non-existent property returns null. So I am trying to replicate this behavior for the bnd properties. I did add the project property directly to BndProperties since that must always exist.

I ended up implementing propertyMissing to replicate the pre-2.14 Gradle behavior where, effectively, my prior propertyMissing logic runs after consulting the ext properties and extensions. https://github.com/bndtools/bnd/blob/ae3b5e2d210931ca2287c789db873299616eece3/biz.aQute.bnd.gradle/src/aQute/bnd/gradle/BndProperties.groovy#L32-L49.

I see you added a note to the release notes to describe this issue for others. https://docs.gradle.org/2.14-rc-6/release-notes#changes-to-dynamic-property-look-up. That will help others in my situation understand the problem. It would be good if https://docs.gradle.org/2.14-rc-6/dsl/org.gradle.api.plugins.ExtensionAware.html also documented the order in which properties are searched to make sure future extension authors understand the order.

Thanks for your help.

No problem. Sorry to have caused you guys trouble.

I’ve updated the docs for ExtensionAware like you suggested: https://github.com/gradle/gradle/commit/2ffed4293af8f5dcded87ab267536b67e1c1f9d7

Thanks!