JVM Projects with Pants
Assuming you know the basic Pants concepts and have gone through the first Tutorial, you've made a great start towards using Pants to work with Java and Scala code. This page goes into some of the details.
If you are accustomed to the Maven tool and contemplating moving to Pants, you are not alone; Pants for Maven Experts has some advice.
Relevant Goals and Targets
When working with JVM languages, the following goals and targets are especially relevant.
Deployable Bundle Runnable Binary, optionally with non-JVM files
Deployable bundles are directories, optionally archived, that contain all files necessary to run the application. The
bundle
goal is used to create these deployable bundles from eitherjvm_binary
orjvm_app
targets.Bundling a
jvm_binary
target is appropriate when your application is entirely jar-based; that is, it's entirely class files and resources packaged inside the jars themselves. If your application requires "extra stuff" (e.g.: start scripts, config files) use ajvm_app
which allows you to include files in the bundle directory that supplement the binary jar and its dependencies. You can learn more about bundles.
Runnable Binary
On its own, a
jvm_binary
BUILD target describes an executable.jar
(something you can run withjava -jar
). The jar is described as executable because it contains a manifest file that specifies the main class as well as classpath for all dependencies. If your program contains only jars (and resources packaged in those jars), this is all you need to run the binary. Use./pants binary
to compile its code;./pants run
to run it "in place".
Importable Code
java_library
BUILD targets make Java source codeimport
-able. The rule of thumb is that each directory of.java
files has aBUILD
file with ajava_library
target. A JVM target that has ajava_library
in itsdependencies
can import its code.scala_library
targets are similar, but compiled with Scala.To use pre-built
.jar
s, a JVM target can depend on ajar
, a reference to published code; thesejar
s normally live in a directory called 3rdparty.Pants can
publish
a JVM library so code in other repos can use it; if the*_library
target has aprovides
parameter, that specifies the repo/address at which to publish.An
annotation_processor
BUILD target defines a Java library one containing one or more annotation processors.
Tests
A
junit_tests
BUILD target holds source code for some JUnit tests; typically, it would have one or morejava_library
targets as dependencies and would import and test their code.Pants also includes support for using the ScalaTest framework. The testing framework automatically picks up scala tests that extend the org.scalatest.Suite class and runs them using org.scalatestplus.junit.JUnitRunner.
Most other scala test frameworks support running with JUnit via a base class/trait or via a
@RunWith
annotation; so you can usejunit_tests
for your scala tests as well.The Pants
test
goal runs tests.
Generated Code
A
java_thrift_library
generates Java code from.thrift
source; a JVM target that has this target in itsdependencies
canimport
the generated Java code. Ajava_protobuf_library
is similar, but generates Java code from protobuffer source.
BUILD for a Simple Binary
The Pants Build Java hello world
sample
code shows the BUILD file for a simple Java binary (in the main/
directory):
jvm_binary(name = 'main-bin', dependencies = [ 'examples/src/java/org/pantsbuild/example/hello/greet', 'examples/src/resources/org/pantsbuild/example/hello', ], sources = ['HelloMain.java'], main = 'org.pantsbuild.example.hello.main.HelloMain', basename = 'hello-example', )
This small program has just one dependency. It is a library, a java_library
, a compiled set of
source code from this workspace.
Depending on a Library
The rule of thumb is that each directory of .java
or .scala
files
has a library target. If you find yourself thinking "we should move some
of this code to another directory," you probably also want to set up a
BUILD
file with a java_library
(or scala_library
) target. Here we
see the library target which main-bin
depends on. This library target
lives in hello/greet/BUILD
:
# Note that the target has no explicit name, so it defaults to the name # of the directory, in this case 'greet'. # It also has no explicit sources, so it defaults to the sources implied # by the target type, in this case "['*.java']". java_library( dependencies = [], # A more realistic example would depend on other libs, # but this "hello world" is pretty simple. provides = artifact(org='org.pantsbuild.example', name='hello-greet', repo=public,), )
This library could depend on other build targets and artifacts; if your
code imports something, that implies a BUILD
dependency.
A Test Target
The Pants Java Hello World example tests are normal JUnit tests. To run them with Pants, we need a target for them:
junit_tests( dependencies=[ 'examples/src/java/org/pantsbuild/example/hello/greet', 'examples/src/resources/org/pantsbuild/example/hello', ], )
As with other targets, this one depends on code that it imports. Thus, a typical test target
depends the library that it tests and perhaps some others (here, junit
).
The dependency on junit
is a "third party" dependency, a pre-compiled artifact whose source
lives somewhere outside the workspace.
Depending on a Jar
The test example depends on a jar, junit
. Instead of compiling from
source, Pants invokes the resolver to fetch such jars. To reduce the danger of
version conflicts, we use the 3rdparty idiom: we keep references to
these "third-party" jars together in BUILD
files under the 3rdparty/
directory. Thus, the test has a 3rdparty
dependency:
junit_tests( dependencies=[ 'examples/src/java/org/pantsbuild/example/hello/greet', 'examples/src/resources/org/pantsbuild/example/hello', ], )
The BUILD
files in 3rdparty/
have targets like:
jar_library(name='junit', jars = [ jar(org='junit', name='junit-dep', rev='4.11').with_sources(), ], dependencies = [ ':hamcrest-core', ], )
Those jar()
things are references to public jars.
You can read more about
JVM 3rdparty dependencies.
The Usual Commands: JVM
Make sure code compiles and tests pass:
Use the test
goal with the targets you're interested in. If they are test targets,
Pants runs the tests. If they aren't test targets, Pants still compiles them since it knows it
must compile before it can test.
$ ./pants test examples/src/java/org/pantsbuild/example/hello/:: examples/tests/java/org/pantsbuild/example/hello/::
Assuming you use junit_test
targets, output from the junit
run is written to
.pants.d/test/junit/
; you can see it on the console with --output-mode=ALL
:
$ ./pants test.junit --output-mode=ALL examples/tests/java/org/pantsbuild/example/hello::
Run just that one troublesome test class: (assuming a JUnit test; other frameworks use other flags)
$ ./pants test.junit --test=org.pantsbuild.example.hello.greet.GreetingTest examples/tests/java/org/pantsbuild/example/hello/::
Packaging Binaries
To create a bundle (a binary and its dependencies, perhaps including helper files):
$ ./pants bundle --bundle-jvm-archive=zip examples/src/java/org/pantsbuild/example/hello/main ...lots of build output... 08:50:54 00:02 [create-monolithic-jar] 08:50:54 00:02 [add-internal-classes] 08:50:54 00:02 [jar-tool] created dist/hello-example.zip 08:50:54 00:02 [dup] 08:50:54 00:02 [apk] 08:50:54 00:02 [apk-bundle] SUCCESS $
This generates a zipfile with runnable contents; instead of a zipfile, we could have put the contents a directory tree, a giant jar, or something else. Learn more about bundles.
Toolchain
Ivy
Pants uses Ivy to resolve jar
dependencies. To change how Pants
resolves these, configure resolve.ivy
.
Coursier
Starting at release 1.4.0.dev26
, Pants added an option to pick coursier
as the JVM 3rdparty resolver, with performance improvement being the main motivation. The goal is to retire ivy
eventually.
Limitation
- Currently coursier only supports maven style repo resolves. Resolving with ivy settings is still not mature.
- Coursier does not do publishing.
Example config to use coursier
For Pants >= 1.7.x
# This will turn on coursier and turn off ivy. [resolver] resolver = "coursier" [resolve.coursier] # jvm option in case of large resolves jvm_options = ['-Xmx4g', '-XX:MaxMetaspaceSize=256m'] [export] # Same if needed for large resolves jvm_options = ['-Xmx4g', '-XX:MaxMetaspaceSize=256m'] [coursier] repos = ['https://repo1.maven.org/maven2', 'https://dl.google.com/dl/android/maven2/'] # Change the following if you choose to [build coursier jar from scratch](https://github.com/coursier/coursier/blob/master/DEVELOPMENT.md#build-with-pants) # or to fetch from different location. bootstrap_jar_url = "<url>" version = "<version>"
- To inspect the resolve result, specify
--resolver-coursier-report
- To show coursier command line invocation, use
-ldebug
./pants --resolver-resolver=coursier resolve.coursier --report examples/tests/scala/org/pantsbuild/example/hello/welcome -ldebug
For Pants <= 1.6.x
Note that this uses pants.ini
rather than pants.toml
.
# This will turn on coursier and turn off ivy. [resolver] resolver: coursier [export] # Same if needed for large resolves jvm_options: ['-Xmx4g', '-XX:MaxMetaspaceSize=256m'] [coursier] # Change the following if you choose to [build coursier jar from scratch](https://github.com/coursier/coursier/blob/master/DEVELOPMENT.md#build-with-pants) # or to fetch from different location. bootstrap_jar_url: <url> version: <version> fetch_options: [ # Specify maven repos '-r', 'https://repo1.maven.org/maven2/', '-r', 'https://dl.google.com/dl/android/maven2/', # Quiet mode '-q', # Do not use default public maven repo. '--no-default', # Concurrent workers '-n', '10', # Specify the type of artifacts to fetch '-A', 'jar,bundle,test-jar,maven-plugin,src,doc,aar' ]
Nailgun
Pants uses Nailgun to speed up compiles. Nailgun is a JVM daemon that runs in the background. This means you don't need to start up a JVM and load classes for each JVM-based operation. Things go faster.
Zinc
Pants uses Zinc, a dependency tracking compiler facade that supports sub-target incremental compilation for Java and Scala.
Java9 vs Java8, Which Java
Pants first looks through any JDKs specified by the paths
map in pants.toml's jvm-distributions
section, eg:
[jvm-distributions] paths = """ { 'macos': [ '/Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk', '/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk', ], 'linux': [ '/usr/java/jdk1.8.0_152', ] } """
If no JVMs are found there, Pants uses the first Java it finds in JDK_HOME
, JAVA_HOME
,
or PATH
. If no paths
are specified in pants.toml, you can use JDK_HOME to set the Java version
for just one pants invocation:
$ JDK_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64 ./pants ...
If you sometimes need to compile some code in Java 8 and sometimes Java 9, you can define jvm-platforms in pants.toml, and set what targets use which platforms. For example, in pants.toml:
[jvm-platform] default_platform = "java8" platforms = """ { 'java8': {'source': '8', 'target': '8', 'args': [] }, 'java9': {'source': '9', 'target': '9', 'args': [] }, } """
And then in a BUILD file:
java_library( name='my-library', sources=['*.java'], platform='java9', )
You can also override these on the cli:
./pants compile --jvm-platform-default-platform=java9 examples/src/java/org/pantsbuild/example/hello/main
If you want to set the -bootclasspath
(or -Xbootclasspath
) to use the
appropriate java distribution, you can use the $JAVA_HOME
symbol in the
args
list. For example:
[jvm-platform] default_platform = "java8" platforms = """ { 'java8': {'source': '8', 'target': '8', 'args': ["-C-bootclasspath:$JAVA_HOME/jre/lib/resources.jar:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/jre/lib/sunrsasign.jar:$JAVA_HOME/jre/lib/jsse.jar:$JAVA_HOME/jre/lib/jce.jar:$JAVA_HOME/jre/lib/charsets.jar:$JAVA_HOME/jre/lib/jfr.jar:$JAVA_HOME/jre/classes"] }, }
Your -bootclasspath
should be designed to work with any compatible version of
the JVM that might be used. If you make use of [jvm-distributions]
and have
strict control over what jvm installations are used by developers, this means you
probably only have to make it work for one version of the JDK. Otherwise, you
should design your bootclasspath to reference the union of all possible jars
you might need to pull in from different JVMs (any paths that aren't available
will simply be ignored by java).
Note: Currently, pants is known to work with OpenJDK and Oracle JDK version 8 or greater.
Bundles: Deploy-able Runnable File Trees
You can enjoy your web service on your development machine's
localhost
, but to let other people enjoy it, you probably want to copy
it to a server machine. With Pants, the easiest way to do this is to
create a bundle: a directory tree of .jar
and helper files.
Our "hello world" sample application needs a configuration file to run
correctly. (You can try to run without the configuration file, but the
program crashes immediately.) We define a jvm_app
that represents a
runnable binary and "bundles" of extra files:
# Note that the target has no explicit name, so it defaults to the name # of the directory, in this case 'main'. jvm_app( basename = 'hello-example', dependencies = [ ':main-bin' ], bundles = [ bundle(relative_to='config', fileset=['config/*']) ] )
Here, we keep the extra files in a subdirectory, config/
so that they
don't clutter up this directory. (In this simple example, there's just
one file, so there isn't actually much clutter.) By using the
bundle
's
relative_to
parameter, we "strip off" that
subdirectory; in the generated bundle, these extra files will be in the
top directory.
(If you want to set up a tree of static files but don't need it to be
runnable, you can define a jvm_app
target with bundles (and/or
resources) but whose jvm_binary
has no source or main; the resulting
bundle will have the files you want (along with a couple of
not-so-useful stub .jar
files).)
Generating a Bundle
Invoke ./pants bundle
on a JVM app or JVM binary target:
$ ./pants bundle examples/src/java/org/pantsbuild/example/hello/main:main
With options, you can tell Pants to archive the bundle in a zip, a tar, and some other common formats. See the bundle help for built-in choices.
Contents of a Bundle
The generated bundle is basically a directory tree containing .jar
s
and extra files. The .jar
in the top-level directory has a manifest so
you can run it with java -jar
:
$ cd dist/hello-example-bundle/ $ java -jar hello-example.jar 16:52:11 INFO : Hello, world!
The "bundle" is basically a tree of files:
$ cd dist/hello-example-bundle/
$ find .
.
./greetee.txt
./hello-example.jar
./libs
$ jar -tf hello-example.jar
META-INF/
META-INF/MANIFEST.MF
com/
org/pantsbuild/
org/pantsbuild/example/
org/pantsbuild/example/hello/
org/pantsbuild/example/hello/main/
org/pantsbuild/example/hello/main/HelloMain.class
org/pantsbuild/example/
org/pantsbuild/example/hello/
org/pantsbuild/example/hello/world.txt
org/pantsbuild/example/hello/greet/
org/pantsbuild/example/hello/greet/Greeting.class
That greetee.txt
file came from the bundles=
parameter.
The libs/
directory contains 3rdparty jars (if any). The jar
in the top directory
contains code compiled for this target.
Deploying a Bundle
Instead of just creating a directory tree, you can specify bundle --bundle-jvm-archive=zip
to
./pants bundle
to generate an archive file (a .zip
, monolithic .jar
, or some other
format) instead.
To use such an archive, put it where you want it, unpack it, and run:
$ ./pants bundle --bundle-jvm-archive=zip examples/src/java/org/pantsbuild/example/hello/main ...lots of build output... 10:14:26 00:01 [create-monolithic-jar] 10:14:26 00:01 [add-internal-classes] 10:14:26 00:01 [jar-tool] created dist/hello-example.zip 10:14:26 00:01 [dup] 10:14:26 00:01 [apk] 10:14:26 00:01 [apk-bundle] SUCCESS $ # let's use it: $ mkdir tmp; cd tmp $ unzip ../dist/hello-example.zip Archive: ../dist/hello-example.zip inflating: greetee.txt inflating: hello-example.jar $ java -jar hello-example.jar Hello, Bundled-File World! Hello, Resource World! $
Omitting or Shading the Contents of a Binary
Omitting
Sometimes you want to leave some files out of your binary.
You can omit jars from the binary by means of the jvm_binary
's deploy_excludes
parameter.
For example, if you're making a binary to run on Hadoop and there are some "standard jars"
already on the destination machines, you can list those in deploy_excludes
.
More generally, you can omit files from the binary jar with deploy_jar_rules
. For example, a
3rdparty dependency might have a transitive dependency with a bad manifest file. If you try to run
the jar you might get Invalid signature file digest for Manifest main attributes
. If you don't
actually use the code in that transitive dependency, you might work around the error by omitting
the dependency.
To tell Pants to omit some files from the binary, set the deploy_jar_rules
parameter of
jvm_binary
to a jar_rules
.
E.g., to omit all files containing the regexp Greeting
, you might set
deploy_jar_rules=jar_rules(rules=[Skip('Greeting')])
After building our hello
example, if we check the binary jar's contents, there is no
Greeting.class
(and running that jar crashes; we omitted a class this binary needs):
$ ./pants binary examples/src/java/org/pantsbuild/example/hello/main:main $ jar -tf dist/hello-example.jar META-INF/ META-INF/MANIFEST.MF com/ org/pantsbuild/ org/pantsbuild/example/ org/pantsbuild/example/hello/ org/pantsbuild/example/hello/main/ org/pantsbuild/example/hello/main/HelloMain.class org/pantsbuild/example/ org/pantsbuild/example/hello/ org/pantsbuild/example/hello/world.txt $
Shading
Sometimes you have dependencies that have conflicting package or class names. This typically occurs in the following scenario: Your jvm_binary depends on a 3rdparty library A (rev 1.0), and a 3rdparty library B (rev 1.3). It turns out that A happens to also depend on B, but it depends on B (rev 2.0), which is backwards-incompatible with rev 1.3. Now B (1.3) and B (2.0) define different versions of the same classes, with the same fully-qualified class names, and you're pulling them all onto the classpath for your project.
This is where shading comes in: you can rename the fully-qualified names of the classes that
conflict, typically by applying a prefix (eg, __shaded_by_pants__.org.foobar.example
).
Pants uses jarjar for shading, and allows shading rules to be specified on jvm_binary
targets with
the shading_rules
argument. The shading_rules
argument is a list of rules. Available rules
include: shading_relocate
,
shading_exclude
,
shading_relocate_package
, and
shading_exclude_package
.
The order of rules in the list matters, as typical of shading logic in general.
These rules are powerful enough to take advantage of jarjar's more advanced syntax, like using wildcards in the middle of package names. E.g., this syntax works:
# Destination pattern will be inferred to be # __shaded_by_pants__.com.@1.foo.bar.@2 shading_relocate('com.*.foo.bar.**')
Which can also be done by:
:::python shading_relocate_package('com.*.foo.bar')
The default shading prefix is __shaded_by_pants__
, but you can change it:
shading_relocate_package('com.foo.bar', shade_prefix='__my_prefix__.')
You can rename a specific class:
shading_relocate('com.example.foo.Main', 'org.example.bar.NotMain')
If you want to shade everything in a package except a particular file (or subpackage), you can use
the shading_exclude
rule.
shading_exclude('com.example.foobar.Main') # Omit the Main class. shading_exclude_package('com.example.foobar.api') # Omit the api subpackage. shading_relocate_package('com.example.foobar')
Again, order matters here: excludes have to appear first.
To see an example, take a look at testprojects/src/java/org/pantsbuild/testproject/shading/BUILD
,
and try running
./pants binary testprojects/src/java/org/pantsbuild/testproject/shading jar -tf dist/shading.jar
Target Scopes
Overview
Pants supports marking targets with one or more scope
values which the JVM backend will use to filter
dependency subgraphs at compiletime and runtime. Scopes are also used for unused dependency
detection: only default
scoped targets are eligible to be considered as "unused" deps.
Correspondence with other build systems
Scopes in pants are similar to scopes in other build systems, with the fundamental difference that they apply to targets (the "nodes" of the build graph), rather than to dependency "edges". The reason that Pants differs in this regard is that, in a monorepo, it is strongly encouraged to make changes to your dependency targets (which benefits all consumers) rather than to work around an issue in a dependency by making a local change to your target.
Scope values
Pants' built in scopes are:
default
: The "default" scope when a scope is not specified on a target.default
targets are included on classpaths at both compiletime and runtime, and are the only targets eligible for unused dep detection.compile
: Indicates that a target is only used at compiletime, and should not be included in runtime binaries or bundles. javac annotation processors or scalac macros are good examples of compiletime-only dependencies.runtime
: Indicates that a target is only used at runtime, and should not be presented to the compiler. Targets which are only used via JVM reflection are good examples of runtime-only dependencies.test
: Indicates that a target is used when running tests. This scope is typically used in addition to another scope (e.g.:scope='compile test'
). Targets which are are provided by an external execution environment are good examples of compile+test dependencies.forced
(available from pants 1.1.0): Theforced
scope is equivalent to thedefault
scope, but additionally indicates that a target is not eligible to be considered an "unused" dependency. It is sometimes necessary to mark a targetforced
due to false positives in the static analysis used for unused dependency detection; if possible, you should always prefer to mark a targetruntime
orcompile
if that more accurately describes their usage.
Setting target scopes
To set the scope of a target, you should generally prefer to pass the scope
parameter for
that target:
java_library(name='lib', .., scope='runtime', )
Multiple scopes can be specified. The equivalent of Maven's provided
scope can be expressed by
specifying both compile and test scopes.
java_library(name='lib', .., scope='compile test', )
If the scope of a target is not matched for a particular context, the entire subgraph represented
by the dependency will be pruned. This means that if a dependency 'B' of a target 'A' is marked
compile
(for example), the dependency targets of 'B' will only be included at compiletime (unless
'A' has other dependency paths to those targets).
One effect of this behavior is that you can introduce intermediate aliases to "re-scope" a target when consumers need to use it in multiple ways:
# An alias of `:lib` which consumers can use to indicate that they only need it at compile time. target(name='lib-compile', dependencies=[':lib'], scope='compile', ) java_library(name='lib', .., scope='default', )
Finally, for cases where only a few consumers need to "re-scope" a particular target, it is possible
to change the scope of a single edge locally to a consumer via the scoped
macro (which, under the
hood, uses the previous technique of creating an intermediate alias):
java_library(name='lib', dependencies=[ scoped('src/java/the/best/lib', scope='compile'), 'src/scala/some/other/lib', ], )
Dependency Hygiene
As the set of targets in a repository grows larger, it becomes increasingly important that they observe good dependency hygiene. In particular, following the 1:1:1 rule helps keep useful code self-contained. But even while observing 1:1:1, it's possible to declare and use dependencies that add little or no benefit for a target.
For example: a particularly large target may expose many different APIs. In cases where other targets depend on the large target, they might need only a fraction of those APIs. But because they can't declare a dependency on a smaller subset of the large target, they are forced to build the entire dependency. Even in the presence of distributed builds and caching, this slows down your build!
To help users address these problems for JVM targets, pants has a dep-usage.jvm
task which
supports scoring and summarizing the fractions of each dependency that a target uses.
For local analysis
In the default output mode ("summary" mode) the dep-usage.jvm
task outputs targets ordered by
a simple 'badness' score. The "badness" score is intended to indicate both how easy the dependency
would be to remove (based on the maximum fraction used by each dependee) and how valuable it would
be remove (based on a estimate of the transitive cost to build the dep).
Stat explanation:
max_usage
: fraction of the dependency target is being used. 0 means the target is safe to drop.cost_transitive
: the cost to bring in this target including its transitive dependencies in the build process-
badness
:cost_transitive/max_usage
, so the lower the better.:::shell $ ./pants -q dep-usage.jvm examples/src/scala/org/pantsbuild/example/hello/: [ {"badness": 0, "max_usage": 1.0, "cost_transitive": 0, "target": "//:scala-library-synthetic"}, {"badness": 8872, "max_usage": 0.125, "cost_transitive": 1109, "target": "examples/src/java/org/pantsbuild/example/hello/greet:greet"}, {"badness": 16410, "max_usage": 0.1, "cost_transitive": 1641, "target": "examples/src/scala/org/pantsbuild/example/hello/welcome:welcome"} ]
The above example indicates that within the scope of the scala examples, the
examples/src/scala/org/pantsbuild/example/hello/welcome
target is the worst dependency. This is
because it has a high transitive "cost" to build, and sees a maximum of 10% usage by its dependees.
For global analysis
The summary mode is great when users want to inspect their own targets. But for more in-depth
analysis, disabling summary mode (by passing the --no-summary
flag) will output raw usage data
for each dependency edge. This mode does no aggregation, so using it effectively usually means
doing analytics or graph analysis with an external tool.
Compiler Plugins
Pants has robust support for both developing and using compiler plugins for javac and scalac. For more details:
Further Reading
If you use Scala, see Scala Projects with Pants.
If you know Maven and want to know Pants equivalents, see Pants for Maven Experts.