Skip to content
Open
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
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Java 21 tools-deps on Alpine: `clojure:temurin-21-tools-deps-alpine` or `clojure

## Examples

### Interactive Shell
### Interactive shell

Run an interactive shell from this image.

Expand All @@ -121,6 +121,36 @@ cd hello-world
lein repl
```

### Running individual commands

For JDK 16+ images:

```sh
# 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

# Plugin tasks and project aliases: 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
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

The Dockerfiles are generated by the `docker-clojure` Clojure app in this repo.
Expand Down
27 changes: 21 additions & 6 deletions resources/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ 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
# 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
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
# 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

exec "$@"
22 changes: 22 additions & 0 deletions resources/lein-routing-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
set -e

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:$PATH" /usr/local/bin/entrypoint "$command" 2>&1 </dev/null) || true
if [ "$routed" != "lein $command" ]; then
echo "entrypoint does not pass $command to lein: $routed" >&2
status=1
fi
done <<EOF
$commands
EOF

exit $status
46 changes: 40 additions & 6 deletions src/docker_clojure/docker.clj
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]
Expand All @@ -12,14 +13,47 @@
(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))
;; Skip helper namespaces that aren't tasks
;; or need a project to load.
(catch Exception _ 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))))

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

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

(defn smoke-test
"Runs the variant's smoke test commands in `image-tag`. Returns true if they
Expand Down
27 changes: 21 additions & 6 deletions target/debian-bookworm-11/lein/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ set -eou pipefail

entrypoint=lein

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
# 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
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
# 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

exec "$@"
27 changes: 21 additions & 6 deletions target/debian-bookworm-11/tools-deps/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ set -eou pipefail

entrypoint=clj

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
# 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
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
# 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

exec "$@"
27 changes: 21 additions & 6 deletions target/debian-bookworm-17/lein/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ set -eou pipefail

entrypoint=lein

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
# 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
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
# 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

exec "$@"
27 changes: 21 additions & 6 deletions target/debian-bookworm-17/tools-deps/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ set -eou pipefail

entrypoint=clj

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
# 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
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
# 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

exec "$@"
27 changes: 21 additions & 6 deletions target/debian-bookworm-21/lein/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ set -eou pipefail

entrypoint=lein

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
# 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
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
# 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

exec "$@"
Loading
Loading