A Clojure backend for the Pants build system
Project description
pants-backend-clojure
A Clojure backend for the Pants build system.
This plugin brings first-class Clojure support to Pants, enabling REPL-driven development, automatic dependency inference, linting, formatting, testing, and uberjar packaging within a monorepo-friendly build system.
A handcrafted word from the author
Hello, yes, it's me, a real person. I'm writing this section to you, another real person. Most markdown docs these days are generated by AI. This has its pros and cons. The advantages are obvious. But among the disadvantages is an explosion of useless, overly verbose, impersonal information.
So I'm writing this section by myself to add a personal touch and help you, the human reviewing this project, to gain immediate insight into what's going on with this project instead of having to clone it down and ask Claude to review it.
AI coding tools are great when used properly. I've found them particularly useful in situations where I want to tackle a project in a domain that is largely unfamiliar to me, but I don't have the time to gain deep mastery of everything. That's basically what's happened here. I've written a lot of Clojure code in my life. I've been working with the Pants build tool for a few years now and I've written many internal plugins for it. This is my first attempt at creating something that has use and value to the outside world. I've used Claude to write all of the code.
I'm "dogfooding" this project though and using that experience as my main method of verifying this project and driving it forward. However, I'd very much welcome input from others who see value in this. Feel free to try it out and let me know what you think. I have a project which is 35k lines of Clojure. It outgrew Leiningen sometime ago. I moved it to poly as that seemed to be the best option for multi-module, multi-build Clojure project. However, I never really loved poly. The commands always felt foreign to me. The errors were unhelpful. I ran into situations where it messed with my test classpath in unexplainable ways. The distinction between "base" and "component" felt arbitrary. And the patterns around "interfaces" didn't sit well with me.
I've used the Pants build tool for a few years now with Python. I've loved the way it provides "resolves". Every large software repo I've ever worked in has two problems:
- Upgrading parts of the codebase either to new language runtime versions or new third party libraries results in hard or impossible to resolve conflicts with other older, unrelated parts of the codebase. Resolving these issues is usually non-trivial. You typically either avoid upgrading because the amount of resources required to upgrade isn't worth the hassle. Or you put hacks in place to test or compile your code against one version, but in production you end up using a different version. This always leads to bugs and distrust in the codebase.
- Unrelated projects end up depending on each other because dependencies must be specified explicitly and at the project level. A relatively minor, unrelated changes in a common project at the bottom of your stack can result in cache misses for everything else in the repo.
Pants addresses both of these issues. Pants registers each source file as a target. It understands dependencies in your codebase at the target level. So changes in one file only mean that the other files / tests / builds with a transitive dependency on that file need cache misses. It also provides "resolves" which you can think of as a combination of your target runtime (i.e. Java 21) and a lockfile of third party dependencies. Every source target belongs to one or more resolves. Suppose you have some common source that needs to work with other first party sources in both Java 8 and Java 21 and against different versions of some way-too-common library like Guava. You'd assign this hypothetical source to both resolves. Its unit tests would run in both resolves. Bugs that might exist in one runtime, but not the other would be caught and surfaced quickly. Also, Pants has the ability to detect first and third party dependencies automatically. So you no longer have to look at a long list of project dependency lines and wonder which ones you can safely remove. Just specify nothing and let Pants figure it out for you.
This is all wonderful and I've felt for awhile now that it was the missing piece for large Clojure projects. So I went out and built it. I'm using it now. I'm incredibly happy with the results.
Both first party and third party dependency inference work. After you run pants generate-lockfiles, Pants automatically analyzes the JARs in your lockfiles and infers third-party Clojure dependencies — no extra steps needed.
Anyhow, back to your regularly scheduled AI generated readme doc:
Features
- Dependency Inference: Automatically discovers dependencies from
requireandimportforms - REPL: Interactive development with nREPL support and rebel-readline
- Testing: Run tests with
clojure.testviapants test - Linting: Static analysis with clj-kondo
- Formatting: Code formatting with cljfmt
- Packaging: Build uberjars with AOT compilation and direct linking
- JVM Integration: Works with Pants' JVM support for mixed Clojure/Java projects
- Provided Dependencies: Maven-style provided scope for excluding runtime dependencies
- IDE Integration: Generate
deps.ednfrom your Pants project for use with Cursive, Calva, or any Clojure-aware IDE
Installation
Add the plugin to your pants.toml:
[GLOBAL]
pants_version = "2.31.0"
plugins = ["pants-backend-clojure==0.2.4"]
backend_packages = [
"pants.backend.experimental.java",
"pants_backend_clojure",
]
[coursier]
repos = [
"https://repo.clojars.org/",
"https://repo1.maven.org/maven2",
]
[jvm.resolves]
jvm-default = "locks/jvm-default.lock"
Quick Start
1. Define your dependencies
Create a BUILD file with your Clojure dependencies:
jvm_artifact(
name="clojure",
group="org.clojure",
artifact="clojure",
version="1.12.0",
)
jvm_artifact(
name="core-async",
group="org.clojure",
artifact="core.async",
version="1.7.701",
)
2. Add source targets
clojure_sources(
name="lib",
dependencies=["//:clojure"],
)
clojure_tests(
name="tests",
dependencies=["//:clojure"],
)
3. Generate the lockfile
pants generate-lockfiles
4. Run common commands
# Start a REPL
pants repl src/myapp:lib
# Run tests
pants test ::
# Lint code
pants lint ::
# Format code
pants fmt ::
# Check for errors
pants check ::
# Build an uberjar
pants package src/myapp:deploy
Target Types
clojure_source / clojure_sources
Source files containing application or library code.
clojure_sources(
name="lib",
sources=["*.clj", "*.cljc"],
dependencies=["//:clojure"],
resolve="jvm-default",
)
clojure_test / clojure_tests
Test files using clojure.test.
clojure_tests(
name="tests",
sources=["*_test.clj"],
dependencies=[":lib", "//:clojure"],
timeout=120,
)
Concurrency slots for parallel tests
When tests grab shared host resources (TCP ports, on-disk paths, database
names, …) parallel runs collide. Mirroring pytest's
[pytest].execution_slot_var, the Clojure test runner can hand each
concurrent test process a unique integer slot id:
# pants.toml
[clojure-test-runner]
execution_slot_var = "PANTS_EXECUTION_SLOT"
Each running test JVM then sees a PANTS_EXECUTION_SLOT env var with an
integer in [0, parallelism). Use it to derive non-overlapping resource
ranges, e.g. a private port window per worker:
(let [slot (Long/parseLong
(or (System/getenv "PANTS_EXECUTION_SLOT")
(throw (ex-info "PANTS_EXECUTION_SLOT not set — configure [clojure-test-runner].execution_slot_var" {}))))
base 30000
size 200
port-start (+ base (* slot size))
port-end (+ port-start size -1)]
;; bind your test fixtures to ports in [port-start, port-end]
...)
Failing loudly when the env var is unset is intentional — silently defaulting to slot 0 would collapse every worker onto the same resource window, defeating the point of the feature.
Slot count is governed by [GLOBAL].process_execution_local_parallelism.
clojure_deploy_jar
An executable uberjar with AOT compilation.
clojure_deploy_jar(
name="deploy",
main="myapp.core", # Namespace with -main and (:gen-class)
dependencies=[":lib", "//:clojure"],
provided=[":servlet-api"], # Excluded from JAR
)
The main namespace must include (:gen-class) and define a -main function:
(ns myapp.core
(:gen-class))
(defn -main [& args]
(println "Hello, World!"))
Goals
| Goal | Command | Description |
|---|---|---|
| REPL | pants repl path/to:target |
Start an interactive nREPL session |
| Test | pants test :: |
Run clojure.test tests |
| Lint | pants lint :: |
Static analysis with clj-kondo |
| Format | pants fmt :: |
Format code with cljfmt |
| Check | pants check :: |
Verify code compiles |
| Package | pants package path/to:jar |
Build an uberjar |
| Generate deps.edn | pants generate-deps-edn |
Generate a deps.edn for IDE and tool integration |
IDE and Tooling Integration
Pants manages your dependencies, source roots, and resolves — but your IDE doesn't know about any of that. The generate-deps-edn goal bridges this gap by producing a deps.edn file from your Pants project, so you can open it in Cursive, Calva, CIDER, or any tool that understands deps.edn.
pants generate-deps-edn
The generated deps.edn includes:
- All source paths discovered from your
clojure_sourcestargets - All Maven dependencies from your Pants lockfile
- A
:testalias with your test source paths :nrepland:rebelaliases for REPL convenience- Maven repository configuration from your
pants.toml
If you have multiple JVM resolves, specify which one to generate for:
pants generate-deps-edn --resolve=java21
You can also control where the file is written:
pants generate-deps-edn --output-path=deps-java21.edn
This also works well as an integration point for Clojure tools that aren't directly supported by this plugin but do support deps.edn — test runners, documentation generators, static analysis tools, and so on.
You'll probably want to add deps.edn to your .gitignore since it's a generated file.
Configuration
Tool versions
Override default tool versions in pants.toml:
[clj-kondo]
version = "2026.01.19"
[cljfmt]
version = "0.16.2"
[nrepl]
version = "1.4.0"
Skip linting/formatting per target
clojure_source(
name="generated",
source="generated.clj",
skip_cljfmt=True,
skip_clj_kondo=True,
)
System Requirements
- Python: 3.9+
- Pants: 2.30.0+
- JDK: 11+ (17 or 21 recommended)
- zip/unzip: Required for clj-kondo
- macOS: Pre-installed
- Debian/Ubuntu:
apt-get install zip unzip - RHEL/Fedora:
dnf install zip unzip - Alpine:
apk add zip unzip
Known Issues
Coursier drops transitive dependencies
Pants uses Coursier to resolve JVM dependencies. There is a known bug where Coursier's locking fails to include transitive dependencies when you request a non-default version of an intermediate dependency. This can cause missing dependency errors during compilation or at runtime.
The fix is available in Coursier v2.1.25-M19 but has not yet been released as a stable version. Until then, you can work around this by pinning the Coursier version in your pants.toml:
[coursier]
version = "v2.1.25-M19"
repos = [
"https://repo.clojars.org/",
"https://maven-central.storage-download.googleapis.com/maven2",
"https://repo1.maven.org/maven2",
# add any other remote repos here
]
known_versions = [
"v2.1.25-M19|macos_x86_64|86298db922051950acac686988b42ab7756359befe24f315ee72124c2b12b3c3|82018352",
"v2.1.25-M19|macos_arm64|7e5fc9510fa08c3f98eb36c5801e4d7605e3f4d3d6bc43fdcd47658dd56bb316|27061694|https://github.com/coursier/coursier/releases/download/v2.1.25-M19/cs-aarch64-apple-darwin.gz",
"v2.1.25-M19|linux_x86_64|13fc9bc82f2e4a38a60cf801ff9399c398644c7e5e4b8cf5116ef80aa4691312|28365130",
"v2.1.25-M19|linux_arm64|a7bbad42163c4fde4cb1191ccfd11261c21873023c495ba7494ca109de4da761|85679032|https://github.com/coursier/coursier/releases/download/v2.1.25-M19/cs-aarch64-pc-linux.gz",
]
This should be resolved once Coursier releases a stable v2.1.25+.
Local Development
If you want to test changes to this plugin in another Pants project before publishing, there are two approaches.
Quick iteration: pythonpath
Load the plugin directly from source with no build step. In the consuming project's pants.toml:
[GLOBAL]
pythonpath = ["/path/to/pants-backend-clojure/pants-plugins"]
backend_packages = [
"pants.backend.experimental.java",
"pants_backend_clojure",
]
Changes are picked up immediately — no rebuild needed. Note that this does not test packaging or dependency metadata.
Full validation: local wheel
Build the wheel and point the consuming project at it. This tests the actual distributed package.
# In this repo
pants package pants-plugins:wheel
Then in the consuming project's pants.toml:
[GLOBAL]
plugins = ["pants-backend-clojure==0.2.4"]
backend_packages = [
"pants.backend.experimental.java",
"pants_backend_clojure",
]
plugins_force_resolve = true # re-resolves on every run; remove when done
[python-repos]
find_links = [
"file:///path/to/pants-backend-clojure/dist",
]
Rebuild the wheel after each change and Pants will pick it up automatically.
Example Projects
- Pedestal — the Pedestal web framework, built with Pants and this plugin
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
Apache License 2.0 - see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pants_backend_clojure-0.2.4.tar.gz.
File metadata
- Download URL: pants_backend_clojure-0.2.4.tar.gz
- Upload date:
- Size: 63.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f55cebea10f9ae7db53de61116badd09e757cca8de23b9a1a52fe43967648c36
|
|
| MD5 |
41442ebb4eee1375cc95c62c71ea6470
|
|
| BLAKE2b-256 |
460f0a720804d89a0630787995f22fa5d7a91b3d7d63535912d182a213dc44b6
|
Provenance
The following attestation bundles were made for pants_backend_clojure-0.2.4.tar.gz:
Publisher:
publish.yaml on enragedginger/pants_backend_clojure
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pants_backend_clojure-0.2.4.tar.gz -
Subject digest:
f55cebea10f9ae7db53de61116badd09e757cca8de23b9a1a52fe43967648c36 - Sigstore transparency entry: 1630633770
- Sigstore integration time:
-
Permalink:
enragedginger/pants_backend_clojure@78467daa860f4b50e041f8a0d241fd30fce1ee30 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/enragedginger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@78467daa860f4b50e041f8a0d241fd30fce1ee30 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pants_backend_clojure-0.2.4-py3-none-any.whl.
File metadata
- Download URL: pants_backend_clojure-0.2.4-py3-none-any.whl
- Upload date:
- Size: 72.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32950013e064930ce38374f00898ec135a9658af4a99e1b4ea9db00a03372549
|
|
| MD5 |
2a59ea887b578699be1baf492d360443
|
|
| BLAKE2b-256 |
ac1d68efd3f9af16c6c60c4d1182ce1408c8e4a6371384fd8f57011f20887ccb
|
Provenance
The following attestation bundles were made for pants_backend_clojure-0.2.4-py3-none-any.whl:
Publisher:
publish.yaml on enragedginger/pants_backend_clojure
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pants_backend_clojure-0.2.4-py3-none-any.whl -
Subject digest:
32950013e064930ce38374f00898ec135a9658af4a99e1b4ea9db00a03372549 - Sigstore transparency entry: 1630633801
- Sigstore integration time:
-
Permalink:
enragedginger/pants_backend_clojure@78467daa860f4b50e041f8a0d241fd30fce1ee30 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/enragedginger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@78467daa860f4b50e041f8a0d241fd30fce1ee30 -
Trigger Event:
workflow_dispatch
-
Statement type: