From 7a14d31fcff11a57250e918c1d0048d5b656e784 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 11:55:34 +0900 Subject: [PATCH 01/13] Route entrypoint commands without probing project code --- resources/entrypoint | 24 ++++--- test/docker_clojure/entrypoint_test.clj | 93 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 test/docker_clojure/entrypoint_test.clj diff --git a/resources/entrypoint b/resources/entrypoint index 3a50a540..df1ad4ee 100755 --- a/resources/entrypoint +++ b/resources/entrypoint @@ -4,18 +4,26 @@ set -eou pipefail entrypoint=@@entrypoint@@ -cmd=${1:-} - -# check if the first arg starts with a hyphen +# No arguments or a leading flag belong to the image's build tool. if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then exec "${entrypoint}" "$@" fi -if [[ -n "${cmd}" ]]; then - # see if help for the subcommand is successful - if "${entrypoint}" "${cmd}" --help >/dev/null 2>&1; then - exec "${entrypoint}" "$@" - fi +# Built-in Leiningen tasks and aliases take precedence over executables such as +# test and cp. Keep this Leiningen 2.13.0 list in sync when upgrading; never probe a +# command with --help, which can evaluate code from the current project. +if [ "${entrypoint}" = lein ]; then + case "$1" in + change|check|classpath|clean|compile|cp|deploy|deps|do|halp|help|install|jar|javac|\ + new|plugin|pom|readme|release|repl|retest|run|sample|search|show-profiles|test|\ + trampoline|tutorial|uberjar|update-in|upgrade|vcs|version|with-profile|with-profiles|überjar) + exec "${entrypoint}" "$@" + ;; + esac +elif [ -f "$1" ] && ! type -P "$1" >/dev/null; then + # Non-executable Clojure scripts run once, through clj. + exec "${entrypoint}" "$@" fi +# Other commands run directly. Use explicit lein for custom tasks and aliases. exec "$@" diff --git a/test/docker_clojure/entrypoint_test.clj b/test/docker_clojure/entrypoint_test.clj new file mode 100644 index 00000000..7d435c8c --- /dev/null +++ b/test/docker_clojure/entrypoint_test.clj @@ -0,0 +1,93 @@ +(ns docker-clojure.entrypoint-test + (:require [clojure.java.io :as io] + [clojure.java.shell :refer [sh]] + [clojure.string :as str] + [clojure.test :refer [deftest is testing]] + [docker-clojure.dockerfile :as df]) + (:import (java.nio.file Files) + (java.nio.file.attribute FileAttribute))) + +(defn- nul-fields [s] + (vec (butlast (str/split s #"\x00" -1)))) + +(defn- run-entrypoint [build-tool & args] + (let [dir (.toFile (Files/createTempDirectory "entrypoint test " (make-array FileAttribute 0))) + bin (io/file dir "bin") + log (io/file dir "calls")] + (try + (.mkdirs bin) + (df/shared-prereqs (str bin) {:build-tool build-tool}) + ;; Record every tool invocation, including probes with hidden output. + (doseq [tool ["lein" "clj"]] + (let [file (io/file bin tool)] + (spit file (str "#!/bin/sh\nprintf '%s\\0' '" tool "' \"$@\" >> \"$CALLS_LOG\"\n")) + (.setExecutable file true false))) + (spit (io/file dir "example.clj") "(println :example)") + ;; A local script must not take precedence over the executable on PATH. + (spit (io/file dir "echo") "(throw (Exception. \"unexpected project execution\"))") + (spit (io/file dir "launch") "#!/bin/sh\nprintf '%s\\n' external\n") + (.setExecutable (io/file dir "launch") true false) + (let [result (apply sh (str (io/file bin "entrypoint")) + (concat args [:dir (str dir) + :env {"PATH" (str bin ":/usr/bin:/bin") + "CALLS_LOG" (str log)}]))] + (assoc result :calls (if (.exists log) (nul-fields (slurp log)) []))) + (finally + (doseq [file (reverse (file-seq dir))] + (io/delete-file file)))))) + +(deftest external-commands-do-not-probe-test + (doseq [build-tool ["lein" "tools-deps" :docker-clojure.core/all]] + (testing (str build-tool " runs executables without launching a build tool") + (is (= {:exit 0 :out "hello\n" :err "" :calls []} + (run-entrypoint build-tool "echo" "hello"))) + (is (= {:exit 0 :out "external\n" :err "" :calls []} + (run-entrypoint build-tool "./launch"))) + (is (= {:exit 23 :out "" :err "failed\n" :calls []} + (run-entrypoint build-tool "/bin/sh" "-c" "echo failed >&2; exit 23")))))) + +(deftest defaults-and-flags-test + (doseq [[build-tool tool] [["lein" "lein"] ["tools-deps" "clj"] + [:docker-clojure.core/all "clj"]] + args [[] ["-h"] ["-M" "-e" "(+ 1 2)"]]] + (let [result (apply run-entrypoint build-tool args)] + (is (= 0 (:exit result))) + (is (= (into [tool] args) (:calls result)))))) + +(deftest lein-shorthand-and-explicit-invocation-test + (testing "built-in tasks and aliases keep precedence over executables" + (doseq [task ["repl" "test" "cp" "install" "help" "uberjar" "überjar"]] + (is (= ["lein" task] (:calls (run-entrypoint "lein" task)))))) + (testing "explicit commands and paths bypass shorthand routing" + (is (= ["lein" "my-alias"] (:calls (run-entrypoint "lein" "lein" "my-alias")))) + (is (= {:exit 0 :out "" :err "" :calls []} + (run-entrypoint "lein" "/bin/test" "1" "=" "1")))) + (testing "custom tasks use explicit Leiningen invocation" + (is (= ["lein" "my-task"] (:calls (run-entrypoint "lein" "lein" "my-task")))))) + +(deftest clojure-scripts-run-once-test + (doseq [build-tool ["tools-deps" :docker-clojure.core/all] + script ["example.clj" "./example.clj"]] + (let [result (run-entrypoint build-tool script "argument")] + (is (= 0 (:exit result))) + (is (= ["clj" script "argument"] (:calls result)))))) + +(deftest unknown-commands-do-not-launch-project-test + (doseq [build-tool ["lein" "tools-deps" :docker-clojure.core/all]] + (let [result (run-entrypoint build-tool "no-such-command")] + (is (= 127 (:exit result))) + (is (str/includes? (:err result) "no-such-command")) + (is (= [] (:calls result)))))) + +(deftest literal-arguments-test + (let [args ["" "two words" "*.clj" "line one\nline two" "$(exit 99)" "'quoted'"]] + (doseq [[build-tool command] [["lein" "test"] ["tools-deps" "-M"]]] + (let [result (apply run-entrypoint build-tool command args)] + (is (= 0 (:exit result))) + (is (= (into [(if (= build-tool "lein") "lein" "clj") command] args) + (:calls result))))) + (doseq [build-tool ["lein" "tools-deps"]] + (let [result (apply run-entrypoint build-tool "printf" "%s\\0" args)] + (is (= 0 (:exit result))) + (is (= args (nul-fields (:out result)))) + (is (= [] (:calls result))))))) From 2736293e63706692030d57cf79ea70b7edcc22b8 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 11:56:05 +0900 Subject: [PATCH 02/13] Document explicit invocation for custom Leiningen commands --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 436bb516..c4de743c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,41 @@ Java 21 tools-deps on Alpine: `clojure:temurin-21-tools-deps-alpine` or `clojure ## Examples +### Running commands + +With JDK 17+ images, you can pass built-in Leiningen tasks and aliases directly: + +```sh +docker run --rm clojure:lein test +docker run --rm clojure:lein cp +``` + +For custom tasks and aliases defined by your project or plugins, always use +explicit `lein ` invocation. The entrypoint only recognises +built-in names for shorthand invocation: + +```sh +docker run --rm -v "$PWD":/app -w /app clojure:lein lein my-alias +``` + +Executables such as `bash` and `git` run directly, without first loading your +Clojure project. If a custom task or alias shares an executable's name, the +executable takes precedence; use explicit `lein` to select the task or alias. +Earlier images probed Leiningen to discover these aliases, which could execute +project code before running an unrelated command. + +JDK 17+ tools-deps images accept Clojure flags and script files directly: + +```sh +docker run --rm clojure:tools-deps -M -e '(println (+ 1 2))' +docker run --rm -v "$PWD":/app -w /app clojure:tools-deps example.clj +``` + +With older JDK images, invoke `lein` or `clj` explicitly. + +To run an executable whose name is also a built-in Leiningen task or alias, +use its path (for example, `/bin/cp`) or Docker's `--entrypoint` option. + ### Interactive Shell Run an interactive shell from this image. From bacd99aa17ba82e4607c0d02bc136d7faa523dd9 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:01:45 +0900 Subject: [PATCH 03/13] Shorten command usage examples --- README.md | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c4de743c..c62ebd8d 100644 --- a/README.md +++ b/README.md @@ -107,38 +107,25 @@ Java 21 tools-deps on Alpine: `clojure:temurin-21-tools-deps-alpine` or `clojure ### Running commands -With JDK 17+ images, you can pass built-in Leiningen tasks and aliases directly: +For JDK 17+ images: ```sh +# Built-in Leiningen tasks and aliases docker run --rm clojure:lein test docker run --rm clojure:lein cp -``` - -For custom tasks and aliases defined by your project or plugins, always use -explicit `lein ` invocation. The entrypoint only recognises -built-in names for shorthand invocation: -```sh +# Custom tasks and aliases: always include lein docker run --rm -v "$PWD":/app -w /app clojure:lein lein my-alias -``` - -Executables such as `bash` and `git` run directly, without first loading your -Clojure project. If a custom task or alias shares an executable's name, the -executable takes precedence; use explicit `lein` to select the task or alias. -Earlier images probed Leiningen to discover these aliases, which could execute -project code before running an unrelated command. -JDK 17+ tools-deps images accept Clojure flags and script files directly: - -```sh +# Clojure flags and scripts docker run --rm clojure:tools-deps -M -e '(println (+ 1 2))' docker run --rm -v "$PWD":/app -w /app clojure:tools-deps example.clj ``` -With older JDK images, invoke `lein` or `clj` explicitly. +Programs such as `bash` and `git` run directly. Use `/bin/cp` to select the +executable instead of Leiningen's `cp` alias. -To run an executable whose name is also a built-in Leiningen task or alias, -use its path (for example, `/bin/cp`) or Docker's `--entrypoint` option. +With older JDK images, invoke `lein` or `clj` explicitly. ### Interactive Shell From 50b5db2604ba5c4ca1adf9a10cb7ff0b292f2de8 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:04:37 +0900 Subject: [PATCH 04/13] Show explicit clj invocation for deps.edn aliases --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c62ebd8d..f4f91c7c 100644 --- a/README.md +++ b/README.md @@ -114,12 +114,15 @@ For JDK 17+ images: docker run --rm clojure:lein test docker run --rm clojure:lein cp -# Custom tasks and aliases: always include lein +# Custom Leiningen tasks and aliases: always include lein docker run --rm -v "$PWD":/app -w /app clojure:lein lein my-alias # Clojure flags and scripts docker run --rm clojure:tools-deps -M -e '(println (+ 1 2))' docker run --rm -v "$PWD":/app -w /app clojure:tools-deps example.clj + +# deps.edn aliases +docker run --rm -v "$PWD":/app -w /app clojure:tools-deps clj -M:my-alias ``` Programs such as `bash` and `git` run directly. Use `/bin/cp` to select the From 892147455a1407341bbc2cda0ea448dc41e55e02 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:07:53 +0900 Subject: [PATCH 05/13] Lead README examples with the interactive shell --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f4f91c7c..dcf2aaf1 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,23 @@ Java 21 tools-deps on Alpine: `clojure:temurin-21-tools-deps-alpine` or `clojure ## Examples -### Running commands +### Interactive shell + +Run an interactive shell from this image. + +``` +docker run -ti clojure bash +``` + +Then within the shell, create a new Leiningen project and start a Clojure REPL. + +``` +lein new hello-world +cd hello-world +lein repl +``` + +### Running individual commands For JDK 17+ images: @@ -130,22 +146,6 @@ executable instead of Leiningen's `cp` alias. With older JDK images, invoke `lein` or `clj` explicitly. -### Interactive Shell - -Run an interactive shell from this image. - -``` -docker run -ti clojure bash -``` - -Then within the shell, create a new Leiningen project and start a Clojure REPL. - -``` -lein new hello-world -cd hello-world -lein repl -``` - ## Builds The Dockerfiles are generated by the `docker-clojure` Clojure app in this repo. From 34ee2448d30ec329940e36ccf6a51d12fe37d0e9 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:14:31 +0900 Subject: [PATCH 06/13] Route lein static-classpath through the entrypoint Leiningen 2.11.0 added the static-classpath task, which the built-in task list missed. Co-Authored-By: Claude Opus 5.5 --- resources/entrypoint | 5 +++-- test/docker_clojure/entrypoint_test.clj | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/entrypoint b/resources/entrypoint index df1ad4ee..e4e000c0 100755 --- a/resources/entrypoint +++ b/resources/entrypoint @@ -15,8 +15,9 @@ fi if [ "${entrypoint}" = lein ]; then case "$1" in change|check|classpath|clean|compile|cp|deploy|deps|do|halp|help|install|jar|javac|\ - new|plugin|pom|readme|release|repl|retest|run|sample|search|show-profiles|test|\ - trampoline|tutorial|uberjar|update-in|upgrade|vcs|version|with-profile|with-profiles|überjar) + new|plugin|pom|readme|release|repl|retest|run|sample|search|show-profiles|\ + static-classpath|test|trampoline|tutorial|uberjar|update-in|upgrade|vcs|version|\ + with-profile|with-profiles|überjar) exec "${entrypoint}" "$@" ;; esac diff --git a/test/docker_clojure/entrypoint_test.clj b/test/docker_clojure/entrypoint_test.clj index 7d435c8c..9c759284 100644 --- a/test/docker_clojure/entrypoint_test.clj +++ b/test/docker_clojure/entrypoint_test.clj @@ -56,7 +56,8 @@ (deftest lein-shorthand-and-explicit-invocation-test (testing "built-in tasks and aliases keep precedence over executables" - (doseq [task ["repl" "test" "cp" "install" "help" "uberjar" "überjar"]] + (doseq [task ["repl" "test" "cp" "install" "help" "static-classpath" + "uberjar" "überjar"]] (is (= ["lein" task] (:calls (run-entrypoint "lein" task)))))) (testing "explicit commands and paths bypass shorthand routing" (is (= ["lein" "my-alias"] (:calls (run-entrypoint "lein" "lein" "my-alias")))) From e16d268c4f1f559a785f5625540a14c5e3507849 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:18:40 +0900 Subject: [PATCH 07/13] Smoke test that lein images route every built-in lein command The entrypoint hardcodes Leiningen's tasks and aliases. Lein images with the entrypoint now list the tasks shipped in the Leiningen jar, plus its aliases, and check the installed entrypoint passes each one to lein, so a Leiningen upgrade that adds a task fails the image build. lein help is not a usable source: it omits static-classpath. Co-Authored-By: Claude Opus 5.5 --- resources/entrypoint | 5 ++-- src/docker_clojure/docker.clj | 52 ++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/resources/entrypoint b/resources/entrypoint index e4e000c0..d7954afc 100755 --- a/resources/entrypoint +++ b/resources/entrypoint @@ -10,8 +10,9 @@ if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then fi # Built-in Leiningen tasks and aliases take precedence over executables such as -# test and cp. Keep this Leiningen 2.13.0 list in sync when upgrading; never probe a -# command with --help, which can evaluate code from the current project. +# test and cp. Keep this Leiningen 2.13.0 list in sync when upgrading (the image +# smoke test checks it); never probe a command with --help, which can evaluate +# code from the current project. if [ "${entrypoint}" = lein ]; then case "$1" in change|check|classpath|clean|compile|cp|deploy|deps|do|halp|help|install|jar|javac|\ diff --git a/src/docker_clojure/docker.clj b/src/docker_clojure/docker.clj index bbdcd804..448a973f 100644 --- a/src/docker_clojure/docker.clj +++ b/src/docker_clojure/docker.clj @@ -12,14 +12,58 @@ (defn pull-image [image] (sh "docker" "pull" image)) +(def ^:private print-lein-commands + "Clojure form that prints every built-in Leiningen task and alias, one per + line. Tasks come from the namespaces in the Leiningen jar because `lein help` + omits tasks missing from its own stock list." + '(do + (require 'leiningen.core.main) + (let [jar (java.util.jar.JarFile. (System/getProperty "java.class.path")) + tasks (for [entry (enumeration-seq (.entries jar)) + :let [[_ file] (re-find #"^leiningen/([^/]+)\.clj$" + (.getName entry))] + :when file + :let [task (.replace file \_ \-) + task-ns (symbol (str "leiningen." task))] + :when (try (require task-ns) + (ns-resolve task-ns (symbol task)) + (catch java.io.FileNotFoundException _ nil))] + task) + out (java.io.PrintWriter. (java.io.OutputStreamWriter. System/out "UTF-8"))] + (doseq [command (concat tasks (keys leiningen.core.main/aliases))] + (.println out command)) + (.flush out)))) + +(def ^:private lein-routing-check + "Shell script that fails unless the entrypoint passes every built-in + Leiningen command to lein. Takes the `print-lein-commands` form as $1." + "set -e +commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e \"$1\") +stub=$(mktemp -d) +printf '%s\\n' '#!/bin/sh' 'echo \"lein $1\"' > \"$stub/lein\" +chmod +x \"$stub/lein\" +status=0 +while IFS= read -r command; do + routed=$(PATH=\"$stub:/usr/bin:/bin\" /usr/local/bin/entrypoint \"$command\" 2>/dev/null) || true + if [ \"$routed\" != \"lein $command\" ]; then + echo \"entrypoint does not pass $command to lein\" >&2 + status=1 + fi +done < (if (= ::core/all build-tool) + (vals cmds) + [(get cmds build-tool)]) + (and (= "lein" build-tool) (df/entrypoint? variant)) + (conj ["sh" "-c" lein-routing-check "sh" (pr-str print-lein-commands)])))) (defn smoke-test "Runs the variant's smoke test commands in `image-tag`. Returns true if they From fdf77b1711952bcf004e310e0a50db0a63ac415d Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:29:36 +0900 Subject: [PATCH 08/13] Describe both smoke test checks in smoke-test-cmds Co-Authored-By: Claude Opus 5.5 --- src/docker_clojure/docker.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/docker_clojure/docker.clj b/src/docker_clojure/docker.clj index 448a973f..16b352d5 100644 --- a/src/docker_clojure/docker.clj +++ b/src/docker_clojure/docker.clj @@ -55,7 +55,9 @@ EOF exit $status") (defn smoke-test-cmds - "Commands to run in a freshly built image to check its build tool(s) work." + "Commands to run in a freshly built image; each must exit 0. They check the + build tool(s) start and, in lein images with the entrypoint, that the + entrypoint passes every built-in lein command to lein." [{:keys [build-tool] :as variant}] (let [cmds {"lein" ["lein" "version"] "tools-deps" ["clojure" "-M" "-e" "(clojure-version)"]}] From 7f9a137fbc888fae914d783b9b26b714c47bd4b7 Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:32:58 +0900 Subject: [PATCH 09/13] Simplify smoke test docstrings --- src/docker_clojure/docker.clj | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/docker_clojure/docker.clj b/src/docker_clojure/docker.clj index 16b352d5..53e945dd 100644 --- a/src/docker_clojure/docker.clj +++ b/src/docker_clojure/docker.clj @@ -13,9 +13,8 @@ (sh "docker" "pull" image)) (def ^:private print-lein-commands - "Clojure form that prints every built-in Leiningen task and alias, one per - line. Tasks come from the namespaces in the Leiningen jar because `lein help` - omits tasks missing from its own stock list." + "Prints built-in Leiningen tasks and aliases, one per line. + Includes tasks missing from `lein help`, e.g. `static-classpath`." '(do (require 'leiningen.core.main) (let [jar (java.util.jar.JarFile. (System/getProperty "java.class.path")) @@ -35,8 +34,8 @@ (.flush out)))) (def ^:private lein-routing-check - "Shell script that fails unless the entrypoint passes every built-in - Leiningen command to lein. Takes the `print-lein-commands` form as $1." + "Checks every built-in command routes to lein, e.g. `test` -> `lein test`. + $1 is the `print-lein-commands` form." "set -e commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e \"$1\") stub=$(mktemp -d) @@ -55,9 +54,9 @@ EOF exit $status") (defn smoke-test-cmds - "Commands to run in a freshly built image; each must exit 0. They check the - build tool(s) start and, in lein images with the entrypoint, that the - entrypoint passes every built-in lein command to lein." + "Returns commands to check build tools start. Each must exit 0. + Lein images with an entrypoint also check every built-in command routes + to lein, e.g. `test` -> `lein test`." [{:keys [build-tool] :as variant}] (let [cmds {"lein" ["lein" "version"] "tools-deps" ["clojure" "-M" "-e" "(clojure-version)"]}] From 6923c04342c98bd0a0b5b4661c7b3140fb9740af Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Sat, 24 Oct 2026 12:50:07 +0900 Subject: [PATCH 10/13] Move lein routing check into a shell resource --- resources/lein-routing-check.sh | 20 ++++++++++++++++++++ src/docker_clojure/docker.clj | 20 +++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 resources/lein-routing-check.sh diff --git a/resources/lein-routing-check.sh b/resources/lein-routing-check.sh new file mode 100644 index 00000000..5493f0b5 --- /dev/null +++ b/resources/lein-routing-check.sh @@ -0,0 +1,20 @@ +set -e + +commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e "$1") + +stub=$(mktemp -d) +printf '%s\n' '#!/bin/sh' 'echo "lein $1"' > "$stub/lein" +chmod +x "$stub/lein" + +status=0 +while IFS= read -r command; do + routed=$(PATH="$stub:/usr/bin:/bin" /usr/local/bin/entrypoint "$command" 2>/dev/null) || true + if [ "$routed" != "lein $command" ]; then + echo "entrypoint does not pass $command to lein" >&2 + status=1 + fi +done < `lein test`. $1 is the `print-lein-commands` form." - "set -e -commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e \"$1\") -stub=$(mktemp -d) -printf '%s\\n' '#!/bin/sh' 'echo \"lein $1\"' > \"$stub/lein\" -chmod +x \"$stub/lein\" -status=0 -while IFS= read -r command; do - routed=$(PATH=\"$stub:/usr/bin:/bin\" /usr/local/bin/entrypoint \"$command\" 2>/dev/null) || true - if [ \"$routed\" != \"lein $command\" ]; then - echo \"entrypoint does not pass $command to lein\" >&2 - status=1 - fi -done < Date: Tue, 3 Nov 2026 10:37:17 -0700 Subject: [PATCH 11/13] Fall back to lein for commands that aren't on PATH Plugin tasks and project aliases such as `cljsbuild once` ran through lein before #301 was fixed, so keep routing them there instead of failing with "not found". Executables on PATH and explicit paths still run directly, so commands like `git log` never load project code. The fallback prints a warning pointing at explicit `lein` invocation. Tools-deps images no longer pass executable files named without a slash to clj. Co-Authored-By: Claude Opus 5.5 --- README.md | 13 +++++--- resources/entrypoint | 9 ++++-- test/docker_clojure/entrypoint_test.clj | 43 +++++++++++++++++-------- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index dcf2aaf1..102b9fed 100644 --- a/README.md +++ b/README.md @@ -123,14 +123,14 @@ lein repl ### Running individual commands -For JDK 17+ images: +For JDK 16+ images: ```sh # Built-in Leiningen tasks and aliases -docker run --rm clojure:lein test -docker run --rm clojure:lein cp +docker run --rm -v "$PWD":/app -w /app clojure:lein test +docker run --rm -v "$PWD":/app -w /app clojure:lein cp -# Custom Leiningen tasks and aliases: always include lein +# Plugin tasks and project aliases: include lein docker run --rm -v "$PWD":/app -w /app clojure:lein lein my-alias # Clojure flags and scripts @@ -144,6 +144,11 @@ docker run --rm -v "$PWD":/app -w /app clojure:tools-deps clj -M:my-alias Programs such as `bash` and `git` run directly. Use `/bin/cp` to select the executable instead of Leiningen's `cp` alias. +In lein images, a command that isn't a built-in task or an executable on `PATH` +still runs through Leiningen, so `clojure:lein my-alias` works but prints a +warning. Include `lein` to skip the warning, and to run a task that shares its +name with an executable. + With older JDK images, invoke `lein` or `clj` explicitly. ## Builds diff --git a/resources/entrypoint b/resources/entrypoint index d7954afc..561d5bba 100755 --- a/resources/entrypoint +++ b/resources/entrypoint @@ -22,10 +22,15 @@ if [ "${entrypoint}" = lein ]; then exec "${entrypoint}" "$@" ;; esac -elif [ -f "$1" ] && ! type -P "$1" >/dev/null; then + # Plugin tasks and project aliases aren't executables, so they still reach + # lein. Paths and executables on PATH run directly. + if [ "${1#*/}" = "$1" ] && ! type -P "$1" >/dev/null; then + echo "entrypoint: '$1' is not on PATH, running 'lein $1'. Prefer 'lein $1' for custom tasks and aliases." >&2 + exec "${entrypoint}" "$@" + fi +elif [ -f "$1" ] && [ ! -x "$1" ] && ! type -P "$1" >/dev/null; then # Non-executable Clojure scripts run once, through clj. exec "${entrypoint}" "$@" fi -# Other commands run directly. Use explicit lein for custom tasks and aliases. exec "$@" diff --git a/test/docker_clojure/entrypoint_test.clj b/test/docker_clojure/entrypoint_test.clj index 9c759284..a2300305 100644 --- a/test/docker_clojure/entrypoint_test.clj +++ b/test/docker_clojure/entrypoint_test.clj @@ -25,11 +25,12 @@ (spit (io/file dir "example.clj") "(println :example)") ;; A local script must not take precedence over the executable on PATH. (spit (io/file dir "echo") "(throw (Exception. \"unexpected project execution\"))") - (spit (io/file dir "launch") "#!/bin/sh\nprintf '%s\\n' external\n") - (.setExecutable (io/file dir "launch") true false) + (doseq [[script out] [["launch" "external"] ["test" "local"]]] + (spit (io/file dir script) (str "#!/bin/sh\nprintf '%s\\n' " out "\n")) + (.setExecutable (io/file dir script) true false)) (let [result (apply sh (str (io/file bin "entrypoint")) (concat args [:dir (str dir) - :env {"PATH" (str bin ":/usr/bin:/bin") + :env {"PATH" (str bin ":" (System/getenv "PATH")) "CALLS_LOG" (str log)}]))] (assoc result :calls (if (.exists log) (nul-fields (slurp log)) []))) (finally @@ -58,13 +59,25 @@ (testing "built-in tasks and aliases keep precedence over executables" (doseq [task ["repl" "test" "cp" "install" "help" "static-classpath" "uberjar" "überjar"]] - (is (= ["lein" task] (:calls (run-entrypoint "lein" task)))))) + (is (= {:exit 0 :out "" :err "" :calls ["lein" task]} + (run-entrypoint "lein" task))))) (testing "explicit commands and paths bypass shorthand routing" - (is (= ["lein" "my-alias"] (:calls (run-entrypoint "lein" "lein" "my-alias")))) - (is (= {:exit 0 :out "" :err "" :calls []} - (run-entrypoint "lein" "/bin/test" "1" "=" "1")))) - (testing "custom tasks use explicit Leiningen invocation" - (is (= ["lein" "my-task"] (:calls (run-entrypoint "lein" "lein" "my-task")))))) + (is (= {:exit 0 :out "" :err "" :calls ["lein" "my-alias"]} + (run-entrypoint "lein" "lein" "my-alias"))) + (is (= {:exit 0 :out "local\n" :err "" :calls []} + (run-entrypoint "lein" "./test"))))) + +(deftest lein-fallback-test + (testing "plugin tasks and project aliases still reach lein, with a warning" + (doseq [args [["cljsbuild" "once"] ["my-alias"] ["no-such-command"]]] + (let [result (apply run-entrypoint "lein" args)] + (is (= 0 (:exit result))) + (is (= (into ["lein"] args) (:calls result))) + (is (str/includes? (:err result) (str "running 'lein " (first args) "'")))))) + (testing "missing paths do not fall back to lein" + (let [result (run-entrypoint "lein" "./no-such-command")] + (is (not= 0 (:exit result))) + (is (= [] (:calls result)))))) (deftest clojure-scripts-run-once-test (doseq [build-tool ["tools-deps" :docker-clojure.core/all] @@ -74,11 +87,13 @@ (is (= ["clj" script "argument"] (:calls result)))))) (deftest unknown-commands-do-not-launch-project-test - (doseq [build-tool ["lein" "tools-deps" :docker-clojure.core/all]] - (let [result (run-entrypoint build-tool "no-such-command")] - (is (= 127 (:exit result))) - (is (str/includes? (:err result) "no-such-command")) - (is (= [] (:calls result)))))) + (doseq [build-tool ["tools-deps" :docker-clojure.core/all] + command ["no-such-command" "launch"]] + (testing (str build-tool " does not pass " command " to clj") + (let [result (run-entrypoint build-tool command)] + (is (= 127 (:exit result))) + (is (str/includes? (:err result) command)) + (is (= [] (:calls result))))))) (deftest literal-arguments-test (let [args ["" "two words" "*.clj" "line one\nline two" "$(exit 99)" "'quoted'"]] From 5443c897ba372f43a825931d4ed853da66fbd88d Mon Sep 17 00:00:00 2001 From: Wes Morgan Date: Tue, 3 Nov 2026 10:37:17 -0700 Subject: [PATCH 12/13] Harden the lein routing smoke test - Check routing with the image's PATH and fail on the fallback warning, so a stale built-in task list is caught whether the task collides with an executable or falls back. - Use the exact Leiningen jar path and keep the entrypoint off the loop's stdin. - Skip task namespaces that fail to load instead of aborting the listing. - Read the check script when building smoke test commands, not at namespace load. - Unit test smoke-test-cmds. Co-Authored-By: Claude Opus 5.5 --- resources/lein-routing-check.sh | 8 +++++--- src/docker_clojure/docker.clj | 9 ++++++--- test/docker_clojure/docker_test.clj | 19 ++++++++++++++++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/resources/lein-routing-check.sh b/resources/lein-routing-check.sh index 5493f0b5..827470fe 100644 --- a/resources/lein-routing-check.sh +++ b/resources/lein-routing-check.sh @@ -1,16 +1,18 @@ set -e -commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e "$1") +commands=$(java -cp "/usr/share/java/leiningen-$LEIN_VERSION-standalone.jar" clojure.main -e "$1") stub=$(mktemp -d) printf '%s\n' '#!/bin/sh' 'echo "lein $1"' > "$stub/lein" chmod +x "$stub/lein" +# Built-ins must route to lein without the fallback warning, even when an +# executable on the image's PATH shares their name. status=0 while IFS= read -r command; do - routed=$(PATH="$stub:/usr/bin:/bin" /usr/local/bin/entrypoint "$command" 2>/dev/null) || true + routed=$(PATH="$stub:$PATH" /usr/local/bin/entrypoint "$command" 2>&1 &2 + echo "entrypoint does not pass $command to lein: $routed" >&2 status=1 fi done < `lein test`. $1 is the `print-lein-commands` form." + [] (slurp (io/resource "lein-routing-check.sh"))) (defn smoke-test-cmds @@ -50,7 +53,7 @@ (vals cmds) [(get cmds build-tool)]) (and (= "lein" build-tool) (df/entrypoint? variant)) - (conj ["sh" "-c" lein-routing-check "sh" (pr-str print-lein-commands)])))) + (conj ["sh" "-c" (lein-routing-check) "sh" (pr-str print-lein-commands)])))) (defn smoke-test "Runs the variant's smoke test commands in `image-tag`. Returns true if they diff --git a/test/docker_clojure/docker_test.clj b/test/docker_clojure/docker_test.clj index e707e3b2..69af0280 100644 --- a/test/docker_clojure/docker_test.clj +++ b/test/docker_clojure/docker_test.clj @@ -1,5 +1,6 @@ (ns docker-clojure.docker-test - (:require [clojure.string :as str] + (:require [clojure.java.shell :refer [sh]] + [clojure.string :as str] [clojure.test :refer :all] [docker-clojure.config :as cfg] [docker-clojure.docker :refer :all])) @@ -68,3 +69,19 @@ :debian-slim/trixie-slim "tools-deps-trixie-slim" :debian/bookworm "tools-deps-bookworm" :debian-slim/bookworm-slim "tools-deps-bookworm-slim"))) + +(deftest smoke-test-cmds-test + (testing "every variant checks its build tools start" + (are [build-tool jdk-version cmds] + (= cmds (smoke-test-cmds {:build-tool build-tool :jdk-version jdk-version})) + "lein" 11 [["lein" "version"]] + "tools-deps" 21 [["clojure" "-M" "-e" "(clojure-version)"]] + :docker-clojure.core/all 21 [["lein" "version"] + ["clojure" "-M" "-e" "(clojure-version)"]])) + (testing "lein images with an entrypoint also check lein routing" + (let [[version-cmd [sh-cmd flag script & args]] + (smoke-test-cmds {:build-tool "lein" :jdk-version 21})] + (is (= ["lein" "version"] version-cmd)) + (is (= ["sh" "-c"] [sh-cmd flag])) + (is (= {:exit 0 :err ""} (select-keys (sh "sh" "-n" "-c" script) [:exit :err]))) + (is (= 2 (count args)))))) From 122a1e90fbab9dec22ab8bdd66f55646f5b6366a Mon Sep 17 00:00:00 2001 From: Paul Lam Date: Thu, 5 Nov 2026 15:33:46 +0900 Subject: [PATCH 13/13] Explain mount and working directory flags in README examples --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 102b9fed..6f741e5c 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,9 @@ lein repl For JDK 16+ images: ```sh +# -v "$PWD":/app makes your current folder available inside the container at /app. +# -w /app runs the command from that folder. + # Built-in Leiningen tasks and aliases docker run --rm -v "$PWD":/app -w /app clojure:lein test docker run --rm -v "$PWD":/app -w /app clojure:lein cp