1. Overview
Pants is a build tool with support for more than one programming language. It’s a fast, scalable, and user-friendly build tool for codebases of all sizes. It’s good for polyglot projects with multiple languages.
In this tutorial, we’ll learn how to configure Pants, define BUILD files, add dependencies, test, and more using Java.
2. Pants Build Tool
Build tools are essential for automating the software development workflow. Maven and Gradle are popular in the Java ecosystem. However, they can struggle with a large polyglot codebase.
The Pants build tool provides the basic functionality of any build system that includes test runners, code generators, code compilers, dependency resolvers, packagers, etc.
This tool is good for a single codebase with multiple projects. Especially in a case where the projects are written in multiple programming languages. This is known as monorepo architecture.
Currently, it supports Python, Go, Java, Scala, and Shell. Support for more programming languages is in sight.
3. Initial Setup
It’s easy to set up the Pants project after installing its binary. It supports Windows, Linux, and MacOS.
To set up the Java Pants project, we’ll first create a directory for the project:
$ mkdir hello-pant
Next, we’ll change into the directory, create pants.toml file, and add a relevant backend for the project:
[GLOBAL]
backend_packages = [
"pants.backend.experimental.java",
]
Here, we instruct Pants that we’re working on a Java project. Since Pants is a good build tool for monorepo projects, we can configure pants.toml to provide support for more than one programming language:
[GLOBAL]
backend_packages = [
"pants.backend.experimental.java",
"pants.backend.experimental.scala",
]
In the configuration above, we configure Pants to provide support for Java and Scala.
After setting the language support, we can proceed to create the directory structure for our project. In the root directory, we’ll create the src directory where our code will reside:
$ mkdir –p src/com/baeldung/hellopant
Also, we’ll create a tests directory in the root folder:
$ mkdir –p test/com/baeldung/hellopant
Here, we’ll place our unit tests.
4. Compiling Code
Pants makes compiling code and generating a JAR file easy. In this section, we’ll explore how to compile, run tests and package our code to a JAR file with Pants.
4.1. Generating BUILD File
In the previous section, we created the package structure for a Java project. Next, let’s add a source file named Hello.java to the src directory:
public class Hello {
public static void main(String [] args) {
System.out.println ("Hello World!");
}
}
The program above contains the traditional “*Hello World!*“.
For Pants to execute any code, we need to generate a BUILD file by executing the tailor command:
$ pants tailor ::
The command above creates java_sources targets in every directory that contains library codes and junit_tests for test files:
Created src/com/baeldung/hellopant/BUILD:
- Add java_sources target hellopant
Created test/com/baeldung/hellopant/BUILD:
- Add junit_tests target tests
Without the BUILD file, the code won’t compile.
Here’s the content of the BUILD file for the test:
junit_tests(
name="tests",
)
Also, here’s the content of the BUILD file for code in the src folder:
java_sources(
)
Additionally, we need to generate a lock file to set the application to use the default JVM and manage third-party dependencies:
$ pants generate-lockfiles
The command above creates a new directory in the project root with a file called default.lock and a sub-folder named jvm.
Finally, let’s execute the Hello.java program:
pants run src/com/baeldung/hellopant/Hello.java
Here, we run the pants command to execute the code. A project can contain more than one target, we always need to specify the code we want to compile by specifying its path. After executing the command, it outputs “*Hello World!*” to the console.
4.2. Adding Third-Party Dependency
Adding a third-party dependency could be tricky – we need the default.lock to define third-party dependencies. In the previous section, we saw how to generate the default.lock file, which resides in the third-party folder.
Let’s understand this process by adding the Guava dependency to our initial setup. First, let’s modify the default.lock file:
# {
# "version": 1,
# "generated_with_requirements": [
# "com.google.guava:guava:18.0,url=not_provided,jar=not_provided",
# ]
# }
Here, we add the Guava dependency to the generated_with_requirements field. Next, we’ll create a new directory in the jvm directory that contains information about the dependency before generating it.
Let’s change to the jvm directory and make folders for Guava:
$ mkdir -p com/google/guava
Then, let’s add a BUILD file that contains the artifact information:
jvm_artifact(
group="com.google.guava",
artifact="guava",
version="32.1.1-jre",
)
This file contains the information needed to download the Guava dependency for use in the project. Notably, the folder structure is similar to the artifact group name. Any other dependency that starts with com should be defined in the com folder.
For example, let’s assume we want to add the Jackson Core dependency. Since its group name starts with com, we can change into the com folder and create the remaining folders before adding the BUILD file:
$ mkdir -p fasterxml/jackson/core
After defining the artifact information, we’ll execute the pants generate-lockfiles command:
$ pants generate-lockfiles
This command looks up the default.lock file and generates the dependencies defined in it.
In this case, it downloads the Guava dependency, and we can import it for use in the project.
4.3. Running Test
We can run a unit test by adding the Junit dependency to the project. Let’s update the default.lock file:
# "generated_with_requirements": [
# "junit:junit:4.13.2,url=not_provided,jar=not_provided",
# ]
Next, let’s create directories to define the information of the artifact in the jvm folder:
$ mkdir -p junit/junit
Here we make directories using the group and artifact names. Next, let’s create a BUILD file in the junit sub-folder:
jvm_artifact(
group="junit",
artifact="junit",
version="4.13.2"
)
Finally, let’s execute the generate command to download the new dependency:
$ pants generate-lockfiles
This updates the default.lock file with details about the added dependency.
Furthermore, let’s write a unit test to test the Joiner class from Guava library:
class GuavaUnitTest {
@Test
void whenConvertListToStringAndSkipNull_thenConverted() {
List<String> names = Lists.newArrayList("John", null, "Jane", "Adam", "Tom");
final String result = Joiner.on(",")
.skipNulls()
.join(names);
assertEquals(result, "John,Jane,Adam,Tom");
}
}
In the code above, we create an ArrayList of names and use Joiner to join the names and skip nulls.
To run the test using Pants, we’ll use the pants test command:
$ pants test test/com/baeldung/hellopant/GuavaUnitTest.java
We specify the path of the test file. Pants outputs the test result to the console whether it succeeds or fails:
The output above shows the test passed.
4.4. Resource Target
We can load files as resources by creating a resources folder in the src directory. Let’s create a resource folder in the src directory:
$ mkdir -p resource/com/baeldung/hellopant
This creates a resource folder with multiple subfolders. Next, let’s create word.txt file in the last subdirectory. The file contains the text “from Us”.
Then, let’s create a BUILD file in the directory containing the information about the resource file:
resources(name = "word", sources=["word.txt"])
The BUILD file has the name of the file and the path.
Next, let’s write a unit test to import the resource file in our code:
@Test
public void whenGettingTextFromAResourceFile_thenJoined() throws IOException {
String world = Resources.toString(Resources.getResource(GuavaUnitTest.class, "word.txt"), Charsets.UTF_8).strip();
String result = Joiner.on(" ").join("Hello", world);
assertEquals(result, "Hello from Us");
}
The code above reads the content of the resource file and joins it with “Hello“. Before we run the test, we need to add the resource target dependency to the test BUILD file:
junit_tests(
name="tests",
dependencies=[
"src/resources/com/baeldung/hellopant:word",
],
)
Here, we updated the test BUILD file to include the word.txt as a dependency for the test. Running the test with the dependency specified ensures success.
4.5. Packaging to JAR
Packaging a program to JAR file is easy. First, we need to define the target in the BUILD file in the src folder:
java_sources()
deploy_jar(
name="HelloPant",
main="com.baeldung.hellopant",
dependencies=[
":hellopant",
],
)
In the configuration above, we add a new attribute deploy_jar, and set the name of the JAR file to HelloPant. Also, we set the directory to look up the entry code.
To build the jar, we’ll run the pants package command:
$ pants package ::
Successful execution of the command creates a dist folder in the project root directory containing the JAR file:
We can now access the JAR file in the dist folder.
5. Working With an IDE
The Pants project can easily be loaded on IntelliJ IDEA via Build Server Protocol (BSP). By default, IntelliJ doesn’t provide BSP support. We need to install the Scala plugin which comes with BSP support.
Next, we need to create bsp-groups.toml file in the project root directory to add group configuration:
[groups.default]
addresses = [
"src/jvm::",
"tests/jvm::",
]
resolve = "jvm:jvm-default"
The configuration above specifies the directories to our code and resolves to use the default JVM.
Then, we’ll update the pants.toml file to create awareness about the BSP configuration:
[experimental-bsp]
groups_config_files = ["bsp-groups.toml"]
Finally, we can import the project into IntelliJ. It calls Pants to run the BSP server and synchronize the state to produce the IntelliJ module.
6. Other Functionalities
Pants is loaded with features to format code based on a specified standard, set a timeout for tests, and explicitly configure the JDK version for a project.
6.1. Lint and Format
We can add formatting style to Pants. Pants supports the Google Java format, which we can enable by updating packaeges_backend in pants.toml:
[GLOBAL]
backend_packages = [
"pants.backend.experimental.java",
"pants.backend.experimental.java.lint.google_java_format"
]
Here, we added a new backend package to provide linting and formatting support.
Let’s format the src directory of our setup project:
$ pants fmt test/com::
The command above formats the code in the test directory to use Google format style. We can check if the code is well formatted by executing the lint command:
$ pants lint ::
This checks through all the code files and ascertains if they’re well formatted.
6.2. Setting Timeout
Pants provides the functionality to set a timeout for tests. Any tests that take longer than the set timeout will be terminated. This is useful to prevent tests from hanging indefinitely.
To set the timeout, we’ll edit the BUILD file in the test directory:
junit_tests(
name="tests",
timeout=120,
)
Here, we set the timeout to 120 seconds. If the test fails to complete in time, it will be terminated.
We can also set the default timeout and maximum timeout in pants.toml:
[test]
timeout_default = 60
timeout_maximum = 120
This is applicable to all targets in the project. If a target set its timeout beyond the default, Pants uses the default timeout.
Additionally, we can disable timeout while running a test, especially in the case of debugging:
$ pants test --no-timeouts
The command above disables timeout restriction on tests.
6.3. Setting JDK Version
We can set the JDK for our project by configuring the jdk field in the pants.toml:
[jvm]
jdk = 1.8
This sets the JDK to Java 8. If this isn’t set, Pants resolves to the default JDK of the host machine.
7. Conclusion
In this article, we learned about the basic functionality of the Pants build tool. Additionally, we looked at its usage using the Java programming language. Also, we saw how to integrate it with an IDE. Pants is a good tool for a project base on the monorepo architecture.
As usual, the source code for the examples is available over on GitHub.