Skip to main content

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:

  1. 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.
  2. 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 require and import forms
  • REPL: Interactive development with nREPL support and rebel-readline
  • Testing: Run tests with clojure.test via pants 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.edn from 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_sources targets
  • All Maven dependencies from your Pants lockfile
  • A :test alias with your test source paths
  • :nrepl and :rebel aliases 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

pants_backend_clojure-0.2.4.tar.gz (63.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pants_backend_clojure-0.2.4-py3-none-any.whl (72.1 kB view details)

Uploaded Python 3

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

Hashes for pants_backend_clojure-0.2.4.tar.gz
Algorithm Hash digest
SHA256 f55cebea10f9ae7db53de61116badd09e757cca8de23b9a1a52fe43967648c36
MD5 41442ebb4eee1375cc95c62c71ea6470
BLAKE2b-256 460f0a720804d89a0630787995f22fa5d7a91b3d7d63535912d182a213dc44b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for pants_backend_clojure-0.2.4.tar.gz:

Publisher: publish.yaml on enragedginger/pants_backend_clojure

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pants_backend_clojure-0.2.4-py3-none-any.whl.

File metadata

File hashes

Hashes for pants_backend_clojure-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 32950013e064930ce38374f00898ec135a9658af4a99e1b4ea9db00a03372549
MD5 2a59ea887b578699be1baf492d360443
BLAKE2b-256 ac1d68efd3f9af16c6c60c4d1182ce1408c8e4a6371384fd8f57011f20887ccb

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page