Gradle and Maven reference
Common build tasks, adding dependencies, shading libraries and run-paper options.
This is the page to keep open while you work on your build files. It lists the commands you will run, shows how to add a library to your plugin, how to pack that library into your jar, how to tune the test server, and how to fix the build errors beginners meet most often. Every snippet is shown for Gradle and for Maven.
You do not have to read it from top to bottom. Use the headings like a menu. If a word is new, hover it: the build tools use a lot of their own vocabulary. For a slower tour of the files themselves, read Anatomy of a plugin project first.
What a build tool does
A build tool turns your folder of source files into a plugin jar you can drop on a server. It does four jobs, always in the same order:
- Find the libraries your code needs (above all the Paper API) and download them once.
- Compile your Java files into class files.
- Process resources: copy
plugin.ymland other files, filling in values such as the version. - Package everything into one jar.
Everything on this page is a way of controlling one of those four jobs.
Commands you will use
You run a build tool by typing a command in a terminal. In IntelliJ, open the terminal tool window at the bottom (Alt+F12) and it already sits inside your project folder. You can also click the same tasks in the Gradle tool window, as the next picture shows.
A Gradle command starts with .\gradlew.bat. That is the Gradle wrapper: a small script inside your project that downloads the exact Gradle version the project wants, so you never have to install Gradle yourself. Always use the wrapper, never a Gradle you installed some other way.
| Command | What it does | When to use it |
|---|---|---|
.\gradlew.bat build | Compiles, packs the jar and runs any tests. The jar lands in build\libs. | Every time you want a jar to install. |
.\gradlew.bat runServer | Builds your plugin, downloads Paper if needed and starts a test server in the run folder. | Testing while you code. |
.\gradlew.bat jar | Packs only the plain jar, without the other build steps. | Rarely; build is easier to remember. |
.\gradlew.bat clean | Deletes the build folder. | When old output confuses you. Use clean build for a fresh jar. |
.\gradlew.bat tasks | Lists every task this project understands. | When you wonder what else you can run. |
.\gradlew.bat dependencies | Prints the tree of libraries your project uses. | To see what ended up where (see below). |
.\gradlew.bat build --refresh-dependencies | Ignores the download cache and asks the repositories again. | When a library should exist but Gradle claims it does not, or a library was updated on the repository after Gradle saved its copy. |
.\gradlew.bat wrapper --gradle-version 9.8.0 | Changes which Gradle version the wrapper downloads. | To upgrade Gradle. Run any task afterward so it downloads. |
.\gradlew.bat cleanPaperCache | Deletes the Paper server jars that run-paper downloaded. | When the test server jar seems broken. |
A task is one named job. build is a task, and it quietly runs several smaller tasks first (compile, process resources, jar). Gradle prints each task name as it runs it, so you can follow along:
.\gradlew.bat build> Task :compileJava> Task :processResources> Task :classes> Task :jar> Task :assemble> Task :compileTestJava NO-SOURCE> Task :processTestResources NO-SOURCE> Task :testClasses UP-TO-DATE> Task :test NO-SOURCE> Task :check UP-TO-DATE> Task :buildBUILD SUCCESSFUL in 4sNO-SOURCE only means the task had nothing to do, because your project has no test files. UP-TO-DATE means nothing changed since the last run, so Gradle skipped the work. Both are good news, not errors.
| Command | What it does | When to use it |
|---|---|---|
mvn package | Compiles, copies resources and packs the jar. The jar lands in target. | Every time you want a jar to install. |
mvn clean package | Deletes target first, then does a full build. | When old output confuses you. |
mvn compile | Only compiles. No jar. | Quick check that the code compiles. |
mvn dependency:tree | Prints the tree of libraries your project uses. | To see what ended up where. |
mvn -U package | Forces Maven to check the repositories for updates. | When a library should exist but Maven claims it does not. |
Maven works in phases that always run in the same order: compile, then test, then package. Asking for package runs every phase before it too. Maven has no test server and no wrapper by default: you install Maven yourself (IntelliJ ships its own copy and uses it behind the scenes) and you copy the jar to a server by hand. See Build, install and test for that.
mvn package[INFO] --- compiler:3.15.0:compile (default-compile) @ my-plugin ---[INFO] --- jar:3.4.1:jar (default-jar) @ my-plugin ---[INFO] BUILD SUCCESSRepositories and dependencies
Your plugin uses code you did not write: the Paper API, and maybe a library for caching, JSON or databases. In build files, such a library is a dependency. A dependency has two parts: where to download it from (a repository) and which library you want (its coordinates).
Coordinates are three pieces of text that together name one exact file, like a postal address with street, house number and apartment:
- Group: who made it, written like a reversed web address. Example:
io.papermc.paper. - Name: the library itself. Example:
paper-api. - Version: which release. Example:
26.3.build.142-beta.
Maven calls them groupId, artifactId and version. You will see the whole thing written with colons: io.papermc.paper:paper-api:26.3.build.142-beta.
To add a library you do two things: make sure its repository is listed, then add one line to the dependencies. Most libraries live on Maven Central, the huge public repository that Gradle and Maven both know. Paper's own files live on the PaperMC repository, and many plugins publish their API on their own repository (the plugin's documentation tells you the address).
1repositories {2 mavenCentral()3 maven("https://repo.papermc.io/repository/maven-public/")4}5 6dependencies {7 compileOnly("io.papermc.paper:paper-api:$paperVersion")8 implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")9}- Adds Maven Central. The shortcut exists because nearly every library is there.
- Adds any other repository by its web address. Add as many as you need.
- Use this library to compile, but do not pack it into the jar, because the server already has it.
- Reads the value from
gradle.properties, so the version lives in one place. - Use this library to compile and also pack it into the jar. Which one you pick is the big decision, see the table below.
1<repositories>2 <repository>3 <id>papermc-repo</id>4 <url>https://repo.papermc.io/repository/maven-public/</url>5 </repository>6</repositories>7 8<dependencies>9 <dependency>10 <groupId>io.papermc.paper</groupId>11 <artifactId>paper-api</artifactId>12 <version>26.3.build.142-beta</version>13 <scope>provided</scope>14 </dependency>15 <dependency>16 <groupId>com.github.ben-manes.caffeine</groupId>17 <artifactId>caffeine</artifactId>18 <version>3.3.0</version>19 </dependency>20</dependencies>- Maven Central is built in, so you only list extra repositories.
- Use this library to compile but do not pack it into the jar, because the server already has it.
- No scope line means the default scope,
compile: use it and pack it.
compileOnly or implementation?
This single choice causes more beginner errors than any other line in a build file. Ask one question: will the server already have this code when my plugin runs?
| Gradle word | Maven word | Meaning | Use it for |
|---|---|---|---|
compileOnly | provided | Available while compiling. Not packed into your jar. | The Paper API and the API of another plugin that is installed on the server (Vault, LuckPerms, PlaceholderAPI). |
implementation | compile (the default) | Available while compiling. Packed into your jar when you shade it (next section). | A normal library the server does not have. |
A plugin API such as Vault is also compileOnly, but you must additionally tell Paper to load Vault first, with depend or softdepend in plugin.yml. That story is in Working with other plugins.
Seeing what you actually depend on
Libraries often need other libraries, which need others again. These are transitive dependencies. Print the tree to see them. The classpath is the list of libraries Java can use. Asking for runtimeClasspath shows the list your plugin needs while it runs, and notice that the Paper API is missing from it because it is compileOnly:
.\gradlew.bat dependencies --configuration runtimeClasspathruntimeClasspath - Runtime classpath of source set 'main'.\--- com.github.ben-manes.caffeine:caffeine:3.3.0 +--- org.jspecify:jspecify:1.0.1 \--- com.google.errorprone:error_prone_annotations:2.50.0With Maven the same question is mvn dependency:tree.
Shading: packing a library into your jar
Here is the trap. You added Caffeine with implementation, your code compiles, you copy the jar to the server, and the plugin crashes at startup with NoClassDefFoundError. The server has the Paper API, but nobody put Caffeine on the server, and a plain jar contains only your own classes.
Shading fixes this: the build tool copies the library's classes into your jar, so your plugin carries everything it needs. Gradle does it with the Shadow plugin and Maven with the Maven Shade plugin.
Relocation: avoiding clashes
One more problem. Imagine two plugins both shade Caffeine, but different versions. On a server, both would use the same class names, and one plugin would end up running the other's version. Relocation prevents this: while packing, the build tool renames the library's package. Caffeine's com.github.benmanes.caffeine becomes com.example.myplugin.libs.caffeine inside your jar, and it also rewrites your own code to match. Now only your plugin sees that copy.
Always relocate to a package inside your own, so the new name is unique to you.
1plugins {2 java3 id("xyz.jpenilla.run-paper") version "3.1.0"4 id("com.gradleup.shadow") version "9.6.1"5}6 7dependencies {8 compileOnly("io.papermc.paper:paper-api:$paperVersion")9 implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")10}11 12tasks {13 shadowJar {14 archiveClassifier = ""15 relocate("com.github.benmanes.caffeine", "com.example.myplugin.libs.caffeine")16 }17 18 build {19 dependsOn(shadowJar)20 }21}- Adds the Shadow plugin. It brings a new task called
shadowJar. - The shaded jar is normally named
my-plugin-1.0.0-all.jar. An empty classifier gives it the plain namemy-plugin-1.0.0.jar, so there is only one jar to pick up. - First the library's package, then the new name. Use a package inside your own.
- Makes
buildalso runshadowJar, so the jar inbuild\libsis always the shaded one.
Build, then look inside the jar to be sure. Only the shaded names should exist, and your classes should sit beside them:
.\gradlew.bat build jar tf build\libs\my-plugin-1.0.0.jarcom/example/myplugin/MyPlugin.classcom/example/myplugin/libs/caffeine/cache/Caffeine.classplugin.ymlGood to know: when the Shadow plugin is present, run-paper starts the test server with the shaded jar automatically, so runServer tests the same jar you will ship.
1<plugin>2 <groupId>org.apache.maven.plugins</groupId>3 <artifactId>maven-shade-plugin</artifactId>4 <version>3.6.2</version>5 <executions>6 <execution>7 <phase>package</phase>8 <goals>9 <goal>shade</goal>10 </goals>11 <configuration>12 <relocations>13 <relocation>14 <pattern>com.github.benmanes.caffeine</pattern>15 <shadedPattern>com.example.myplugin.libs.caffeine</shadedPattern>16 </relocation>17 </relocations>18 <createDependencyReducedPom>false</createDependencyReducedPom>19 </configuration>20 </execution>21 </executions>22</plugin>- Run the shade step as part of
package, right after the jar is made. - The library's package, exactly as it is now.
- The new name. Use a package inside your own.
- Stops Maven from writing an extra pom file you do not need.
After mvn package, the jar in target is the shaded one, and an untouched copy is kept as original-my-plugin-1.0.0.jar. Install the one without original- in its name.
Filling in plugin.yml automatically
Changing your version in three places is how bugs happen. Build tools can copy the version into plugin.yml for you. You write a placeholder in the file and the build replaces it while copying resources. This is what the paper-templates projects do.
1name: BuildInfo2version: '${version}'3main: com.example.refgradlemavenversion.BuildInfoPlugin4api-version: '${apiVersion}'- A placeholder. The build swaps it for the real version.
- Same here, for the Minecraft version.
1processResources {2 val properties = mapOf("version" to project.version, "apiVersion" to minecraftVersion)3 inputs.properties(properties)4 filteringCharset = Charsets.UTF_8.name()5 filesMatching(listOf("plugin.yml", "paper-plugin.yml")) {6 expand(properties)7 }8}- The names on the left are what you can write between
${and}in the file. The values come fromgradle.properties. - Only these two files are filled in. Other resources are copied untouched, which matters because a stray
$in a config file would break the expansion. - Does the swap.
1name: BuildInfo2version: '${project.version}'3main: com.example.refgradlemavenversion.BuildInfoPlugin4api-version: '${minecraft.version}'- Maven's built-in name for the
versionline of the pom. - A property you define yourself in the pom, see below.
1<properties>2 <minecraft.version>26.3</minecraft.version>3</properties>4 5<build>6 <resources>7 <resource>8 <directory>src/main/resources</directory>9 <filtering>true</filtering>10 </resource>11 </resources>12</build>- Your own property. Any name works; it must match the placeholder.
- Turns on placeholder replacement for every file in the resources folder.
To prove it works, here is a tiny plugin that prints what ended up in plugin.yml. getPluginMeta() gives you the data Paper read from the file, so you can check the build did its job without opening the jar:
Where this file livesref-gradle-maven-versionsrcmainjavacomexamplerefgradlemavenversionBuildInfoPlugin.java
The package com.example.refgradlemavenversion is the folder path com/example/refgradlemavenversion inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- ref-gradle-maven-version/
- src/main/
- java/com/example/refgradlemavenversion/Package com.example.refgradlemavenversion
- BuildInfoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/refgradlemavenversion/Package com.example.refgradlemavenversion
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.refgradlemavenversion;2 3import io.papermc.paper.plugin.configuration.PluginMeta;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class BuildInfoPlugin extends JavaPlugin {7 8 @Override9 public void onEnable() {10 PluginMeta meta = getPluginMeta();11 getLogger().info("Running " + meta.getName() + " " + meta.getVersion());12 getLogger().info("Written for Paper API " + meta.getAPIVersion());13 }14}- Returns the information Paper read from
plugin.yml: name, version, API version and more. - If the placeholder was never replaced, this would print the literal text
${version}. That is the sign that resource processing is not working. - The
api-versionline fromplugin.yml.
[14:02:11 INFO]: [BuildInfo] Enabling BuildInfo v1.0.0[14:02:11 INFO]: [BuildInfo] Running BuildInfo 1.0.0[14:02:11 INFO]: [BuildInfo] Written for Paper API 26.3Test server options (run-paper)
The runServer task comes from a Gradle plugin called run-paper. You configure it inside tasks { runServer { ... } }. Maven has no equivalent, but you can run a normal Paper server in a folder and copy your jar into its plugins folder after each mvn package.
1runServer {2 minecraftVersion(minecraftVersion)3 jvmArgs("-Xms2G", "-Xmx2G", "-Dcom.mojang.eula.agree=true")4 runDirectory.set(layout.projectDirectory.dir("test-server"))5 downloadPlugins {6 modrinth("luckperms", "v5.5.71-bukkit")7 }8}- Which Paper version to download and run. The value comes from
gradle.properties. - Options for the Java process.
-Xms2G -Xmx2Ggives the server 2 GB of memory. The last one accepts Minecraft's EULA for you, so only use it once you have read the EULA. - Where the server's world and config files go. The default is a folder called
run. Change it to keep several test worlds apart. - Downloads other plugins into the test server for you. Here LuckPerms from Modrinth. The second text is the version number shown on the plugin's Modrinth page.
| Option | What it does |
|---|---|
minecraftVersion("26.3") | Paper version to run. Required. |
jvmArgs(...) | Extra Java options: memory, system properties, debug flags. |
runDirectory | Folder the server runs in. Default run. |
downloadPlugins { ... } | Adds plugins from modrinth(id, version), hangar(name, version), github(owner, repo, tag, fileName) or url(address). |
pluginJars(...) | Adds jar files you already have on disk as extra plugins. |
cleanPaperCache (a task) | Deletes the downloaded Paper jars so they are fetched again. |
The test server section explains what happens when you press Run, and the Test Server tool shows what a running server looks like.
Common build errors and fixes
When a build fails, Gradle prints FAILURE: Build failed with an exception and then, under What went wrong, the reason. Read that part first, then find your message below. Maven prints lines starting with [ERROR]. More error messages, with plugin startup and runtime errors, are in the Error encyclopedia, and you can paste any of them into the Error Doctor.
Could not find paper-api
* What went wrong:Execution failed for task ':compileJava'.> Could not resolve all files for configuration ':compileClasspath'. > Could not find io.papermc.paper:paper-api:26.3.build.999-beta. Searched in the following locations: - https://repo.maven.apache.org/maven2/io/papermc/paper/paper-api/26.3.build.999-beta/paper-api-26.3.build.999-beta.pom - https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/26.3.build.999-beta/paper-api-26.3.build.999-beta.pomGradle looked in every repository you listed and found no such file. Check three things, in this order: the version number has a typo (the real one is 26.3.build.142-beta at the time of writing, and the update-paper-version.ps1 script in paper-templates picks the newest), the PaperMC repository line is missing from repositories, or you have no internet connection. Run again with --refresh-dependencies after fixing.
Cannot get non-null property
* Where:Build file 'C:\Users\you\IdeaProjects\my-plugin\build.gradle.kts' line: 8* What went wrong:Cannot get non-null property 'paperVersion' on root project 'my-plugin' as it does not existThe build file asks for a value that gradle.properties does not contain. Open gradle.properties and add the missing line (paperVersion=26.3.build.142-beta), or the file may have been deleted or saved in the wrong folder.
Plugin was not found
* What went wrong:Plugin [id: 'com.gradleup.shadow', version: '9.99.9'] was not found in any of the following sources:The plugin id or its version is wrong. Copy both exactly from the plugin's page on plugins.gradle.org. Old tutorials that say com.github.johnrengelman.shadow are the usual cause.
The wrong Java version
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project my-plugin: Fatal error compiling: error: release version 25 not supportedPaper 26.3 needs Java 25, and the build is running on an older Java. With Gradle the java.toolchain line downloads a matching JDK for you, so this error is rarer there. With Maven, or when IntelliJ uses the wrong JDK, open and pick a JDK 25 under SDK, then check that the terminal's java -version says 25. A similar message, Unsupported class file major version 69, means a tool is too old to read Java 25 class files: update that tool. The toolbox page shows how to install the JDK.
NoClassDefFoundError after the build succeeded
[14:02:11 ERROR]: Error occurred while enabling MyPlugin v1.0.0 (Is it up to date?)java.lang.NoClassDefFoundError: com/github/benmanes/caffeine/cache/CaffeineThe build was fine, but the server cannot find a library's class. You used implementation but did not shade, or you shaded and the jar you installed was the plain one. Build again, then open the jar (jar tf, shown above) and look for the library's classes. If you installed a jar named original-... or ...-all by mistake, install the other one.
Other quick fixes
- IntelliJ shows red code but the terminal build works. IntelliJ has not reloaded the project. Click the Gradle reload button.
- The jar is huge (megabytes of Paper classes). The Paper API is declared with
implementationorcompile. Change it tocompileOnlyorprovided. - The plugin shows version
${version}in/plugins. Resource processing is not replacing the placeholder: checkprocessResources(Gradle) orfiltering(Maven). - The build seems stuck on an old file. Run
clean build(Gradle) orclean package(Maven). - Weird characters in text after building. Set the encoding to UTF-8 as in the starter build file (
options.encodingfor Gradle,project.build.sourceEncodingfor Maven).
Practice
Shade a library and check your work
Add the Caffeine cache library to a plugin project, make sure it is packed into the jar under your own package name, and prove it by listing the jar contents.
Hint 1
Caffeine's coordinates are com.github.ben-manes.caffeine:caffeine:3.3.0. It is not on the server, so it must be implementation.
Hint 2
You need the Shadow plugin in plugins, a relocate call with Caffeine's package com.github.benmanes.caffeine, and build must depend on shadowJar.
Show the solution
The three changes from the shading section, together. Gradle first:
1plugins {2 id("com.gradleup.shadow") version "9.6.1"3}4 5dependencies {6 implementation("com.github.ben-manes.caffeine:caffeine:3.3.0")7}8 9tasks {10 shadowJar {11 archiveClassifier = ""12 relocate("com.github.benmanes.caffeine", "com.example.myplugin.libs.caffeine")13 }14 15 build {16 dependsOn(shadowJar)17 }18}Then run .\gradlew.bat build and jar tf build\libs\my-plugin-1.0.0.jar. You should see lines starting with com/example/myplugin/libs/caffeine/ and none starting with com/github/benmanes. In your Java code you still write the normal import, import com.github.benmanes.caffeine.cache.Caffeine;, because the relocation happens afterward, inside the jar.
Bump the version once
Put the BuildInfo plugin from above into a project that uses the placeholders (any paper-templates project does), then change the plugin version to 1.1.0 by editing only one line in one file. Rebuild, and confirm that both the jar name and the console message show the new version.
Hint
In Gradle the version lives in gradle.properties. In Maven it is the version line of the pom. plugin.yml is filled in automatically, so it must not be edited.
Show the solution
Change version=1.0.0 to version=1.1.0 in gradle.properties (Maven: <version>1.1.0</version> in pom.xml). Run the build again. The jar is now named after your project and the new version, for example build\libs\build-info-1.1.0.jar (Maven: target\build-info-1.1.0.jar) and the server prints:
[14:09:40 INFO]: [BuildInfo] Running BuildInfo 1.1.0One edit, three places updated: the jar name, the version inside plugin.yml, and what getVersion() returns.
Recap
- A build tool finds libraries, compiles, processes resources and packs the jar. Gradle uses
.\gradlew.bat build, Maven usesmvn package. - A dependency is a repository plus coordinates: group, name and version.
compileOnly(provided) is for code the server already has: the Paper API and other plugins.implementationis for libraries you ship.- Shading copies a library into your jar, and relocation renames its package so plugins cannot clash. Use the Shadow plugin
com.gradleup.shadowor the Maven Shade plugin. - Placeholders in
plugin.ymlare filled in byprocessResources(Gradle) or resource filtering (Maven), so the version lives in one place. - The run-paper plugin gives you
runServer, with options for the Minecraft version, memory, run folder and downloaded plugins. - When a build fails, read the What went wrong part first.
Quick quiz
You add the Paper API with
implementationinstead ofcompileOnly. What goes wrong?It still compiles, which is why the mistake is easy to make. The server already has the Paper API, so packing a second copy only bloats the jar and causes clashes.Your plugin uses a small library from Maven Central. The build succeeds but the server prints
NoClassDefFoundErrorfor the library's class. What is the likely fix?The server does not contain the library, and a plain jar holds only your classes.softdependis for other plugins, not for libraries.What does relocation do?
Relocation happens only inside the finished jar. Your source files stay untouched, and you keep writing the normal import.Where does Maven put the jar, and where does Gradle?
Therunfolder belongs to the Gradle test server and holds world data, not jars you ship.The server lists your plugin with the version
${version}written literally. What is wrong?The placeholder is only text untilprocessResources(Gradle) or filtering (Maven) swaps it for the real value while copying the file.
Next steps
- Anatomy of a plugin project explains every file the build uses, line by line.
- Build, install and test covers running the jar on a real server.
- Working with other plugins shows
compileOnlywith Vault and PlaceholderAPI in practice. - The error encyclopedia has the startup and runtime errors that come after the build.
- Releasing and sharing your plugin uses the shaded jar when you publish.