How do I include Groovy Extension methods in jar?

Viewed 625

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.

2 Answers

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'))
  }
}

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() )
    }
}
Related