Repository navigation
Route entrypoint commands without probing project code #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d153f98
6506e30
e27eb3b
d6cc948
e94a91f
a156c5b
68ff4b1
e2225b7
6b6cf98
563ccab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,28 @@ 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 (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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Generated by Claude Code |
||
| 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|\ | ||
| static-classpath|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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Executable scripts named without a slash are evaluated as Clojure. Generated by Claude Code |
||
| # Non-executable Clojure scripts run once, through clj. | ||
| exec "${entrypoint}" "$@" | ||
| fi | ||
|
|
||
| # Other commands run directly. Use explicit lein for custom tasks and aliases. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Breaking change for plugin tasks and project aliases. Previously Generated by Claude Code |
||
| exec "$@" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| set -e | ||
|
|
||
| commands=$(java -cp /usr/share/java/leiningen-*-standalone.jar clojure.main -e "$1") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 6. Jar path uses a glob. If Generated by Claude Code |
||
|
|
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. The entrypoint shares the loop's stdin. The entrypoint runs inside Generated by Claude Code |
||
| if [ "$routed" != "lein $command" ]; then | ||
| echo "entrypoint does not pass $command to lein" >&2 | ||
| status=1 | ||
| fi | ||
| done <<EOF | ||
| $commands | ||
| EOF | ||
|
|
||
| exit $status | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| (ns docker-clojure.docker | ||
| (:require [clojure.java.shell :refer [sh with-sh-dir]] | ||
| (:require [clojure.java.io :as io] | ||
| [clojure.java.shell :refer [sh with-sh-dir]] | ||
| [clojure.spec.alpha :as s] | ||
| [clojure.string :as str] | ||
| [docker-clojure.config :as cfg] | ||
|
|
@@ -12,14 +13,44 @@ | |
| (defn pull-image [image] | ||
| (sh "docker" "pull" image)) | ||
|
|
||
| (def ^:private print-lein-commands | ||
| "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")) | ||
| 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))] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. Only Generated by Claude Code |
||
| 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 | ||
| "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"))) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 7. Resource slurped at namespace load. If Generated by Claude Code |
||
|
|
||
| (defn smoke-test-cmds | ||
| "Commands to run in a freshly built image to check its build tool(s) work." | ||
| [{:keys [build-tool]}] | ||
| "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)"]}] | ||
| (if (= ::core/all build-tool) | ||
| (vals cmds) | ||
| [(get cmds build-tool)]))) | ||
| (cond-> (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)])))) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Generated by Claude Code |
||
|
|
||
| (defn smoke-test | ||
| "Runs the variant's smoke test commands in `image-tag`. Returns true if they | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| (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" "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")))) | ||
| (is (= {:exit 0 :out "" :err "" :calls []} | ||
| (run-entrypoint "lein" "/bin/test" "1" "=" "1")))) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 10. Tests depend on the host layout. The tests rely on Generated by Claude Code |
||
| (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))))))) | ||
There was a problem hiding this comment.
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 testandclojure:lein cpdon'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