Skip to content

Commit 681ef10

Browse files
authored
Properly parse project.clj instead of grepping (#198)
1 parent 48510dd commit 681ef10

File tree

9 files changed

+152
-96
lines changed

9 files changed

+152
-96
lines changed

bin/compile

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ ENV_DIR="${3}"
1010

1111
# shellcheck source=lib/util.sh
1212
source "${BUILDPACK_DIR}/lib/util.sh"
13-
# shellcheck source=lib/lein.sh
14-
source "${BUILDPACK_DIR}/lib/lein.sh"
1513
# shellcheck source=lib/metrics.sh
1614
source "${BUILDPACK_DIR}/lib/metrics.sh"
1715
# shellcheck source=lib/output.sh
@@ -24,7 +22,62 @@ util::export_env_dir "${ENV_DIR}" "." "JAVA_OPTS"
2422
metrics::init "${CACHE_DIR}" "clojure"
2523
metrics::setup
2624

27-
if grep -q lein-npm "${BUILD_DIR}/project.clj"; then
25+
# To ensure the cache created by older Clojure buildpack versions doesn't stay around indefinitely, we explicitly
26+
# delete these old directories. This will speed up subsequent app builds since it will take less time to restore
27+
# the cache before each build.
28+
#
29+
# Note: This buildpack used to install and run npm. The node_modules dir is specific to the old Clojure install
30+
# and does not overlap with the official Node.js buildpack.
31+
rm -rf \
32+
"${CACHE_DIR}/clojure-bp-apt" \
33+
"${CACHE_DIR}/node_modules"
34+
35+
openjdk::install_openjdk_via_jvm_common_buildpack "${BUILD_DIR}" "${BUILDPACK_DIR}"
36+
37+
util::cache_copy ".m2" "${CACHE_DIR}" "${BUILD_DIR}"
38+
39+
# Install rlwrap shim for Leiningen 1.x and clj REPLs.
40+
# rlwrap is technically a dependency for both, but it only enhances REPL functionality
41+
# with line editing and command history. This shim allows clj and lein 1.x to run without
42+
# errors, but does not provide actual readline capabilities. Most users don't use REPLs
43+
# in a Heroku context. Users can install their own rlwrap via the APT buildpack and this
44+
# shim will not interfere.
45+
mkdir -p "${BUILD_DIR}/.heroku/bin"
46+
cp "${BUILDPACK_DIR}/opt/rlwrap" "${BUILD_DIR}/.heroku/bin/rlwrap"
47+
48+
# Run clojure install script (clojure / clj may be needed from leiningen for newer cli tools)
49+
CLOJURE_CLI_VERSION="${CLOJURE_CLI_VERSION:-1.10.0.411}"
50+
51+
output::step "Installing Clojure ${CLOJURE_CLI_VERSION} CLI tools"
52+
53+
CLOJURE_INSTALL_SCRIPT="$(mktemp)"
54+
curl --retry 3 --retry-connrefused --connect-timeout 5 -sSfL --max-time 60 -o "${CLOJURE_INSTALL_SCRIPT}" "https://download.clojure.org/install/linux-install-${CLOJURE_CLI_VERSION}.sh"
55+
chmod +x "${CLOJURE_INSTALL_SCRIPT}"
56+
57+
# Clojure CLI is non-portable as it embeds absolute paths during installation.
58+
# Create a symlink before installation to ensure those paths work at both compile and runtime.
59+
CLOJURE_CLI_DIR=".heroku/clj"
60+
mkdir -p "${BUILD_DIR}/${CLOJURE_CLI_DIR}"
61+
if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then
62+
mkdir -p "/app/$(dirname "${CLOJURE_CLI_DIR}")"
63+
ln -nsf "${BUILD_DIR}/${CLOJURE_CLI_DIR}" "/app/${CLOJURE_CLI_DIR}"
64+
fi
65+
66+
"${CLOJURE_INSTALL_SCRIPT}" --prefix "/app/${CLOJURE_CLI_DIR}" 2>/dev/null | output::indent
67+
chmod +x "/app/${CLOJURE_CLI_DIR}/bin/"*
68+
export PATH="/app/${CLOJURE_CLI_DIR}/bin:${PATH}"
69+
70+
# Pre-warm Clojure CLI cache to avoid download messages for its own dependencies when its used for the first time by
71+
# the buildpack or the user at runtime.
72+
clojure -e "(println)" &>/dev/null
73+
74+
output::step "Reading Leiningen project properties"
75+
76+
lein_project_plugins=$("${BUILDPACK_DIR}/opt/get_project_property.clj" "${BUILD_DIR}/project.clj" "plugins")
77+
lein_project_uberjar_name=$("${BUILDPACK_DIR}/opt/get_project_property.clj" "${BUILD_DIR}/project.clj" "uberjar-name")
78+
lein_project_min_lein_version=$("${BUILDPACK_DIR}/opt/get_project_property.clj" "${BUILD_DIR}/project.clj" "min-lein-version")
79+
80+
if grep -q lein-npm <<<"${lein_project_plugins}"; then
2881
if ! command -v npm &>/dev/null; then
2982
output::error <<-EOF
3083
Error: Your project.clj references lein-npm but npm is not available.
@@ -46,52 +99,31 @@ if grep -q lein-npm "${BUILD_DIR}/project.clj"; then
4699
fi
47100
fi
48101

49-
openjdk::install_openjdk_via_jvm_common_buildpack "${BUILD_DIR}" "${BUILDPACK_DIR}"
50-
51-
# Install rlwrap shim for Leiningen 1.x and clj REPLs.
52-
# rlwrap is technically a dependency for both, but it only enhances REPL functionality
53-
# with line editing and command history. This shim allows clj and lein 1.x to run without
54-
# errors, but does not provide actual readline capabilities. Most users don't use REPLs
55-
# in a Heroku context. Users can install their own rlwrap via the APT buildpack and this
56-
# shim will not interfere.
57-
mkdir -p "${BUILD_DIR}/.heroku/bin"
58-
cp "${BUILDPACK_DIR}/opt/rlwrap" "${BUILD_DIR}/.heroku/bin/rlwrap"
59-
60-
# To ensure the cache created by older Clojure buildpack versions doesn't stay around indefinitely, we explicitly
61-
# delete these old directories. This will speed up subsequent app builds since it will take less time to restore
62-
# the cache before each build.
63-
#
64-
# Note: This buildpack used to install and run npm. The node_modules dir is specific to the old Clojure install
65-
# and does not overlap with the official Node.js buildpack.
66-
rm -rf \
67-
"${CACHE_DIR}/clojure-bp-apt" \
68-
"${CACHE_DIR}/node_modules"
69-
70-
# Run clojure install script (clojure / clj may be needed from leiningen for newer cli tools)
71-
CLOJURE_CLI_VERSION="${CLOJURE_CLI_VERSION:-1.10.0.411}"
72-
output::step "Installing Clojure ${CLOJURE_CLI_VERSION} CLI tools"
73-
CLOJURE_INSTALL_NAME="linux-install-${CLOJURE_CLI_VERSION}.sh"
74-
CLOJURE_INSTALL_URL="https://download.clojure.org/install/${CLOJURE_INSTALL_NAME}"
75-
curl --retry 3 --retry-connrefused --connect-timeout 5 -sSfL --max-time 60 -o "/tmp/${CLOJURE_INSTALL_NAME}" "${CLOJURE_INSTALL_URL}"
76-
chmod +x "/tmp/${CLOJURE_INSTALL_NAME}"
77-
mkdir -p "${BUILD_DIR}/.heroku/clj"
78-
"/tmp/${CLOJURE_INSTALL_NAME}" --prefix "${BUILD_DIR}/.heroku/clj" 2>/dev/null | output::indent
79-
chmod +x "${BUILD_DIR}/.heroku/clj/bin/"*
80-
export PATH="${BUILD_DIR}/.heroku/clj/bin:${PATH}"
81-
82102
# Check for vendored lein script
83103
if [[ -x "${BUILD_DIR}/bin/lein" ]]; then
84104
output::step "Using vendored Leiningen at bin/lein"
85105
LEIN_BIN_PATH="${BUILD_DIR}/bin/lein"
86-
calculate_lein_build_task "${BUILD_DIR}"
106+
if [[ -n "${lein_project_uberjar_name}" ]]; then
107+
export LEIN_BUILD_TASK="${LEIN_BUILD_TASK:-uberjar}"
108+
export LEIN_INCLUDE_IN_SLUG="${LEIN_INCLUDE_IN_SLUG:-no}"
109+
else
110+
export LEIN_BUILD_TASK="${LEIN_BUILD_TASK:-with-profile production compile :all}"
111+
fi
87112
"${LEIN_BIN_PATH}" version 2>/dev/null | output::indent
88113
else
89114
# Determine Leiningen version
90-
if is_lein_2 "${BUILD_DIR}"; then
115+
case "${lein_project_min_lein_version}" in
116+
2*)
91117
LEIN_VERSION="2.9.1"
92118
LEIN_BIN_SOURCE="$(dirname "${0}")/../opt/lein2"
93-
calculate_lein_build_task "${BUILD_DIR}"
94-
else
119+
if [[ -n "${lein_project_uberjar_name}" ]]; then
120+
export LEIN_BUILD_TASK="${LEIN_BUILD_TASK:-uberjar}"
121+
export LEIN_INCLUDE_IN_SLUG="${LEIN_INCLUDE_IN_SLUG:-no}"
122+
else
123+
export LEIN_BUILD_TASK="${LEIN_BUILD_TASK:-with-profile production compile :all}"
124+
fi
125+
;;
126+
*)
95127
LEIN_VERSION="1.7.1"
96128
LEIN_BIN_SOURCE="$(dirname "${0}")/../opt/lein1"
97129
LEIN_BUILD_TASK="${LEIN_BUILD_TASK:-deps}"
@@ -103,7 +135,8 @@ else
103135
WARNING: No :min-lein-version found in project.clj; using ${LEIN_VERSION}.
104136
You probably don't want this!
105137
EOF
106-
fi
138+
;;
139+
esac
107140

108141
# install leiningen jar
109142
LEIN1_JAR_URL="https://lang-jvm.s3.us-east-1.amazonaws.com/leiningen-${LEIN_VERSION}-standalone.jar"
@@ -153,11 +186,6 @@ if [[ ! -e "${BUILD_DIR}/.lein/profiles.clj" ]]; then
153186
echo '{}' >"${BUILD_DIR}/.lein/profiles.clj"
154187
fi
155188

156-
# unpack existing cache
157-
if [[ ! -d "${BUILD_DIR}/.m2" ]]; then
158-
util::cache_copy ".m2" "${CACHE_DIR}" "${BUILD_DIR}"
159-
fi
160-
161189
output::step "Building with Leiningen"
162190

163191
# Calculate build command
@@ -194,10 +222,12 @@ mkdir -p "$(dirname "${PROFILE_PATH}")"
194222
echo "export RING_ENV=\"\${RING_ENV:-production}\""
195223
} >>"${PROFILE_PATH}"
196224

197-
# rewrite Clojure CLI path
198-
mv "${BUILD_DIR}/.heroku/clj/bin/clojure" "${BUILD_DIR}/.heroku/clj/bin/clojure.old"
199-
sed -e "s/\/tmp\/$(basename "${BUILD_DIR}")/\/app/g" "${BUILD_DIR}/.heroku/clj/bin/clojure.old" >"${BUILD_DIR}/.heroku/clj/bin/clojure"
200-
chmod +x "${BUILD_DIR}/.heroku/clj/bin/clojure"
225+
# Write export script for use by subsequent buildpacks. The export script sets up the environment so that other
226+
# buildpacks in the chain can access Clojure CLI and Leiningen during their build phase. This script
227+
# is sourced by the buildpack framework between buildpack executions.
228+
cat <<-EOF >>"${BUILDPACK_DIR}/export"
229+
export PATH="/app/.heroku/bin:/app/${CLOJURE_CLI_DIR}/bin:/app/.lein/bin:\${PATH}"
230+
EOF
201231

202232
# repack cache with new assets
203233
mkdir -p "${CACHE_DIR}"

bin/release

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ BUILDPACK_DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)"
66

77
BUILD_DIR="${1}"
88

9-
# shellcheck source=lib/lein.sh
10-
source "${BUILDPACK_DIR}/lib/lein.sh"
9+
# Unfortunately the build system doesn't source the `export` script before
10+
# running `bin/release`, so we have to do so manually to ensure Clojure CLI
11+
# and OpenJDK are available for the property extraction script.
12+
# shellcheck source=/dev/null
13+
source "${BUILDPACK_DIR}/export"
14+
15+
lein_project_uberjar_name=$("${BUILDPACK_DIR}/opt/get_project_property.clj" "${BUILD_DIR}/project.clj" "uberjar-name")
16+
lein_project_min_lein_version=$("${BUILDPACK_DIR}/opt/get_project_property.clj" "${BUILD_DIR}/project.clj" "min-lein-version")
1117

1218
if [[ ! -f "${BUILD_DIR}/Procfile" ]]; then
1319
cat <<EOF
@@ -16,13 +22,13 @@ config_vars:
1622
default_process_types:
1723
EOF
1824

19-
if [[ "$(calculate_lein_build_task "${BUILD_DIR}")" == "uberjar" ]]; then
25+
if [[ -n "${lein_project_uberjar_name}" ]]; then
2026
cd "${BUILD_DIR}" || exit
2127
while IFS= read -r -d '' jarFile; do
2228
echo " web: java -jar ${jarFile}"
2329
break
2430
done < <(find target -maxdepth 1 -name "*.jar" -type f -print0)
25-
elif is_lein_2 "${BUILD_DIR}"; then
31+
elif [[ "${lein_project_min_lein_version}" == 2* ]]; then
2632
echo " web: lein with-profile production trampoline run"
2733
else
2834
echo " web: lein trampoline run"

lib/lein.sh

Lines changed: 0 additions & 22 deletions
This file was deleted.

opt/get_project_property.clj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env clojure
2+
3+
;; This script extracts properties from project.clj files.
4+
;;
5+
;; This leverages Clojure's homoiconicity (code is data) to parse project.clj as a data
6+
;; structure. Since project.clj is valid Clojure code, we can use read-string to parse it
7+
;; and extract properties reliably.
8+
;;
9+
;; This is much more robust than grep because it:
10+
;; - Handles multi-line values correctly
11+
;; - Respects Clojure syntax (strings, nested structures, comments)
12+
;; - Won't be fooled by property names appearing in comments or strings
13+
;; - Properly handles quoted strings and escape sequences
14+
15+
(when (< (count *command-line-args*) 2)
16+
(binding [*out* *err*]
17+
(println "Usage: get_project_property.clj <project.clj> <property-name>"))
18+
(System/exit 1))
19+
20+
(defn get-project-property [file-path property-name]
21+
(let [project-data (read-string (slurp file-path))
22+
properties (apply hash-map (drop 3 project-data))]
23+
(get properties (keyword property-name))))
24+
25+
(let [file-path (first *command-line-args*)
26+
property-name (second *command-line-args*)
27+
property-value (get-project-property file-path property-name)]
28+
(println (or property-value "")))

test/spec/ci_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Installing man pages into /app/.heroku/clj/share/man/man1
1818
Removing download
1919
Use clj -h for help.
20+
-----> Reading Leiningen project properties
2021
-----> Installing Leiningen
2122
Downloading: leiningen-2.9.1-standalone.jar
2223
Writing: lein script
@@ -125,7 +126,6 @@
125126
Retrieving $DEPENDENCY from $REPO
126127
Retrieving $DEPENDENCY from $REPO
127128
Retrieving $DEPENDENCY from $REPO
128-
Retrieving $DEPENDENCY from $REPO
129129
-----> No test-setup command provided. Skipping.
130130
-----> Running Clojure buildpack tests...
131131
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -XX:MaxRAM=2684354560 -XX:MaxRAMPercentage=80.0
@@ -156,6 +156,7 @@
156156
Installing man pages into /app/.heroku/clj/share/man/man1
157157
Removing download
158158
Use clj -h for help.
159+
-----> Reading Leiningen project properties
159160
-----> Using cached Leiningen 2.9.1
160161
Writing: lein script
161162
-----> Building with Leiningen

test/spec/compile_spec.rb

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
remote: -----> Installing Azul Zulu OpenJDK $VERSION
1212
remote: -----> Installing Clojure 1.10.0.411 CLI tools
1313
remote: Downloading and expanding tar
14-
remote: Installing libs into $BUILD_DIR/.heroku/clj/lib/clojure
15-
remote: Installing clojure and clj into $BUILD_DIR/.heroku/clj/bin
16-
remote: Installing man pages into $BUILD_DIR/.heroku/clj/share/man/man1
14+
remote: Installing libs into /app/.heroku/clj/lib/clojure
15+
remote: Installing clojure and clj into /app/.heroku/clj/bin
16+
remote: Installing man pages into /app/.heroku/clj/share/man/man1
1717
remote: Removing download
1818
remote: Use clj -h for help.
19+
remote: -----> Reading Leiningen project properties
1920
remote: -----> Installing Leiningen
2021
remote: Downloading: leiningen-2.9.1-standalone.jar
2122
remote: Writing: lein script
@@ -143,11 +144,12 @@
143144
remote: -----> Installing Azul Zulu OpenJDK $VERSION
144145
remote: -----> Installing Clojure 1.10.0.411 CLI tools
145146
remote: Downloading and expanding tar
146-
remote: Installing libs into $BUILD_DIR/.heroku/clj/lib/clojure
147-
remote: Installing clojure and clj into $BUILD_DIR/.heroku/clj/bin
148-
remote: Installing man pages into $BUILD_DIR/.heroku/clj/share/man/man1
147+
remote: Installing libs into /app/.heroku/clj/lib/clojure
148+
remote: Installing clojure and clj into /app/.heroku/clj/bin
149+
remote: Installing man pages into /app/.heroku/clj/share/man/man1
149150
remote: Removing download
150151
remote: Use clj -h for help.
152+
remote: -----> Reading Leiningen project properties
151153
remote: -----> Using cached Leiningen 2.9.1
152154
remote: Writing: lein script
153155
remote: -----> Building with Leiningen
@@ -177,11 +179,12 @@
177179
remote: -----> Installing Azul Zulu OpenJDK $VERSION
178180
remote: -----> Installing Clojure 1.10.0.411 CLI tools
179181
remote: Downloading and expanding tar
180-
remote: Installing libs into $BUILD_DIR/.heroku/clj/lib/clojure
181-
remote: Installing clojure and clj into $BUILD_DIR/.heroku/clj/bin
182-
remote: Installing man pages into $BUILD_DIR/.heroku/clj/share/man/man1
182+
remote: Installing libs into /app/.heroku/clj/lib/clojure
183+
remote: Installing clojure and clj into /app/.heroku/clj/bin
184+
remote: Installing man pages into /app/.heroku/clj/share/man/man1
183185
remote: Removing download
184186
remote: Use clj -h for help.
187+
remote: -----> Reading Leiningen project properties
185188
remote: -----> Installing Leiningen
186189
remote: Downloading: leiningen-2.9.1-standalone.jar
187190
remote: Writing: lein script
@@ -306,11 +309,12 @@
306309
remote: -----> Installing Azul Zulu OpenJDK $VERSION
307310
remote: -----> Installing Clojure 1.10.0.411 CLI tools
308311
remote: Downloading and expanding tar
309-
remote: Installing libs into $BUILD_DIR/.heroku/clj/lib/clojure
310-
remote: Installing clojure and clj into $BUILD_DIR/.heroku/clj/bin
311-
remote: Installing man pages into $BUILD_DIR/.heroku/clj/share/man/man1
312+
remote: Installing libs into /app/.heroku/clj/lib/clojure
313+
remote: Installing clojure and clj into /app/.heroku/clj/bin
314+
remote: Installing man pages into /app/.heroku/clj/share/man/man1
312315
remote: Removing download
313316
remote: Use clj -h for help.
317+
remote: -----> Reading Leiningen project properties
314318
remote: -----> Using cached Leiningen 2.9.1
315319
remote: Writing: lein script
316320
remote: -----> Building with Leiningen

test/spec/config_spec.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@
3131
it 'respects BUILD_COMMAND environment variable override' do
3232
new_default_hatchet_runner('lein-2.x-with-uberjar').tap do |app|
3333
app.before_deploy do
34-
app.set_config('BUILD_COMMAND' => 'echo "Custom build command executed" && lein deps')
34+
app.set_config('BUILD_COMMAND' => 'lein deps')
3535
end
3636

3737
app.deploy do
38-
expect(clean_output(app.output)).to include('Running: echo "Custom build command executed" && lein deps')
39-
expect(clean_output(app.output)).to include('Custom build command executed')
38+
expect(clean_output(app.output)).to include('Running: lein deps')
4039
expect(clean_output(app.output)).not_to include('Running: lein uberjar')
4140
end
4241
end

test/spec/lein_1_spec.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
remote: -----> Installing Azul Zulu OpenJDK $VERSION
1212
remote: -----> Installing Clojure 1.10.0.411 CLI tools
1313
remote: Downloading and expanding tar
14-
remote: Installing libs into $BUILD_DIR/.heroku/clj/lib/clojure
15-
remote: Installing clojure and clj into $BUILD_DIR/.heroku/clj/bin
16-
remote: Installing man pages into $BUILD_DIR/.heroku/clj/share/man/man1
14+
remote: Installing libs into /app/.heroku/clj/lib/clojure
15+
remote: Installing clojure and clj into /app/.heroku/clj/bin
16+
remote: Installing man pages into /app/.heroku/clj/share/man/man1
1717
remote: Removing download
1818
remote: Use clj -h for help.
19+
remote: -----> Reading Leiningen project properties
1920
2021
remote: ! WARNING: No :min-lein-version found in project.clj; using 1.7.1.
2122
remote: ! You probably don't want this!

0 commit comments

Comments
 (0)