Share JAR artifact & its dependencies in multi-project build

Hello,

In the example build.gradle file below I’m struggling to understand why project app cannot access the JAR file produced by a dependent project lib & its dependencies:

allprojects {
    repositories {
        mavenCentral()
    }
}

// -----------------------------------------------------------------

project("lib") {
    apply plugin: 'java-library'

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(17)
        }
    }

    dependencies {
        api('org.springframework.boot:spring-boot:3.0.0')
    }
}

// -----------------------------------------------------------------

project('app') {
    apply plugin: 'java'

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(17)
        }
    }

    dependencies {
        implementation(project(':lib'))
    }

    task copyDependencies() {
        doFirst {
            copy {
                from(configurations.runtimeClasspath)
                into(file("$buildDir/dependencies"))
            }
        }
    }
}

If I run the command ./gradlew :app:copyDependencies --info it does not trigger the JAR task to run for the lib project & I can see that the copyDependencies task logs that the lib.jar was not found:

... snip ...

> Task :app:copyDependencies
Caching disabled for task ':app:copyDependencies' because:
  Build cache is disabled
Task ':app:copyDependencies' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
→ file or directory '/home/g-dx/Workspaces/gradle-issue/lib/build/libs/lib.jar', not found
:app:copyDependencies (Thread[included builds,5,main]) completed. Took 0.002 secs.

My first thought was to add the jar task output to the default configuration of the lib project like this:

artifacts {
  "default" jar
}

However, this makes no difference. The lib:jar task is never executed as part of calling app:copyDependencies.

I am aware of simple artifact sharing and the lib project dependency could be expressed as:

    dependencies {
        implementation(project(path: ':lib', configuration: 'archives'))
    }

This does trigger the lib:jar task to run, however doing this I lose access the lib project dependencies required to actually use the library, in this example spring-boot.

The only way I’ve found to make this work is to specify the dependency twice:

    dependencies {
        implementation(project(':lib'))
        implementation(project(path: ':lib', configuration: 'archives'))
    }

But this just seems wrong.

What am I missing here? Is it not possible to add the output JAR to the default configuration?

Besides that it is highly discouraged to do cross-project configuration like you do here using allprojects { ... } and project(...) { ... } due to various reasons and some other bad or obsolete practices like using apply instead of the plugins { ... } DSL, the answer to your actual question is, that you do not declare inputs / outputs of your copyDependencies task, so Gradle cannot know the task dependencies.

You just use the runtimeClasspath configuration at execution time in a copy closure.
If you would instead use a task of type Copy (or usually better one of type Sync) and configured it instead, the dependency would be there as expected.

tasks.register("copyDependencies", Sync) {
    from(configurations.runtimeClasspath)
    into(layout.buildDirectory.dir("dependencies"))
}

for example should have the proper implicit task dependency. (Code is written from the top of my head, so could have errors)

…the answer to your actual question is, that you do not declare inputs / outputs of your copyDependencies task, so Gradle cannot know the task dependencies.

Ah, thank you very much. This is fairly obvious in retrospect & using your code everything now works as expected.

Besides that it is highly discouraged to do cross-project configuration like you do here using allprojects { ... } and project(...) { ... } due to various reasons and some other bad or obsolete practices like using apply instead of the plugins { ... }

I would mention that the configuration presented was intentionally compressed into a single file to aid readability. My own multi-project setup doesn’t look like this at all.

However, I am now interested to learn more about bad or obsolete practices and how I can avoid them. Are there any good sources on this? Can I ask Gradle to warn me when I use something considered obsolete?

1 Like

Can I ask Gradle to warn me when I use something considered obsolete?

Unfortunately not.

I personally would say using Groovy DSL is obsolete, but that is highly subjective of course. :smiley:
Using cross-project configuration with allprojects, subprojects, project(...) and so on shouldn’t be used.
You should leverage task configuration avoidance APIs and lazy APIs wherever possible.
You should avoid afterEvaluate as if the devil were behind you.
You should avoid ext resp. extra properties.
Tasks should declare their inputs and outputs properly.
Tasks worth it should be cacheable.
I’d even try to be configuration cache compatible as it hopefully becomes stable with Gradle 8.

There are for sure some other points to keep in mind.
The problem is, that Gradle is still evolving relatively fast and things change.
What was best practice yesterday could be obsolete tomorrow,
and if a blog post or SO answer has a certain - not too high - age, it is quite questionable whether it should still be used. :-/

An interesting list of advice.

Thanks again for your input.

1 Like