How to add local maven package to configuration artifacts?

My Producer project has a task :produce which creates a maven package in a local maven repository, let’s say “localMavenRepo/com/my/package/...”. The package holds important gradle module metadata and several artifacts.

My Consumer project needs to consume it. Right now it does so by declaring it as a module dependency (add("api", "com.my.package:my-artifact:1.0.0")). However I would like to use a project dependency instead, so that the Producer task is called whenever needed and can produce the artifact on the fly.

I’m having troubles doing so. I know how to do it for a single file: create an output configuration in Producer, then add the file as an artifact and specify the produce task in builtBy(), or use TaskProvider.map.

// Producer
val produce by tasks.registering { /* ... */ }
val output by configurations.registering
artifacts {
    add(output.name, file("my-artifact.jar")) { builtBy(produce) }
}

// Consumer
dependencies {
    add("api", project(":Producer", "output"))
}

This works very well (sorry for typos, just wrote this down without checking).

But how to do this for the whole maven package, instead of a single artifact file? The artifact API does not play very well with “external” content, even if local. I have tried to add an artifact with the folder “localMavenRepo/com/my/package/...” but it doesn’t work, Consumer just sees it as a directory, not as a maven package.

Can anyone help?

It doesn’t matter to me if Consumer pulls files from the configuration or from the local maven repository. I just want the Producer task to be invoked whenever it does so.

I suggest that you read the creating multi project build docs

You’ll do something like

project(':producer') {
   apply plugin: 'java'
   dependencies {
      api 'foo:bar:1.0'
   } 
} 
project(':consumer') {
   apply plugin: 'java'
   dependencies {
      api project(':producer') 
   } 
} 

@Lance you’re saying to let the plugin (java in your example) export artifacts properly through the default configuration and let the same plugin read them properly from the consumer.

But what if I’m not using the java plugin at all in the producer? The default configuration will have no artifacts, and the consumer dependency will fail. Imagine that my producer task is producing a maven hierarchy from an unspecified source:

com
- my
  - package
    - my-artifact
      - 1.0.0
        - my-artifact-1.0.0.jar
        - my-artifact-1.0.0.pom
        - my-artifact-1.0.0.module
    - my-artifact-ios
      - 1.0.0
        - my-artifact-ios-1.0.0.jar
        - my-artifact-ios-1.0.0.pom
        - my-artifact-ios-1.0.0.module
    - my-artifact-android
      - 1.0.0
        - my-artifact-android-1.0.0.jar
        - my-artifact-android-1.0.0.pom
        - my-artifact-android-1.0.0.module

These can be consumed from Consumer with add("api", "com.my.package:my-artifact:1.0.0") - then .pom / .module files will be read by Gradle and artifacts might be transformed or it might even end up selecting a different artifact (like my-artifact-ios or my-artifact-android) based on some condition, thanks to Gradle module metadata.

This is very different than exporting my-artifact-1.0.0.jar alone.

My question is, can I replicate this complex add("api", "com.my.package:my-artifact:1.0.0") behavior with add("api", project(":producer")), so that my task is called on resolution? What should I export in the producer outgoing configuration?

  • definitely not the jar alone
  • not the folder, won’t work
  • not all files, each one of them will be interpreted as a separate artifact

Basically, I’m struggling to have the project dependency project(":producer") be interpreted as a maven dependency. I can control what is exported because I control the producer, but I’m not able to control how it is interpreted.

If this is not possible, then I’d like to know how can I keep add("api", "com.my.package:my-artifact:1.0.0") but also call a task whenever it’s resolved. Thanks for answering by the way!

I think I better understand your use case now. Perhaps something like:

project(':producer') {
   apply plugin: 'base'
   task createMavenRepo {
      outputs.dir 'mavenRepo' 
      doLast {
         // create poms and jars in mavenRepo dir
      } 
   } 
   assemble.dependsOn 'createMavenRepo' 
} 
project(':consumer') {
   apply plugin: 'java'
   repositories {
      maven {
         url = file('../producer/mavenRepo') 
      } 
   } 
   compileJava.dependsOn ':producer:createMavenRepo' 
   dependencies {
      api 'com.my.package:my-artifact:1.0.0' 
   } 
} 

That’s closer to what I’d need, but still not enough… Because createMavenRepo is called in consumer only indirectly, when compiling, not at dependency resolution time. For example, if you manually resolve the api config (something like configurations.api.resolvedConfiguration), the producer task will not be called, while if we used a project() dependency, it would.

So this is where I’m stuck at right now…

The reason why a project(...) dependency works is because under the hood, Gradle is making the necessary task dependencies.

You’ll notice that I included the following

compileJava.dependsOn ':producer:createMavenRepo' 

This was assuming that the compileJava was the task which causes configurations.api to be resolved.

If you have other tasks that resolve configurations.api then you’ll need to configure it to dependsOn the producer. Eg:

task resolveAndPrint {
   dependsOn ':producer:createMavenRepo'
   doLast {
      println "configurations.api = ${configurations.api.files}"
   }
} 

Note: You can only resolve configurations.api in the execution phase, you can’t resolve it in the configuration phase (see build phases)

Another option for wiring the task graph dependencies is to use ConfigurableFileCollection.builtBy(…) to wire the Configuration to the task. This would mean you could use the Configuration as a task input instead of directly depending on the task. Eg:

def dummyFiles = files() 
dummyFiles.builtBy ':producer:createMavenRepo'
dependencies {
   api dummyFiles
   api 'com.my.package:my-artifact:1.0.0' 
} 
task resolveAndPrint {
   inputs.files configurations.api
   doLast {
      println "configurations.api = ${configurations.api.files}"
   }
} 

See here

The returned file collection maintains the details of the tasks that produce the files, so that these tasks are executed if this file collection is used as an input to some task.

@Lance unfortunately I’m using a nasty (but not replaceable at the moment…) plugin which does exactly this, resolves all configurations in target.afterEvaluate { } . So I don’t have a consumer task that I could hook into to add the correct dependency.

I liked your files().builtBy idea, but from my tests it seems that it works only the first time - then, the FileCollection result is probably cached somewhere? And the task is not invoked again, making the real dependency fail.

For example, if I delete the maven folder and sync again, I’d expect the task to be run, but it isn’t. This first-run behavior is very important for CI so I think I’m stuck again. Wonder if this is worth opening a GitHub issue. An ExternalModuleDependency.builtBy would be ideal, so that one can do:

api("com.package:module:version") {
    builtBy("produce")
}

Thanks for all the insight, if you have any other idea please let me know!

I’m using a nasty (but not replaceable at the moment…) plugin which does exactly this, resolves all configurations in target.afterEvaluate { }

afterEvaluate {} is in the configuration phase which is before any tasks have run so you must fix this if you want to proceed

I liked your files().builtBy idea, but from my tests it seems that it works only the first time - then, the FileCollection result is probably cached somewhere? And the task is not invoked again

I’m not entirely sure what you mean by “the first time”. A task will only ever run once per Gradle invocation. Assuming you’ve correctly configured the task inputs and task outputs for createMavenRepo then this task will be skipped if it’s up-to-date so it may not run at all.

For example, if I delete the maven folder and sync again, I’d expect the task to be run, but it isn’t

Are you still resolving the API configuration in afterEvaluate? As I said this won’t work, you need to move this to the execution phase to make use of the task graph

if you have any other idea please let me know

If you really, really, really need to resolve the configuration in the configuration phase (eg afterEvaluate) then you’ll need to move the task which creates the maven repository into a seperate build. This seperate build would probably create a “zip” artifact containing the maven repo. You could then include the seperate build into the main build via a Comopsite Build

1 Like

If you really, really, really need to resolve the configuration in the configuration phase

I don’t, it’s just a badly designed plugin, here if you’re curious https://github.com/kezong/fat-aar-android/blob/master/source/src/main/groovy/com/kezong/fataar/FatLibraryPlugin.groovy#L57-L58 .

I understand what you are saying, for this reason I was hoping to make a project() dependency work… Which wouldn’t have this issue as far as I understand. But that’s apparently a big headache as well (replicate .pom/.module behavior with file artifacts, pass correct attributes…), so I guess I will consider re-writing this FatLibraryPlugin. Thanks a lot !

I was hoping to make a project() dependency work… Which wouldn’t have this issue

A project() dependency is a little different to your use case. With a project() dependency it’s a single jar file and the path to the file is actually known in the configuration phase (before the jar exists) which may allow you to do more in the configuration phase. It’s also likely that the transitive dependencies are defined in the configuration phase too for a project dependency.

In your use case you generate pom files in the execution phase, so the both the jar paths and the transitive dependencies aren’t known until execution phase

I guess I will consider re-writing this FatLibraryPlugin

I was hoping you might say that :slight_smile: