Gradle multi project build with empty projects
Asked Answered
P

3

18

When applying a multi-project Gradle structure to our project, my settings.gradle looks like this:

include "source:compA:api"
include "source:compA:core"
include "source:compB"

gradle projects give me

Root project 'tmp'
\--- Project ':source'
     +--- Project ':source:compA'
     |    +--- Project ':source:compA:api'
     |    \--- Project ':source:compA:core'
     \--- Project ':source:compB'

This is exactly the directory structure!
In my root directory I have a build.gradle which applies the java plugin to all subprojects:

subprojects {
    apply plugin: 'java'
}

When building I end up having artifacts for :source:compA which are empty because this is actually not a project just the subdirectories api and core are proper Java projects.

What's the best way to avoid having an empty artifact?

Pants answered 22/6, 2012 at 7:32 Comment(3)
Why don't you just apply the Java plugin to api and core in their respective scripts?Epigeal
I could do that, but my actual project structure is far more nested, with lots of projects. I think this is a common problem for legacy project structures. And I'm looking for a flexible solution, where each project don't need to know much about the build process it lives in.Pants
IMHO the submodule should explicitly know that it is a Java project. This will make it far more expressive to anyone looking at it than to declare this information on the root level.Epigeal
I
20

You can try using the trick they use in Gradle's own settings.gradle file. Note how each of the sub projects are located in the 'subprojects/${projectName}' folder, but the subprojects folder itself is not a project.

So in your case you'd do something like:

include "source:compA-api"
include "source:compA-core"
include "source:compB"

project(':source:compA-api').projectDir = new File(settingsDir, 'source/compA/api')
project(':source:compA-core').projectDir = new File(settingsDir, 'source/compA/core')

I have intentionally omitted the colon between compA and api to make sure source:compA does not get evaluated as a project container.

Alternatively, you can try excluding the source:compA project from having the java plugin applied to it, by doing something like:

def javaProjects() { 
  return subprojects.findAll { it.name != 'compA' }
} 

configure(javaProjects()) { 
  apply plugin: 'java' 
} 

Edit: Alternatively you can try something like this (adjust to your liking):

def javaProjects() { 
  return subprojects.findAll { new File(it.projectDir, "src").exists() }
} 

configure(javaProjects()) { 
  apply plugin: 'java' 
} 
Ida answered 2/7, 2012 at 14:37 Comment(3)
Thanks for you answer. I was hoping for something like a skipIfEmpty setting.Pants
You can achieve something equivalent to that by customising the findAll clause. See the edit above.Ida
That's a good solution. I still have to get used to the fact that the buildfiles are groovy scripts ;)Pants
H
2

Starting with Gradle 6.7, the Gradle user manual recommends against configuring subprojects using the "cross project configuration" feature using subprojects and allprojects:

Another, discouraged, way to share build logic between subproject is cross project configuration via the subprojects {} and allprojects {} DSL constructs. With cross configuration, build logic can be injected into a subproject and this is not obvious when looking at the subproject’s build script, making it harder to understand the logic of a particular subproject. In the long run, cross configuration usually grows complex with more and more conditional logic and a higher maintenance burden. Cross configuration can also introduce configuration-time coupling between projects, which can prevent optimizations like configuration-on-demand from working properly.

The suggested approach is to instead use convention plugins to define the common traits:

Gradle’s recommended way of organizing build logic is to use its plugin system. A plugin should define the type of a subproject. In fact, Gradle core plugins are modeled in the same way - for example, the Java Plugin configures a generic java project, while Java Library Plugin internally applies the Java Plugin and configures aspects specific to a Java library in addition. Similarly, the Application Plugin applies and configures the Java Plugin and the Distribution Plugin.

You can compose custom build logic by applying and configuring both core and external plugins and create custom plugins that define new project types and configure conventions specific to your project or organization. For each of the example traits from the beginning of this section, we can write a plugin that encapsulates the logic common to the subproject of a given type.

We recommend putting source code and tests for the convention plugins in the special buildSrc directory in the root directory of the project. For more information about buildSrc, consult Using buildSrc to organize build logic.


In your particular case, you could follow the approach given in Gradle's sample:

├── buildSrc
│   ├── build.gradle
│   ├── src
│   │   ├── main
│   │   │   └── groovy
│   │   │       ├── source.java-conventions.gradle

The buildSrc/build.gradle file would consist of just the groovy-gradle-plugin:

plugins {
    id 'groovy-gradle-plugin'
}

The buildSrc/src/main/groovy/source.java-conventions.gradle would contain the common logic for your Java projects. In your example, you just had the application of the Java plugin, but you would add any other commonality of the Java plugins that wouldn't be shared with non-Java projects:

plugins {
    id 'groovy-gradle-plugin'
}

Each Java project would then include the convention plugin:

plugins {
    id 'source.java-conventions'
}

Note that this doesn't buy much if literally the only thing that's common is the java plugin; you're replacing one plugin inclusion with another. But as soon as you end up with more shared build logic than that, it starts to pay off in terms of cross-project consistency & reduction of duplicated code.

Hazen answered 27/12, 2020 at 3:1 Comment(0)
S
1

I have the situation two. The empty parent directory is regarded as project. We can have some check to ignore the project.

project.getBuildFile().exists()
Siskin answered 4/2, 2021 at 10:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.