Prevent a module from being consumed by another module?

Hi. I have a Gradle build with a few hundred modules. They are split into two categories: shared libraries and applications. These are defined by convention plugins:

  • id("com.example.java-application") for applications
  • id("com.example.java-library") for shared libraries

We have a business rule that only shared libraries can be consumed by other modules. If the module is defined as an application we don’t want to be consumed by another application or a shared library. Unfortunately we haven’t found a way to enforce this in Gradle.

We first tried making the runtimeElements configuration not consumable in the application convention plugin like this:

configurations.named("runtimeElements").configure {
        isCanBeConsumed = false
    }

The above works for simple compiling but it breaks in situations like integration test suites where the module is imported to itself as a part of the dependencies of the test suite (see below).

register<JvmTestSuite>("integrationTest") {
    testType.set(TestSuiteType.INTEGRATION_TEST)
    useJUnitJupiter()

    dependencies {
        implementation(project(project.path)) // THIS BREAKS
    }
}

Does anyone know a way of preventing a module from being consumable by another module if it’s defined with a certain plugin ID (while preserving its ability to be imported in things like tests)?

I’m still having difficulties here.

I attempted to use variant attributes to identify and fail on the different plugin types. Here’s what I tried:

In the com.example.java-application plugin I defined an attribute com.example.attr.module-type with a value of application. I added it to the configurations that can be consumed.

val moduleTypeAttr = Attribute.of("com.example.attr.module-type", String::class.java)

configurations.configureEach {
    if (isCanBeConsumed) {
        attributes {
            attribute(moduleTypeAttr, "application")
        }
    }
}

In the com.example.java-library plugin I did the same thing but I changed the value of the attribute to shared-library:

val moduleTypeAttr = Attribute.of("com.example.attr.module-type", String::class.java)

configurations.configureEach {
    if (isCanBeConsumed) {
        attributes {
            attribute(moduleTypeAttr, "shared-library")
        }
    }
}

In both plugins I’ve added the attribute to the attributes schema:

dependencies {
    attributesSchema {
        attribute(moduleTypeAttr)
    }

    // ...
}

When I look at the outgoing variants I see my custom attribute and when I examine a dependency that should be disallowed it has the right attribute provided but there’s no attribute requested so the attribtue has no effect on the build.

> Task :example-service:dependencyInsight
project :example-bad-dependency
  Variant apiElements:
    | Attribute Name                       | Provided    | Requested    |
    |--------------------------------------|-------------|--------------|
    | com.example.attr.module-type         | application |              |  <<< Why is this blank???
    | org.gradle.category                  | library     | library      |
    | org.gradle.dependency.bundling       | external    | external     |
    | org.gradle.jvm.version               | 11          | 11           |
    | org.gradle.libraryelements           | jar         | classes      |
    | org.gradle.usage                     | java-api    | java-api     |
    | org.gradle.jvm.environment           |             | standard-jvm |

I’ve also tried adding an AttributeCompatibilityRule to the attribute but it isn’t even executing.

Are custom attributes even the right path towards solving my problem of preventing certain modules from being consumed by others? If so, what am I missing in this setup to make it work? Is there a better way than custom attributes? I’m in need of some guidance here. Thanks in advance.