5

I have a groovy library that I publish as a jar file on a nexus repository. When I use extension methods in the library from another project's Gradle script, I get a MissingMethodException.

As a reference, say I have a String extension method, such as:

static boolean containsIgnoreCase(String self, String str) {
    self.toLowerCase().contains(str.toLowerCase())
}

If in my library I call the method using "foobar".containsIgnoreCase("Foo"), I will get the exception. If I instead call it using StringExtensions.containsIgnoreCase("foobar", "Foo"), it works, no problem.

My guess is that this is an issue with publishing the Groovy project without the META-INF file that defines the extensions. Here is the project structure:

- Library
  - src/main/
    - groovy/ 
      - (here are my sources)
    - resources/META-INF/groovy
      - org.codehaus.groovy.runtime.ExtensionModule (contains details about my extension classes)

My ExtensionModule file looks like this:

moduleName=string-extensions
moduleVersion=1.0
extensionClasses=com.my.project.StringExtensions

In my publish block in build.gradle, I use the following:

plugins {
    id 'groovy'
    id 'java'
    id 'maven-publish'
}

//...

sourceSets {
    main.groovy.outputDir = sourceSets.main.java.outputDir
    test.groovy.outputDir = sourceSets.test.java.outputDir
}

//...

publishing {

    publications {
        library(MavenPublication) {
            from components.java
        }
    }

    //...
}

What do I need to include in my publication in order to get the extension methods correctly registered when I use this library in another project's Gradle build script? I have added the classpath/repo url for the dependency in my buildscript, and can access methods of the library - these just fail when calling an extension method.

Phil
  • 35,852
  • 23
  • 123
  • 164
  • 1
    What does your org.codehaus.groovy.runtime.ExtensionModule file look like? And have you checked that it has been included in the published jar file? – emilles Jan 07 '21 at 18:35
  • 1
    could you please show how and where you want to use this extension? – daggett Jan 09 '21 at 16:11
  • @emilies I added the contents. – Phil Jan 11 '21 at 15:29
  • @daggett this is shown near the top. Essentially, I call a method of the plugin, which in-turn calls its own extension method. – Phil Jan 11 '21 at 15:30
  • @Phil, do you want to use it inside gradle or inside the code that gradle builds? – daggett Jan 11 '21 at 17:25
  • @daggett inside gradle. i want the gradle classpath to correctly register the extension methods. – Phil Jan 11 '21 at 20:29
  • how have you tried to add extension to the gradle classpath (it's just not to repeat false way) – daggett Jan 11 '21 at 20:38
  • @daggett it is in my buildscript dependencies - `buildscript { repositories { ... } dependencies { classpath 'com.myproject:library:+' }}` – Phil Jan 11 '21 at 20:42
  • you could define ext methods using metaclass. or ectensionmodule is principal? – daggett Jan 12 '21 at 03:04

2 Answers2

1

Bad news: there is a bug in Gradle because of which Gradle build scripts don’t support Groovy extension modules at the moment.

One thing to note: the Groovy docs seem to be wrong in that they specify to put the module descriptor under META-INF/groovy/. When I do that, I can’t even use the extension method of the resulting library in a plain Groovy application. I had to put the descriptor under META-INF/services/ instead to make it work.

That still doesn’t help with the Gradle use case. However, it shows that building and publishing works correctly: FWIW, I’ve just set up two tiny projects myself and I can reproduce the issue that you’re seeing (with Gradle 6.7.1). With the mentioned switch to the META-INF/services/ directory, I could at least get the extension to work in a Groovy application. So, given that you can access the Groovy methods as non-extension methods in your Gradle build, looking at the rest of your question and by switching to META-INF/services/, I would suppose that your build incl. the publication should be configured correctly, too.

Minimal Reproducer Projects

The following two minimal reproducer projects show that the publication works, that the library can be used in a Groovy application and that it fails in a Gradle build. Gradle Wrapper files are not shown.

├── my_extension_lib
│   ├── build.gradle
│   └── src
│       └── main
│           ├── groovy
│           │   └── MyExtension.groovy
│           └── resources
│               └── META-INF
│                   └── services
│                       └── org.codehaus.groovy.runtime.ExtensionModule
└── my_groovy_app
    ├── build.gradle
    └── src
        └── main
            └── groovy
                └── Test.groovy

First run ./gradlew publish under my_extension_lib/. Then run ./gradlew run under my_groovy_app.

my_extension_lib/build.gradle
plugins {
    id 'groovy'
    id 'maven-publish'
}

group = 'com.example'
version = '1.0'

repositories {
    jcenter()
}

dependencies {
    implementation 'org.codehaus.groovy:groovy-all:2.4.15'
}

publishing {
    publications {
        library(MavenPublication) {
            from components.java
        }
    }
    repositories {
        maven {
            name = 'test'
            url = 'file:///tmp/my_test_repo'
        }
    }
}
my_extension_lib/src/main/groovy/MyExtension.groovy
class MyExtension {

    static boolean containsIgnoreCase(String self, String str) {
        self.toLowerCase().contains(str.toLowerCase())
    }
}
my_extension_lib/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName=My Test
moduleVersion=1.0
extensionClasses=MyExtension
my_groovy_app/build.gradle
/* Test extension in Gradle */
buildscript {
    repositories {
        maven {
            url = 'file:///tmp/my_test_repo'
        }
        jcenter()
    }
    dependencies {
        classpath 'com.example:my_extension_lib:1.0'
    }
}

// works, i.e., the publication is ok
println(MyExtension.containsIgnoreCase("foobar", "Foo"))
// doesn’t work due to Gradle bug
//println("foobar".containsIgnoreCase('Foo'))


/* Test extension in Groovy application */

apply plugin: 'groovy'
apply plugin: 'application'

application {
    mainClass = 'Test'
}

repositories {
    maven {
        url = 'file:///tmp/my_test_repo'
    }
    jcenter()
}

dependencies {
    implementation 'org.codehaus.groovy:groovy-all:2.4.15'
    implementation 'com.example:my_extension_lib:1.0'
}
my_groovy_app/src/main/groovy/Test.groovy
class Test {

  static void main(String... args) {
      // works, i.e., extension library JAR was published correctly
      println("foobar".containsIgnoreCase('Foo'))
  }
}
Chriki
  • 15,638
  • 3
  • 51
  • 66
  • I am aware of this defect (I am one of the contributors on it), however I am interested in the solution. I believe that this is likely an issue with publication, not an actual issue with Gradle. Since there are no comments from Gradle contributors on the bug, I am hoping that it is user error, not an actual defect. Have you ensured the extension file is published with your plugin? – Phil Jan 11 '21 at 15:32
  • Three commentators on that ticket – melix, blindpirate and eskatos – are Gradle members. Given that they haven’t closed the ticket or commented negatively, I’d say they’ve acknowledged the bug. That also aligns with my further testing, see also my updated answer. Let me know if more details on the answer would be helpful, like a minimal reproducer showing that the publication works and can successfully be used at least in a Groovy project. – Chriki Jan 12 '21 at 09:06
  • can you include the link to your project(s) mentioned above, or show details of you implementation if they differ from mine? – Phil Jan 12 '21 at 17:10
  • I’ve added the minimal reproducer projects to my answer. – Chriki Jan 13 '21 at 09:13
  • 1
    thank you for this thorough walk-through! – Phil Jan 13 '21 at 14:41
1

not really and answer - just another portion of information.

groovy version 2.5+ takes care about both: META-INF/services and META-INF/groovy

link: https://github.com/apache/groovy/blob/1c358e84e427b3a6ff808533a93e1d76f4fa0d67/src/main/java/org/codehaus/groovy/runtime/m12n/ExtensionModuleScanner.java#L42

before groovy 2.5 only META-INF/services was processed

groovy scans resources named org.codehaus.groovy.runtime.ExtensionModule only once when statically creating MetaClassRegistry singleton in GroovySystem.java

link: https://github.com/apache/groovy/blob/1c358e84e427b3a6ff808533a93e1d76f4fa0d67/src/main/java/groovy/lang/GroovySystem.java#L37


to implement it somehow using this approach check how it's done in GrapeIvy.groovy class

link: https://github.com/apache/groovy/blob/1c358e84e427b3a6ff808533a93e1d76f4fa0d67/src/main/groovy/groovy/grape/GrapeIvy.groovy#L292


I think it's much easier to create a plugin for gradle and use metaClass to define custom methods

String.metaClass.up={ delegate.toUpperCase() }


task x{
    doLast{
        println( "hello".up() )
    }
}
daggett
  • 26,404
  • 3
  • 40
  • 56
  • Thanks for this! Hopefully they will address the defect soon. It is unfortunate that even Apache is looking for work-arounds for this issue in enterprise tools. Since I am the author of the library, I don't need to go this route. This definitely provides some great insight though! – Phil Jan 13 '21 at 14:43