How to generate, compile, jar, and depend on a gradle module
Asked Answered
P

2

6

I have a Java Gradle project that uses an OpenAPI specified API. I've used the org.openapi.generator plugin which generates sources as well as a complete Gradle module.

I expect that there's a way to define the generate, compile, jar steps such that I can have other modules depend on the generated module.

I.e.

# api/build.gradle:
plugins {
    id 'java'
    id "org.openapi.generator" version "5.0.0"
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation group: 'junit', name: 'junit', version: '4.12'
}

compileJava.dependsOn "openApiGenerate"

openApiGenerate {
    generatorName = "java"
    inputSpec = "$projectDir/src/main/openapi/spec.yaml".toString()
    outputDir = "$buildDir/generated"
    apiPackage = "com.example.api"
    invokerPackage = "com.example.api.invoker"
    modelPackage = "com.example.api.model"
    configOptions = [
            dateLibrary: "java8",
            library    : "native"
    ]
    groupId = "com.example"
    id = "api"
}

gradlew api:openApiGenerate generates (extraneous files elided):

api/build/generated/
├── build.gradle
├── pom.xml
├── settings.gradle
└── src
    ├── main/java/...
    └── test/java/...

Is there some way I can delegate-to, include, or depend on this generated module from other modules in the project? The generated module has a reliable group:artifact:version coordinate.

I.e. I'd like to be able to specify com.example:api:1.0 elsewhere in the project.


I've had a read through of https://docs.gradle.org/current/userguide/composite_builds.html as that seemed to be close to what I expect, but I am new to Gradle and it was a little to deep.

I've tried overriding the main and test source sets in api/build.gradle but I dislike having to copy and paste the dependencies from the api/build/generated/build.gradle.

I found https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:dependency-types which includes a tantalizing example but falls down as it is a source-only dependency.

dependencies {
    implementation files("$buildDir/classes") {
        builtBy 'compile'
    }
}

I looked at this example but how do I depend on a project (api/build/generated/) that does not exist yet?

dependencies {
    implementation project(':shared')
}
Pullet answered 30/12, 2020 at 8:45 Comment(0)
M
2

Great question! I don’t have a perfect answer but hopefully the following will still help.

Suggested Approach

I would keep the builds of the modules that depend on the generated API completely separate from the build that generates the API. The only connection between such builds should be a dependency declaration. That means, you’ll have to manually make sure to build the API generating project first and only build the dependent projects afterwards.

By default, this would mean to also publish the API module before the dependent projects can be built. An alternative to this default would indeed be composite builds – for example, to allow you to test a newly generated API locally first before publishing it. However, before creating/running the composite build, you would have to manually run the API generating build each time that the OpenAPI document changes.

Example

Let’s say you have project A depending on the generated API. Its Gradle build would contain something like this:

dependencies {
    implementation 'com.example:api:1.0'
}

Before running A’s build, you’d first have to run

  1. ./gradlew openApiGenerate from your api project.
  2. ./gradlew publish from the api/build/generated/ directory.

Then A’s build could fetch the published dependency from the publishing repository.

Alternatively, you could drop step 2 locally and run A’s build with an additional Gradle CLI option:

./gradlew --include-build $path_to/api/build/generated/ …

Idea for Less Manual Work

I have thought quite a bit about this but didn’t come up with any complete solution – hence my imperfect suggestion above. Let me still summarize my idea for how this could work.

  • You would have a Gradle build which generates the API – similar to your api project. That build would also be committed to your VCS.
  • That build would publish the generated API, even if it wouldn’t produce it itself. Instead, it would somehow delegate to the Gradle build generated by the openApiGenerate task. The delegation would have to happen via a GradleBuild task.
    Here lies the crux: all information on dependencies and published artifacts would effectively have to be retrieved via the Gradle CLI. I doubt that that’s currently possible.
  • Projects that dependend on the API could then include the api-like Gradle project in a composite build without requiring the manual hassle from the approach above.
Murr answered 30/12, 2020 at 11:46 Comment(2)
adding to your answer, below code in gradle build file can be handy: sourceSets{ main{ java.srcDirs+=path_to/api/build/generated/ } }Thigh
Thanks, Sunil, I believe the OP has already tried that approach but ruled it out – quoting from the question: “I've tried overriding the main and test source sets in api/build.gradle but I dislike having to copy and paste the dependencies from the api/build/generated/build.gradle.”Murr
P
1

To expand on @Chriki's answer with what I've actually used:

  1. Define api/ as it's own project with an empty api/settings.gradle file.

    This tells gradle that it is a self-contained project.

  2. Define the api module with:

    # api/build.gradle
    plugins {
       id 'java'
       id "org.openapi.generator" version "5.0.0"
    }
    
    repositories {
       mavenCentral()
    }
    
    openApiGenerate {
       generatorName = "java"
       inputSpec = "$projectDir/src/main/openapi/specification.yaml"
       outputDir = "$buildDir/generated"
       apiPackage = "com.example.api"
       invokerPackage = "com.example.api.invoker"
       modelPackage = "com.example.api.model"
       configOptions = [
          dateLibrary: "java8",
          library    : "native"
       ]
       groupId = "com.example"
       id = "api"
       version = "1.0.0"
    }
    

    Note the group and id (and version) explicitly define its maven coordinate.

  3. Include the build with a substitution so that the dependents can just use its maven coordinate:

    # settings.gradle
    
    includeBuild('api/build/generated') {
        dependencySubstitution {
            substitute module('com.example:api') with project(':')
        }
    }
    

    ... and in some other module:

    # app/build.gradle
    
    dependencies {
       implementation group: 'com.example', name: 'api'
    }
    

    The main advantage of this over ./gradlew --include-build api/build/generated is that [my] IDE will 'link' it all up too.

  4. Generate the API library:

     ./gradlew --project-dir api/ openApiGenerate
    
  5. Build/run the main project:

     ./gradlew build
     ./gradlew run
    
Pullet answered 31/12, 2020 at 0:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.