Skip to content

Route entrypoint commands without probing project code - #302

Open
Quantisan wants to merge 10 commits into
masterfrom
fix-entrypoint-routing
Open

Quantisan wants to merge 10 commits into
masterfrom
fix-entrypoint-routing

Conversation

@Quantisan

@Quantisan Quantisan commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner
  • Entrypoint no longer probes commands with <cmd> --help, which could evaluate code from the current project; Lein images now route built-in tasks by a fixed list, and other commands run directly.
  • Non-executable Clojure scripts in tools-deps images still run through clj; custom Leiningen tasks and aliases need an explicit lein (documented in the README).
  • Adds a smoke test that every built-in lein command routes correctly (resources/lein-routing-check.sh), plus entrypoint unit tests.
  • Tradeoff: the built-in task list is pinned to Leiningen 2.13.0 and must be updated on upgrade; the smoke test runs on CI image builds as a guardrail against drift.

Fixes #301

🤖 Generated with Claude Code

Quantisan and others added 10 commits October 24, 2026 11:55
Leiningen 2.11.0 added the static-classpath task, which the built-in
task list missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Quantisan
Quantisan marked this pull request as ready for review October 24, 2026 03:54
@Quantisan
Quantisan requested a review from cap10morgan October 24, 2026 03:54

@cap10morgan cap10morgan left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll do a more thorough review of this soon. In the meantime here’s some things Claude flagged (AIs reviewing AIs haha!) for your consideration. I know #1 is a known issue but I think it is worth some consideration of how to not break existing users if we can.

I wonder if it would make sense to work with lein & clj upstream to add a subcommand / flag discovery option that can’t execute code? Or maybe generate the static list of subcommands & flags when we build the project and find a better way to discover project-specific aliases, etc. at runtime? Just a thought.

Good catch though! We should definitely fix this.


The rest was... Generated by Claude Code

Comment thread resources/entrypoint
exec "${entrypoint}" "$@"
fi

# Other commands run directly. Use explicit lein for custom tasks and aliases.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. Breaking change for plugin tasks and project aliases. Previously lein <cmd> --help succeeded for plugin tasks (ring, cljsbuild, midje, figwheel, kibit, …) and project :aliases, so e.g. docker run -v $PWD:/app -w /app clojure:lein cljsbuild once ran lein cljsbuild once. Now it falls through to exec cljsbuild once and fails with not found (exit 127). The README note is the only notice existing CI pipelines get.


Generated by Claude Code

Comment thread resources/entrypoint
exec "${entrypoint}" "$@"
;;
esac
elif [ -f "$1" ] && ! type -P "$1" >/dev/null; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2. Executable scripts named without a slash are evaluated as Clojure. type -P only searches PATH, so with -w /app and an executable /app/run.sh, docker run … clojure:tools-deps run.sh fails the type -P check, passes [ -f run.sh ], and runs clj run.sh, giving a confusing reader/compiler error. The comment and README say only non-executable Clojure scripts take this branch; adding [ ! -x "$1" ] (or similar) plus a test for an executable file named without ./ would cover it.


Generated by Claude Code

Comment thread resources/entrypoint
# 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. Hand-maintained task list pinned to Leiningen 2.13.0. The lein version lives in cfg/build-tools, so a version bump leaves this list stale and the drift only shows up as failed image builds in CI. Could the list be generated instead, e.g. when shared-prereqs writes the entrypoint (it already substitutes @@entrypoint@@), or by running print-lein-commands during the Dockerfile build, so it always comes from the installed jar?


Generated by Claude Code


status=0
while IFS= read -r command; do
routed=$(PATH="$stub:/usr/bin:/bin" /usr/local/bin/entrypoint "$command" 2>/dev/null) || true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4. The entrypoint shares the loop's stdin. The entrypoint runs inside while read … <<EOF, so a fallback command that reads stdin can consume the rest of the command list. The loop then ends early and the check passes without testing the remaining commands. Suggest /usr/local/bin/entrypoint "$command" </dev/null.


Generated by Claude Code

task-ns (symbol (str "leiningen." task))]
:when (try (require task-ns)
(ns-resolve task-ns (symbol task))
(catch java.io.FileNotFoundException _ nil))]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5. Only FileNotFoundException is caught. If any leiningen/*.clj namespace in the jar fails to load standalone (missing optional dep, init that needs a project), require throws a Compiler$CompilerException. That aborts the whole listing, set -e stops the check script, and the image build fails even though routing is fine. Catching Exception (or skipping namespaces that aren't tasks) would avoid this.


Generated by Claude Code

@@ -0,0 +1,20 @@
set -e

commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e "$1")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6. Jar path uses a glob. If leiningen-*-standalone.jar matches several files, the second one becomes the main-class argument. If it matches none, the literal pattern becomes the classpath. Either way -cp is wrong. The image already sets LEIN_VERSION, so /usr/share/java/leiningen-$LEIN_VERSION-standalone.jar would be exact.


Generated by Claude Code

(def ^:private lein-routing-check
"Checks every built-in command routes to lein, e.g. `test` -> `lein test`.
$1 is the `print-lein-commands` form."
(slurp (io/resource "lein-routing-check.sh")))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7. Resource slurped at namespace load. If resources isn't on the classpath (or the file is renamed), io/resource returns nil and slurp throws while docker-clojure.docker loads. That breaks every entry point requiring the namespace, including generate/manifest commands that never smoke test. Reading it lazily inside smoke-test-cmds keeps the failure local.


Generated by Claude Code

(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)]))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8. No unit test for the new routing-check branch. A regression in this cond-> condition (e.g. the check stops being added for JDK 21 lein variants, or is wrongly added for ::core/all or JDK 11) or a syntax error in lein-routing-check.sh would pass clojure -X:test and only show up (or be silently missed) in CI image builds. A docker_test case asserting smoke-test-cmds output for lein/jdk-11, lein/jdk-21 and tools-deps would cover it.


Generated by Claude Code

Comment thread README.md
Comment on lines +125 to +130

For JDK 17+ images:

```sh
# Built-in Leiningen tasks and aliases
docker run --rm clojure:lein test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9. Docs vs. code. entrypoint? installs the wrapper for JDK 16+, but this says JDK 17+. Also, docker run --rm clojure:lein test and clojure:lein cp don't mount a project, so as written they fail with "Couldn't find project.clj". The custom-alias example below includes -v "$PWD":/app -w /app, and these probably should too.


Generated by Claude Code

(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"))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10. Tests depend on the host layout. The tests rely on /bin/test, /bin/sh, and #!/usr/bin/env bash resolving under a PATH restricted to /usr/bin:/bin. On hosts like NixOS/Guix, or where bash only lives in /usr/local/bin or /opt/homebrew/bin, every test fails with 127 instead of exercising the routing logic.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Running git log in the container can execute Clojure project code

2 participants