Skip to content

Latest commit

 

History

History
1340 lines (1059 loc) · 28.9 KB

File metadata and controls

1340 lines (1059 loc) · 28.9 KB

Multi-Module Projects

Spigradle supports Gradle multi-module projects, allowing you to organize your plugin code into multiple modules with shared dependencies and platform-specific implementations.

Table of contents

Why Multi-Module?

Multi-module projects offer several benefits for Minecraft plugin development:

  • Code Reuse: Share common code across multiple platform implementations (Spigot, BungeeCord, NukkitX)
  • Separation of Concerns: Isolate platform-specific code from business logic
  • API/Implementation Split: Expose a public API while keeping implementation details private
  • Better Organization: Manage large codebases with clear module boundaries
  • Independent Versioning: Version modules independently if needed
  • Cleaner Dependencies: Each module declares only the dependencies it needs

Common use cases:

  • Cross-platform plugins (Spigot + BungeeCord + Velocity)
  • Plugin suite with shared utilities
  • Plugin with public API for developers
  • Large plugins split into feature modules

Requirements

  • Gradle 9.0+ (the latest version is recommended)
  • Spigradle 4.0.2 or higher
  • Basic understanding of Gradle multi-module projects

To update your gradle wrapper:

gradlew wrapper --gradle-version 9.2.1 --distribution-type all

Project Structure

A typical multi-module Spigradle project structure:

my-plugin/
├── settings.gradle                # Declares all modules
├── build.gradle                   # Root build configuration
├── gradle.properties              # Shared properties (including version)
├── gradle/
├── └── libs.versions.toml         # your unique dependencies
├── common/                        # Shared code module
│   ├── build.gradle
│   └── src/main/java/...
├── spigot/                        # Spigot implementation
│   ├── build.gradle
│   └── src/main/java/...
├── bungee/                        # BungeeCord implementation
│   ├── build.gradle
│   └── src/main/java/...
└── api/                           # Public API (optional)
    ├── build.gradle
    └── src/main/java/...

Setup Guide

Step 1: Configure settings.gradle

Define all modules in your project.

Gradle supports importing multiple version catalogs from settings.gradle(.kts) via dependencyResolutionManagement { versionCatalogs { ... } }. See Gradle’s “Version Catalogs” docs for details. citeturn0search0

Groovy DSL:

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
    versionCatalogs {
        // For Spigot modules
        create('spigots') {
            from("io.typst:spigot-catalog:1.0.0")
        }
        // For BungeeCord modules
        create('bungees') {
            from("io.typst:bungee-catalog:1.0.0")
        }
        // For common plugins/dependencies (idea-ext, lombok, etc.)
        // NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
        create('commons') {
            from("io.typst:common-catalog:1.1.0")
        }
    }
}

rootProject.name = 'my-plugin'

include('common', 'spigot', 'bungee', 'api') // api is optional

// OPTIONAL: rename child projects.
// Purpose: archiveBaseName / Maven artifactId derived from project.name
// rootProject.children.each { p ->
//     p.name = "my-plugin-${p.name}"
// }

Kotlin DSL (settings.gradle.kts):

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
    versionCatalogs {
        // For Spigot modules
        create("papers") {
            from("io.typst:spigot-catalog:1.0.0")
        }
        // For BungeeCord modules
        create("bungees") {
            from("io.typst:bungee-catalog:1.0.0")
        }
        // For common plugins/dependencies (idea-ext, lombok, etc.)
        // NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
        create("commons") {
            from("io.typst:common-catalog:1.1.0")
        }
    }
}

rootProject.name = "my-plugin"

include("common", "spigot", "bungee", "api") // api is optional

// OPTIONAL: rename child projects.
// Purpose: archiveBaseName / Maven artifactId derived from project.name
// rootProject.children.forEach { p ->
//     p.name = "my-plugin-${p.name}"
// }

Important: ideaExt is only in the commons catalog (not in platform catalogs).

Step 2: Configure root build.gradle

Configure shared settings for all subprojects.

Important: Version is managed centrally in gradle.properties. Do not set version = "1.0.0" (or similar) in build scripts.

Groovy DSL
plugins {
    id 'base'
}

// You can configure shared settings here (for example, repositories and dependencies),
// but I recommend configuring them explicitly in each submodule.
allprojects {
    group = 'com.example.myplugin'
    // version is managed by gradle.properties (do not set it here)
}
Kotlin DSL (build.gradle.kts)
plugins {
    id("base")
}

// You can configure shared settings here (for example, repositories and dependencies),
// but I recommend configuring them explicitly in each submodule.
allprojects {
    group = "com.example.myplugin"
    // version is managed by gradle.properties (do not set it here)
}

Example gradle.properties (root):

# Project version (centralized)
version=1.0.0

# External catalogs (centralized)
catalog.spigot.version=1.0.0
catalog.bungee.version=1.0.0
catalog.common.version=1.0.0

Example gradle/libs.version.toml (root):

[versions]
gson = "2.10.1"

[libraries]
gson = {group = "com.google.code.gson", name = "gson", version.ref = "gson"}

Step 3: Configure subproject build.gradle

Common module (common/build.gradle)

The common module has no platform-specific dependencies.

Groovy DSL
plugins {
    id 'java'
    // or: id 'org.jetbrains.kotlin.jvm' version '2.2.21'
}

repositories {
    mavenCentral()
}

// No Spigradle plugin needed for common module
dependencies {
    // Add common dependencies here
    compileOnly 'org.jetbrains:annotations:24.0.1'
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}
Kotlin DSL (common/build.gradle.kts)
plugins {
    id("java")
    // or: id("org.jetbrains.kotlin.jvm") version "2.2.21"
}

repositories {
    mavenCentral()
}

// No Spigradle plugin needed for common module
dependencies {
    // Add common dependencies here
    compileOnly("org.jetbrains:annotations:24.0.1")
}

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

Gradle’s Java toolchain configuration is done via the java { toolchain { languageVersion = JavaLanguageVersion.of(...) } } block. citeturn0search1

Spigot module (spigot/build.gradle)

Important: Platform plugins do not apply java automatically. Add java or org.jetbrains.kotlin.jvm (version 2.2.21) explicitly.

Groovy DSL
plugins {
    id 'java'

    // Spigradle plugin (via version catalog)
    alias(papers.plugins.spigot)

    // Optional (commons catalog only)
    // alias(commons.plugins.ideaExt)
    // alias(commons.plugins.lombok)
}

repositories {
    mavenCentral()

    // Repository shortcuts
    spigotRepos {
        papermc()
    }
}

dependencies {
    // Depend on common module
    implementation project(':common')

    // Paper API (from catalog)
    compileOnly papers.paper
}

spigot {
    apiVersion = '1.21'
    depend = ['Vault']

    commands {
        register('mycommand') {
            description = 'My command'
        }
    }
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

// include the common in jar
tasks.jar {
    dependsOn(':common:jar')
    from project(':common').sourceSets.main.output
}
Kotlin DSL (spigot/build.gradle.kts)
plugins {
    id("java")

    // Spigradle plugin (via version catalog)
    alias(papers.plugins.spigot)

    // Optional (commons catalog only)
    // alias(commons.plugins.ideaExt)
    // alias(commons.plugins.lombok)
}

repositories {
    mavenCentral()

    // Repository shortcuts
    spigotRepos {
        papermc()
    }
}

dependencies {
    // Depend on common module
    implementation(project(":common"))

    // Paper API (from catalog)
    compileOnly(papers.paper.api)
}

spigot {
    apiVersion = "1.21"
    depends = listOf("Vault")

    commands {
        register("mycommand") {
            description = "My command"
        }
    }
}

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

// include the common in jar
tasks.jar {
    dependsOn(":common:jar")
    from(project(":common").sourceSets.main.get().output)
}

BungeeCord module (bungee/build.gradle)

Important: Platform plugins do not apply java automatically. Add java or org.jetbrains.kotlin.jvm (version 2.2.21) explicitly.

Groovy DSL
plugins {
    id 'java'

    // Spigradle plugin (via version catalog)
    alias(bungees.plugins.bungee)

    // Optional (commons catalog only)
    // alias(commons.plugins.lombok)
}

repositories {
    mavenCentral()

    // Repository shortcuts
    bungeeRepos {
        sonatype()
        minecraftLibraries()
    }
}

dependencies {
    // Depend on common module
    implementation project(':common')

    // BungeeCord API (from catalog)
    compileOnly bungees.bungeecord
}

bungee {
    author = 'YourName'
    depends = ['SomePlugin']
}

// Shadow common module into final JAR
tasks.jar {
    dependsOn(':common:jar')
    from project(':common').sourceSets.main.output
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}
Kotlin DSL (bungee/build.gradle.kts)
plugins {
    id("java")

    // Spigradle plugin (via version catalog)
    alias(bungees.plugins.bungee)

    // Optional (commons catalog only)
    // alias(commons.plugins.lombok)
}

repositories {
    mavenCentral()

    // Repository shortcuts
    bungeeRepos {
        sonatype()
        minecraftLibraries()
    }
}

dependencies {
    // Depend on common module
    implementation(project(":common"))

    // BungeeCord API (from catalog)
    compileOnly(bungees.bungeecord)
}

bungee {
    author = "YourName"
    depends = listOf("SomePlugin")
}

// Shadow common module into final JAR
tasks.jar {
    dependsOn(":common:jar")
    from(project(":common").sourceSets.main.get().output)
}

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

Common Patterns

Common/Shared Module

The common module typically contains:

  • Business logic independent of platform
  • Data models and DTOs
  • Utility classes
  • Configuration handling
  • Database access layer
  • API interfaces

Key characteristics:

  • No Spigradle plugin applied
  • No platform-specific dependencies (Spigot, BungeeCord APIs)
  • Can use libraries like Gson, databases, etc.

Example structure:

common/
└── src/main/java/com/example/myplugin/
    ├── config/
    │   └── PluginConfig.java
    ├── model/
    │   └── PlayerData.java
    ├── service/
    │   └── DataService.java
    └── util/
        └── MessageFormatter.java

Platform-Specific Modules

Each platform module contains:

  • Platform-specific entry point (extends JavaPlugin/Plugin/PluginBase)
  • Platform API integrations
  • Event listeners
  • Command implementations
  • Platform-specific utilities

Example Spigot module:

spigot/
└── src/main/java/com/example/myplugin/spigot/
    ├── MySpigotPlugin.java      # extends JavaPlugin
    ├── listeners/
    │   └── PlayerJoinListener.java
    └── commands/
        └── MyCommand.java

Example BungeeCord module:

bungee/
└── src/main/java/com/example/myplugin/bungee/
    ├── MyBungeePlugin.java      # extends Plugin
    └── listeners/
        └── ServerConnectListener.java

API Module

For plugins that expose a public API for other developers:

Groovy DSL (api/build.gradle):

plugins {
    id 'java'
    // Optionally apply platform plugin via catalog if your API needs it.
    // alias(papers.plugins.spigot)
}

repositories {
    mavenCentral()
    spigotRepos { papermc() }
}

dependencies {
    // API typically doesn't depend on implementation
    compileOnly papers.paper  // Only if needed
}

// tasks.detectSpigotEntrypoints.enabled = false

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

Kotlin DSL (api/build.gradle.kts):

plugins {
    id("java")
    // Optionally apply platform plugin via catalog if your API needs it.
    // alias(papers.plugins.base)
}

repositories {
    mavenCentral()
    spigotRepos { papermc() }
}

dependencies {
    // API typically doesn't depend on implementation
    compileOnly(papers.paper.api)  // Only if needed
}

// tasks.detectSpigotEntrypoints.enabled = false

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

Other modules depend on the API:

Groovy DSL:

// spigot/build.gradle
dependencies {
    api project(':api')           // Expose API to consumers
    implementation project(':common')  // Hide implementation
}

Kotlin DSL:

// spigot/build.gradle.kts
dependencies {
    api(project(":api"))           // Expose API to consumers
    implementation(project(":common"))  // Hide implementation
}

Complete Examples

Cross-Platform Plugin Example

A complete example of a plugin that works on both Spigot and BungeeCord:

Project structure:

my-plugin/
├── settings.gradle
├── build.gradle
├── common/
│   ├── build.gradle
│   └── src/main/java/com/example/
│       ├── MyPluginConfig.java
│       └── MessageService.java
├── spigot/
│   ├── build.gradle
│   └── src/main/java/com/example/spigot/
│       ├── MySpigotPlugin.java
│       └── listeners/PlayerListener.java
└── bungee/
    ├── build.gradle
    └── src/main/java/com/example/bungee/
        ├── MyBungeePlugin.java
        └── listeners/ProxyListener.java

Common module code:

// common/src/main/java/com/example/MessageService.java
package com.example;

public class MessageService {
    public String formatMessage(String player, String message) {
        return "[MyPlugin] " + player + ": " + message;
    }
}

Spigot module code:

// spigot/src/main/java/com/example/spigot/MySpigotPlugin.java
package com.example.spigot;

import com.example.MessageService;
import org.bukkit.plugin.java.JavaPlugin;

public class MySpigotPlugin extends JavaPlugin {
    private MessageService messageService;

    @Override
    public void onEnable() {
        messageService = new MessageService();
        getLogger().info("Spigot plugin enabled!");
    }
}

BungeeCord module code:

// bungee/src/main/java/com/example/bungee/MyBungeePlugin.java
package com.example.bungee;

import com.example.MessageService;
import net.md_5.bungee.api.plugin.Plugin;

public class MyBungeePlugin extends Plugin {
    private MessageService messageService;

    @Override
    public void onEnable() {
        messageService = new MessageService();
        getLogger().info("BungeeCord plugin enabled!");
    }
}

Using build-logic for Shared Configuration

For complex multi-module projects, use build-logic composite build with convention plugins to share build logic. This is the modern approach recommended by Gradle over buildSrc.

Project structure:

my-plugin/
├── build-logic/
│   ├── settings.gradle.kts
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       ├── common-conventions.gradle.kts
│       ├── spigot-conventions.gradle.kts
│       └── bungee-conventions.gradle.kts
├── settings.gradle.kts
├── build.gradle.kts
└── ...modules...

Step 1: Configure build-logic/settings.gradle.kts:

rootProject.name = "build-logic"

versionCatalogs {
    // For Spigot modules
    create('spigots') {
        from("io.typst:spigot-catalog:1.0.0")
    }
    // For BungeeCord modules
    create('bungees') {
        from("io.typst:bungee-catalog:1.0.0")
    }
    // For common plugins/dependencies (idea-ext, lombok, etc.)
    // NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
    create('commons') {
        from("io.typst:common-catalog:1.1.0")
    }
}

Step 2: Configure build-logic/build.gradle.kts:

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
    gradlePluginPortal()
}

dependencies {
    // Add Spigradle plugin(s) to convention plugin classpath if needed
    implementation(papers.spigradleSpigot)
    implementation(papers.spigradleBungee)
}

Step 3: Create convention plugins:

build-logic/src/main/kotlin/common-conventions.gradle.kts:

plugins {
    id("java")
}

repositories {
    mavenCentral()
}

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

build-logic/src/main/kotlin/spigot-conventions.gradle.kts:

plugins {
    id("common-conventions")
    alias(papers.plugins.spigot)
}

repositories {
    spigotRepos { papermc() }
}

dependencies {
    compileOnly(papers.paper.api)
}

spigot {
    apiVersion = "1.21"
}

build-logic/src/main/kotlin/bungee-conventions.gradle.kts:

plugins {
    id("common-conventions")
    alias(bungees.plugins.bungee)
}

repositories {
    bungeeRepos {
        sonatype()
        minecraftLibraries()
    }
}

dependencies {
    compileOnly(bungees.bungeecord)
}

Step 4: Include build-logic in main settings.gradle.kts:

pluginManagement {
    includeBuild("build-logic")
}

rootProject.name = "my-plugin"

include("common", "spigot", "bungee", "api")

Step 5: Apply conventions in module build files:

common/build.gradle.kts:

plugins {
    id("common-conventions")
}

dependencies {
    compileOnly("org.jetbrains:annotations:24.0.1")
}

spigot/build.gradle.kts:

plugins {
    id("spigot-conventions")
}

dependencies {
    implementation(project(":common"))
}

spigot {
    depends = listOf("Vault")

    commands {
        register("mycommand") {
            description = "My command"
        }
    }
}

tasks.jar {
    dependsOn(":common:jar")
    from(project(":common").sourceSets.main.get().output)
}

bungee/build.gradle.kts:

plugins {
    id("bungee-conventions")
}

dependencies {
    implementation(project(":common"))
}

bungee {
    author = "YourName"
    depends = listOf("SomePlugin")
}

tasks.jar {
    dependsOn(":common:jar")
    from(project(":common").sourceSets.main.get().output)
}

Benefits of build-logic over buildSrc:

  • Faster builds: Changes to convention plugins don't invalidate the entire build cache
  • Better IDE performance: Gradle doesn't treat it as a special directory
  • Explicit dependency management: You control when convention plugins are rebuilt
  • Can be published as a separate artifact if needed

Dependency Management

Inter-Module Dependencies

Using implementation:

Groovy DSL:

// Hides the dependency from consumers
implementation project(':common')

Kotlin DSL:

// Hides the dependency from consumers
implementation(project(":common"))

Using api:

Groovy DSL:

// Exposes the dependency to consumers (requires java-library plugin)
api project(':api')

Kotlin DSL:

// Exposes the dependency to consumers (requires java-library plugin)
api(project(":api"))

Platform Dependencies

Each platform module should declare its own dependencies:

Groovy DSL:

// spigot/build.gradle
dependencies {
    implementation project(':common')
    compileOnly papers.paper
    implementation 'com.google.code.gson:gson:2.10.1'
}

// bungee/build.gradle
dependencies {
    implementation project(':common')
    compileOnly bungees.bungeecord
    implementation 'com.google.code.gson:gson:2.10.1'
}

Kotlin DSL:

// spigot/build.gradle.kts
dependencies {
    implementation(project(":common"))
    compileOnly(papers.paper.api)
    implementation("com.google.code.gson:gson:2.10.1")
}

// bungee/build.gradle.kts
dependencies {
    implementation(project(":common"))
    compileOnly(bungees.bungeecord)
    implementation("com.google.code.gson:gson:2.10.1")
}

Groovy DSL:

// In build.gradle
dependencies {
    implementation libs.gson
}

Kotlin DSL:

// In build.gradle.kts
dependencies {
    implementation(libs.gson)
}

Shadowing Dependencies

For Bukkit plugin, use the libraries option in plugin.yml instead of shadowJar:

Groovy DSL:

// spigot/build.gradle
plugins {
    id 'java'
    alias(papers.plugins.spigot)
}

// ...

dependencies {
    // compileOnlySpigot will be exported to plugin.yml libraries and compileOnly
    compileOnlySpigot libs.gson
}

// ...

Kotlin DSL:

// spigot/build.gradle.kts
plugins {
    id("java")
    alias(papers.plugins.spigot)
}

// ...

dependencies {
    // compileOnlySpigot will be exported to plugin.yml libraries and compileOnly
    compileOnlySpigot(libs.gson)
}

// ...

When using Shadow plugin to bundle dependencies:

Groovy DSL:

// bungee/build.gradle
plugins {
    id 'java'
    id("com.gradleup.shadow") version "9.2.2"
    alias(bungees.plugins.bungee)
}

shadowJar {
    // Relocate to avoid conflicts
    relocate 'com.google.gson', 'com.example.myplugin.lib.gson'
}

// Register the shadowJar task
tasks {
    assemble.dependsOn(shadowJar)
}

Kotlin DSL:

// bungee/build.gradle.kts
plugins {
    id("java")
    id("com.gradleup.shadow") version "9.2.2"
    alias(bungees.plugins.bungee)
}

tasks.shadowJar {
    // Relocate to avoid conflicts
    relocate("com.google.gson", "com.example.myplugin.lib.gson")
}

// Register the shadowJar task
tasks {
    assemble {
        dependsOn(shadowJar)
    }
}

Building Multi-Module Projects

Build All Modules

# Build all modules
./gradlew assemble

# Build specific module
./gradlew :spigot:assemble

# Clean and build
./gradlew clean assemble

# NOTE: build = build with test
# NOTE: assemble = build without test

Build Order

Gradle automatically handles build order based on dependencies:

  1. common module builds first
  2. spigot and bungee build after (since they depend on common)

Artifacts Location

Built JARs are located in each module's build/libs/ directory.

If you enabled the optional rootProject.children renaming in settings.gradle(.kts), the jar base names follow the renamed project names (useful for artifactId/archive naming). Otherwise, they follow the default module names.

Example (with renaming enabled):

my-plugin/
├── common/build/libs/my-plugin-common-1.0.0.jar
├── spigot/build/libs/my-plugin-spigot-1.0.0.jar
└── bungee/build/libs/my-plugin-bungee-1.0.0.jar

Distribution

For distribution, you typically only need platform-specific JARs:

  • my-plugin-spigot-1.0.0.jar (includes common code)
  • my-plugin-bungee-1.0.0-all.jar (includes common code through ShadowJar)

The my-plugin-common-1.0.0.jar is an intermediate artifact not needed for distribution.

Best Practices

1. Keep Common Module Platform-Agnostic

Good:

// common module
public interface MessageSender {
    void sendMessage(UUID playerId, String message);
}

Bad:

// common module - DON'T DO THIS
import org.bukkit.entity.Player; // Platform-specific!

public class MessageService {
    public void send(Player player, String msg) { }
}

2. Use Dependency Injection

Avoid static dependencies between modules:

Good:

// Spigot module
public class MySpigotPlugin extends JavaPlugin {
    private final MessageService messageService;

    public MySpigotPlugin() {
        this.messageService = new MessageService(new SpigotMessageSender());
    }
}

Bad:

// Using static references
MessageService.getInstance().send(...);

3. Version Consistency

Keep all modules at the same version by managing it centrally in gradle.properties:

version=1.0.0

4. Shared Resources

For shared resources (like messages, configs), include them in the common module and copy to platform modules:

Groovy DSL:

// spigot/build.gradle
processResources {
    from(project(':common').sourceSets.main.resources)
}

Kotlin DSL:

// spigot/build.gradle.kts
tasks.processResources {
    from(project(":common").sourceSets.main.get().resources)
}

5. Main Class Detection

Spigradle's main class detection works independently in each platform module:

  • spigot module: Detects class extending JavaPlugin
  • bungee module: Detects class extending Plugin
  • common module: No main class needed

6. Separate Test Dependencies

Each module can have its own test dependencies:

Groovy DSL:

// spigot/build.gradle
dependencies {
    testImplementation 'org.mockito:mockito-core:5.8.0'
    testImplementation project(':common')
}

// common/build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
}

Kotlin DSL:

// spigot/build.gradle.kts
dependencies {
    testImplementation("org.mockito:mockito-core:5.8.0")
    testImplementation(project(":common"))
}

// common/build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
}

Troubleshooting

Issue: Circular dependencies

Cause: Module A depends on Module B, and Module B depends on Module A

Solution: Restructure your modules to have a clear dependency hierarchy:

api → common → spigot/bungee

Issue: Main class not detected in platform module

Cause: Main class might be in common module instead of platform module

Solution: Ensure your JavaPlugin/Plugin class is in the platform-specific module:

✓ spigot/src/.../MySpigotPlugin.java extends JavaPlugin
✗ common/src/.../MyPlugin.java extends JavaPlugin

Issue: NoClassDefFoundError for common classes

Cause: Common module classes not included in final JAR

Solution: Add common module output to JAR:

Groovy DSL:

tasks.jar {
    from project(':common').sourceSets.main.output
}

Kotlin DSL:

tasks.jar {
    from(project(":common").sourceSets.main.get().output)
}

Or use Shadow plugin to bundle:

Groovy DSL:

shadowJar {
    from project(':common').sourceSets.main.output
}

Kotlin DSL:

tasks.shadowJar {
    from(project(":common").sourceSets.main.get().output)
}

Issue: Duplicate files in JAR

Cause: Multiple modules contribute the same resource files

Solution: Use duplicatesStrategy:

Groovy DSL:

tasks.jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from project(':common').sourceSets.main.output
}

Kotlin DSL:

tasks.jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from(project(":common").sourceSets.main.get().output)
}

See also