Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

* Remove automatic Node.js installation. Users must explicitly add the Node.js buildpack if their project requires Node.js or npm. ([#192](https://github.com/heroku/heroku-buildpack-clojure/pull/192))
* Suppress curl output during leiningen installation to reduce build log noise and improve testability. ([#188](https://github.com/heroku/heroku-buildpack-clojure/pull/188))
* Replace `apt-get` `rlwrap` installation with shim. ([#187](https://github.com/heroku/heroku-buildpack-clojure/pull/187))
* Buildpack output slightly changed. If you match against the buildpack output, verify your matching still works and adjust if necessary. ([#191](https://github.com/heroku/heroku-buildpack-clojure/pull/191))
Expand Down
233 changes: 70 additions & 163 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,197 +1,104 @@
# Heroku buildpack: Clojure [![CI](https://github.com/heroku/heroku-buildpack-clojure/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/heroku-buildpack-clojure/actions/workflows/ci.yml)
![clojure](https://raw.githubusercontent.com/heroku/buildpacks/refs/heads/main/assets/images/buildpack-banner-clojure.png)

This is the official [Heroku buildpack](http://devcenter.heroku.com/articles/buildpack) for Clojure apps. It uses
[Leiningen](http://leiningen.org).
# Heroku Buildpack: Clojure (Leiningen) [![CI](https://github.com/heroku/heroku-buildpack-clojure/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/heroku-buildpack-clojure/actions/workflows/ci.yml)

Note that you don't have to do anything special to use this buildpack
with Clojure apps on Heroku; it will be used by default for all
projects containing a project.clj file, though it may be an older
revision than what you're currently looking at.
This is the official [Heroku buildpack](https://devcenter.heroku.com/articles/buildpacks) for apps that use [Leiningen](https://leiningen.org/) as their build tool. It's used to build [Clojure](https://clojure.org/) applications.

## How it works
If you're using a different JVM build tool, use the appropriate buildpack:
* [Java buildpack](https://github.com/heroku/heroku-buildpack-java) for [Maven](https://maven.apache.org/) projects (including [clojure-maven-plugin](https://github.com/talios/clojure-maven-plugin))
* [Gradle buildpack](https://github.com/heroku/heroku-buildpack-gradle) for [Gradle](https://gradle.org/) projects
* [Scala buildpack](https://github.com/heroku/heroku-buildpack-scala) for [sbt](https://www.scala-sbt.org/) projects

The buildpack will detect your app as Clojure if it has a
`project.clj` file in the root. If you use the
[clojure-maven-plugin](https://github.com/talios/clojure-maven-plugin),
[the standard Java buildpack](http://github.com/heroku/heroku-buildpack-java)
should work instead.
## Table of Contents

## Documentation
- [Supported Leiningen Versions](#supported-leiningen-versions)
- [Getting Started](#getting-started)
- [Application Requirements](#application-requirements)
- [Configuration](#configuration)
- [OpenJDK Version](#openjdk-version)
- [Leiningen Version](#leiningen-version)
- [Buildpack Configuration](#buildpack-configuration)
- [Uberjar Deployment](#uberjar-deployment)
- [Leiningen at Runtime](#leiningen-at-runtime)
- [Custom Build Script](#custom-build-script)
- [Documentation](#documentation)

For more information about using Clojure and buildpacks on Heroku, see these Dev Center articles:

+ [Getting Started with Clojure on Heroku](https://devcenter.heroku.com/articles/getting-started-with-clojure)
+ [Heroku Clojure Support](https://devcenter.heroku.com/articles/clojure-support)
+ [Building a Database-Backed Clojure Web Application](https://devcenter.heroku.com/articles/clojure-web-application)
+ [Database Connection Pooling with Clojure](https://devcenter.heroku.com/articles/database-connection-pooling-with-clojure)
+ [Live-Debugging Remote Clojure Apps with Drawbridge](https://devcenter.heroku.com/articles/debugging-clojure)
+ [WebSockets on Heroku with Clojure and Immutant](https://devcenter.heroku.com/articles/using-websockets-on-heroku-with-clojure-and-immutant)
+ [Queuing in Clojure with Langohr and RabbitMQ](https://devcenter.heroku.com/articles/queuing-in-clojure-with-langohr-and-rabbitmq)

## Usage

Example usage for an app already stored in git:

```sh-session
$ tree
|-- Procfile
|-- project.clj
|-- README
`-- src
`-- sample
`-- core.clj

$ heroku create
...

$ git push heroku main
...
remote: -----> Fetching custom tar buildpack... done
remote: -----> Clojure (Leiningen 2) app detected
remote: -----> Installing OpenJDK 1.8...done
remote: -----> Installing Leiningen
remote: Downloading: leiningen-2.5.2-standalone.jar
remote: Writing: lein script
remote: -----> Building with Leiningen
remote: Running: lein uberjar
remote: Created /tmp/build_37f1ae84b9f8b63c3ddef2a4b691ef41/target/clojure-getting-started-1.0.0-SNAPSHOT.jar
remote: Created /tmp/build_37f1ae84b9f8b63c3ddef2a4b691ef41/target/clojure-getting-started-standalone.jar
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing... done, 53.6MB
remote: -----> Launching... done, v5
remote: https://gentle-water-6857.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy.... done.
```
## Supported Leiningen Versions

## Configuration
This buildpack officially supports Leiningen `2.x`. Legacy support is available for Leiningen `1.7.1`, though using Leiningen 2.x is highly recommended. The buildpack will automatically use Leiningen 2.x if your `project.clj` contains `:min-lein-version "2.0.0"` or higher.

Leiningen 1.7.1 will be used by default, but if you have
`:min-lein-version "2.0.0"` in project.clj (highly recommended) then
the latest Leiningen 2.x release will be used instead.

Your `Procfile` should declare what process types which make up your
app. Often in development Leiningen projects are launched using `lein
run -m my.project.namespace`, but this is not recommended in
production because it leaves Leiningen running in addition to your
project's process. It also uses profiles that are intended for
development, which can let test libraries and test configuration sneak
into production.

In order to ensure consistent builds, normally values set with `heroku
config:add ...` (other than `LEIN_USERNAME`, `LEIN_PASSWORD`, and
`LEIN_PASSPHRASE`) will not be visible at compile time. To expose more
to the compilation process, set a `BUILD_CONFIG_ALLOWLIST` config var
containing a space-delimited list of config var names. Note that this
can result in unpredictable behaviour since changing your app's config
does not result in a rebuild of your app. So it's easy to get into a
situation where your build is broken, but you don't notice it until
later when you push. For this reason it's recommended to take care
with this feature and always push after changing a allowlisted config
value.

### Uberjar

If your `project.clj` contains an `:uberjar-name` setting, then
`lein uberjar` will run during deploys. If you do this, your `Procfile`
entries should consist of just `java` invocations.

If your main namespace doesn't have a `:gen-class` then you can use
`clojure.main` as your entry point and indicate your app's main
namespace using the `-m` argument in your `Procfile`:

web: java -cp target/myproject-standalone.jar clojure.main -m myproject.web

If you have custom settings you would like to only apply during build,
you can place them in an `:uberjar` profile. This can be useful to use
AOT-compiled classes in production but not during development where
they can cause reloading issues:

```clj
:profiles {:uberjar {:main myproject.web, :aot :all}}
```
## Getting Started

If you need Leiningen in a `heroku run` session, it will be downloaded
on-demand.
See the [Getting Started with Clojure on Heroku](https://devcenter.heroku.com/articles/getting-started-with-clojure) tutorial.

Note that if you use Leiningen features which affect runtime like
`:jvm-opts`, extraction of native dependencies, or `:java-agents`,
then you'll need to do a little extra work to ensure your Procfile's
`java` invocation includes these things. In these cases it might be
simpler to use Leiningen at runtime instead.
## Application Requirements

### Leiningen at Runtime
Your app requires a `project.clj` file in the root directory. The buildpack will detect your application as Clojure if this file is present.

## Configuration

### OpenJDK Version

Specify an OpenJDK version by creating a `system.properties` file in the root of your project directory and setting the `java.runtime.version` property. See the [Java Support article](https://devcenter.heroku.com/articles/java-support#supported-java-versions) for available versions and configuration instructions.

Instead of putting a direct `java` invocation into your Procfile, you
can have Leiningen handle launching your app. If you do this, be sure
to use the `trampoline` and `with-profile` tasks. Trampolining will
cause Leiningen to calculate the classpath and code to run for your
project, then exit and execute your project's JVM, while
`with-profile` will omit development profiles:
### Leiningen Version

web: lein with-profile production trampoline run -m myapp.web
The buildpack will use Leiningen 2.x if your `project.clj` contains `:min-lein-version "2.0.0"` or higher. Otherwise, it defaults to Leiningen 1.7.1.

Including Leiningen in your slug will add about ten megabytes to its
size and will add a second or two of overhead to your app's boot time.
You can also provide your own `bin/lein` script in your repository to use a specific Leiningen version.

### Overriding build behavior
### Buildpack Configuration

If neither of these options get you quite what you need, you can check
in your own executable `bin/build` script into your app's repo and it
will be run instead of `compile` or `uberjar` after setting up Leiningen.
Configure the buildpack by setting environment variables:

## Leiningen Version
| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `BUILD_CONFIG_ALLOWLIST` | Space-delimited list of config var names to expose during compilation | (none) |

The buildpack will check for a `bin/lein` script in the repo, and run it instead
of the default `lein` command. This allows you to control the exact version of
Leiningen used to build the app.
**Note:** By default, config vars set with `heroku config:set` (except `LEIN_USERNAME`, `LEIN_PASSWORD`, and `LEIN_PASSPHRASE`) are not visible during compilation to ensure consistent builds. Use `BUILD_CONFIG_ALLOWLIST` with caution, as changing config values won't trigger rebuilds, which can lead to broken builds that go unnoticed until the next push.

## JDK Version
### Uberjar Deployment

By default you will get OpenJDK 1.8. To use a different version, you
can commit a `system.properties` file to your app.
If your `project.clj` contains an `:uberjar-name` setting, the buildpack will run `lein uberjar` during deployment. This is the recommended approach for production deployments.

```sh-session
$ echo "java.runtime.version=1.7" > system.properties
$ git add system.properties
$ git commit -m "JDK 7"
Your `Procfile` should invoke Java directly rather than using Leiningen:

```
web: java -cp target/myproject-standalone.jar clojure.main -m myproject.web
```

## Hacking
If your main namespace has a `:gen-class`, you can use it as the entry point. Otherwise, use `clojure.main` with the `-m` flag as shown above.

You can use the `:uberjar` profile in your `project.clj` to apply settings only during builds, such as AOT compilation:

```clojure
:profiles {:uberjar {:main myproject.web, :aot :all}}
```

To change this buildpack, fork it on GitHub. Push up changes to your
fork, then create a test app with `--buildpack YOUR_GITHUB_URL` and
push to it. If you already have an existing app you may use
`heroku config:add BUILDPACK_URL=YOUR_GITHUB_URL` instead.
**Important:** If you use Leiningen features that affect runtime (`:jvm-opts`, native dependencies, `:java-agents`), ensure your `Procfile` includes these settings in the `java` invocation, or use Leiningen at runtime instead.

For example, you could adapt it to generate a tarball at build time.
### Leiningen at Runtime

Open `bin/compile` in your editor, and replace the block labeled
"Calculate build command" with something like this:
Instead of using an uberjar, you can have Leiningen launch your application at runtime. Use the `trampoline` and `with-profile` tasks to ensure Leiningen exits after calculating the classpath and to omit development profiles:

```bash
echo "-----> Generating tar with Leiningen:"
echo " Running: lein tar"
cd $BUILD_DIR
PATH=.lein/bin:/usr/local/bin:/usr/bin:/bin JAVA_OPTS="-Xmx500m -Duser.home=$BUILD_DIR" lein tar 2>&1 | sed -u 's/^/ /'
if [ "${PIPESTATUS[*]}" != "0 0" ]; then
echo " ! Failed to create tar with Leiningen"
exit 1
fi
```
web: lein with-profile production trampoline run -m myapp.web
```

Commit and push the changes to your buildpack to your GitHub fork,
then push your sample app to Heroku to test. The output should include:
**Note:** This approach adds about 10 MB to your slug size and 1-2 seconds to boot time. Leiningen will be downloaded on-demand if needed in `heroku run` sessions.

-----> Generating tar with Leiningen:
### Custom Build Script

If it's something other users would find useful, pull requests are welcome.
If you need custom build behavior, you can provide an executable `bin/build` script in your repository. The buildpack will run this script instead of `compile` or `uberjar` after setting up Leiningen.

## Documentation

## Troubleshooting
For more information about using Clojure on Heroku, see these Dev Center articles:

To see what the buildpack has produced, do `heroku run bash` and you
will be logged into an environment with your compiled app available.
From there you can explore the filesystem and run `lein` commands.
* [Getting Started with Clojure on Heroku](https://devcenter.heroku.com/articles/getting-started-with-clojure)
* [Heroku Clojure Support](https://devcenter.heroku.com/articles/clojure-support)
* [Building a Database-Backed Clojure Web Application](https://devcenter.heroku.com/articles/clojure-web-application)
* [Database Connection Pooling with Clojure](https://devcenter.heroku.com/articles/database-connection-pooling-with-clojure)
* [Live-Debugging Remote Clojure Apps with Drawbridge](https://devcenter.heroku.com/articles/debugging-clojure)
* [WebSockets on Heroku with Clojure and Immutant](https://devcenter.heroku.com/articles/using-websockets-on-heroku-with-clojure-and-immutant)
* [Queuing in Clojure with Langohr and RabbitMQ](https://devcenter.heroku.com/articles/queuing-in-clojure-with-langohr-and-rabbitmq)
35 changes: 29 additions & 6 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ metrics::setup
# Install JDK from jvm-common
install_jdk "${BUILD_DIR}"

# Install Node.js if needed
detect_and_install_nodejs "${BUILD_DIR}"

# Install rlwrap shim for Leiningen 1.x and clj REPLs.
# rlwrap is technically a dependency for both, but it only enhances REPL functionality
# with line editing and command history. This shim allows clj and lein 1.x to run without
Expand All @@ -45,7 +42,12 @@ cp "${BP_DIR}/opt/rlwrap" "${BUILD_DIR}/.heroku/bin/rlwrap"
# To ensure the cache created by older Clojure buildpack versions doesn't stay around indefinitely, we explicitly
# delete these old directories. This will speed up subsequent app builds since it will take less time to restore
# the cache before each build.
rm -rf "${CACHE_DIR}/clojure-bp-apt"
#
# Note: This buildpack used to install and run npm. The node_modules dir is specific to the old Clojure install
# and does not overlap with the official Node.js buildpack.
rm -rf \
"${CACHE_DIR}/clojure-bp-apt" \
"${CACHE_DIR}/node_modules"

# Run clojure install script (clojure / clj may be needed from leiningen for newer cli tools)
CLOJURE_CLI_VERSION="${CLOJURE_CLI_VERSION:-1.10.0.411}"
Expand Down Expand Up @@ -136,13 +138,34 @@ if [[ ! -e "${LEIN_PROFILE_DESTINATION}" ]]; then
fi

# unpack existing cache
CACHED_DIRS=(".m2" "node_modules")
CACHED_DIRS=(".m2")
for DIR in "${CACHED_DIRS[@]}"; do
if [[ ! -d "${BUILD_DIR}/${DIR}" ]]; then
cache_copy "${DIR}" "${CACHE_DIR}" "${BUILD_DIR}"
fi
done

if grep -q lein-npm "${BUILD_DIR}/project.clj"; then
if ! command -v npm &>/dev/null; then
metrics::set_raw "lein_npm_without_nodejs" "true"
output::error <<-EOF
Error: Your project.clj references lein-npm but npm is not available.

The Clojure buildpack no longer automatically installs Node.js.

If your project uses the lein-npm plugin, you must explicitly add the Node.js buildpack to provide Node.js and npm.

To add the Node.js buildpack to your app, run:
heroku buildpacks:add --index 1 heroku/nodejs

For more information, see:
https://devcenter.heroku.com/articles/nodejs-support
https://devcenter.heroku.com/articles/managing-buildpacks#use-multiple-buildpacks
EOF
exit 1
fi
fi

output::step "Building with Leiningen"

# extract environment
Expand Down Expand Up @@ -195,7 +218,7 @@ PROFILE_PATH="${BUILD_DIR}/.profile.d/clojure.sh"
mkdir -p "$(dirname "${PROFILE_PATH}")"
{
echo "export LEIN_NO_DEV=\"\${LEIN_NO_DEV:-yes}\""
echo "export PATH=\"\$HOME/.heroku/bin:\$HOME/.heroku/nodejs/bin:\$HOME/.heroku/clj/bin:\$HOME/.jdk/bin:\$HOME/.lein/bin:\$PATH\""
echo "export PATH=\"\$HOME/.heroku/bin:\$HOME/.heroku/clj/bin:\$HOME/.jdk/bin:\$HOME/.lein/bin:\$PATH\""
echo "export RING_ENV=\"\${RING_ENV:-production}\""
} >>"${PROFILE_PATH}"

Expand Down
31 changes: 0 additions & 31 deletions lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,37 +42,6 @@ function cache_copy() {
fi
}

# Install node.js
function install_nodejs() {
local version="${1:?}"
local dir="${2:?}"
local url="https://heroku-nodebin.s3.us-east-1.amazonaws.com/node/release/linux-x64/node-v${version}-linux-x64.tar.gz"

echo "Downloading Node.js ${version}..."
local code
code=$(curl "${url}" -L --silent --fail --retry 5 --retry-max-time 15 --retry-connrefused --connect-timeout 5 -o /tmp/node.tar.gz --write-out "%{http_code}")
if [[ "${code}" != "200" ]]; then
echo "Unable to download Node.js version ${version}: ${code}"
return 1
fi

tar xzf /tmp/node.tar.gz -C /tmp
mv "/tmp/node-v${version}-linux-x64" "${dir}"
chmod +x "${dir}/bin/"*
}

function detect_and_install_nodejs() {
local buildDir="${1}"
if [[ ! -d "${buildDir}/.heroku/nodejs" ]] && [[ "true" != "${SKIP_NODEJS_INSTALL:-}" ]]; then
if grep -q lein-npm "${buildDir}/project.clj" || [[ -n "${NODEJS_VERSION:-}" ]]; then
local nodejsVersion="${NODEJS_VERSION:-18.16.0}"
output::step "Installing Node.js ${nodejsVersion}"
install_nodejs "${nodejsVersion}" "${buildDir}/.heroku/nodejs" 2>&1 | output::indent
export PATH="${buildDir}/.heroku/nodejs/bin:${PATH}"
fi
fi
}

function install_jdk() {
local install_dir="${1}"

Expand Down
Loading
Loading