From 42f7033111f1ccc4086c20cbea6bab176d7aca68 Mon Sep 17 00:00:00 2001 From: Wes Morgan Date: Tue, 9 Jun 2026 11:54:40 -0600 Subject: [PATCH] Generate Dockerfiles from Selmer templates Replace the hand-rolled string/vector concatenation in the Dockerfile generators with Selmer templates under resources/templates/: - Dockerfile.tmpl - the outer skeleton (FROM, JDK copy, install section(s), entrypoint, CMD) - lein.tmpl - the leiningen install section - tools-deps.tmpl - the tools-deps install section The templates read like real Dockerfiles: the install RUN commands live in them directly, with the distro-specific dep lists looped in and dynamic values (checksums, signing key, bundled clojure version) interpolated as Selmer placeholders. The build-tool namespaces are reduced to computing that context. Output is identical to before for every image except the combined "latest" image, which loses a stray blank line between ENTRYPOINT and CMD so it matches the single-build-tool images. Verified by regenerating under both babashka (bb run dockerfiles) and the JVM build-images path. bb test and cljfmt pass. Adds selmer/selmer to deps.edn for the JVM path; babashka bundles Selmer. --- deps.edn | 1 + resources/templates/Dockerfile.tmpl | 12 +++ resources/templates/lein.tmpl | 35 +++++++++ resources/templates/tools-deps.tmpl | 19 +++++ src/docker_clojure/dockerfile.clj | 81 ++++++++++++-------- src/docker_clojure/dockerfile/lein.clj | 64 ++++------------ src/docker_clojure/dockerfile/shared.clj | 34 ++++---- src/docker_clojure/dockerfile/tools_deps.clj | 45 +++-------- target/debian-bookworm-25/latest/Dockerfile | 1 - test/docker_clojure/dockerfile_test.clj | 12 +-- 10 files changed, 160 insertions(+), 144 deletions(-) create mode 100644 resources/templates/Dockerfile.tmpl create mode 100644 resources/templates/lein.tmpl create mode 100644 resources/templates/tools-deps.tmpl diff --git a/deps.edn b/deps.edn index 6767b856..7f5a1834 100644 --- a/deps.edn +++ b/deps.edn @@ -2,6 +2,7 @@ {org.clojure/clojure {:mvn/version "1.12.4"} org.clojure/math.combinatorics {:mvn/version "0.3.2"} org.clojure/core.async {:mvn/version "1.9.865"} + selmer/selmer {:mvn/version "1.12.61"} com.gfredericks/test.chuck {:git/url "https://github.com/gfredericks/test.chuck" :git/sha "ab5c11b013d3526e587dd53a860fa651b3e8a5a7"}} diff --git a/resources/templates/Dockerfile.tmpl b/resources/templates/Dockerfile.tmpl new file mode 100644 index 00000000..d0b29c0d --- /dev/null +++ b/resources/templates/Dockerfile.tmpl @@ -0,0 +1,12 @@ +FROM {{from}} + +{% if copy-java %}ENV JAVA_HOME=/opt/java/openjdk +COPY --from=eclipse-temurin:{{jdk-version}} $JAVA_HOME $JAVA_HOME +ENV PATH="${JAVA_HOME}/bin:${PATH}" + +{% endif %}{{body}} + +{% if entrypoint %}COPY entrypoint /usr/local/bin/entrypoint + +ENTRYPOINT ["entrypoint"] +{% endif %}{{cmd}} diff --git a/resources/templates/lein.tmpl b/resources/templates/lein.tmpl new file mode 100644 index 00000000..98f59c8e --- /dev/null +++ b/resources/templates/lein.tmpl @@ -0,0 +1,35 @@ +ENV LEIN_VERSION={{lein-version}} +ENV LEIN_INSTALL=/usr/local/bin/ + +WORKDIR /tmp + +# Download the whole repo as an archive +RUN set -eux; \ +{% for dep in install-deps %}{{dep}} && \ +{% endfor %}mkdir -p $LEIN_INSTALL && \ +wget -q https://codeberg.org/leiningen/leiningen/raw/tag/$LEIN_VERSION/bin/lein-pkg && \ +echo "Comparing lein-pkg checksum ..." && \ +sha256sum lein-pkg && \ +echo "{{lein-pkg-hash}} *lein-pkg" | sha256sum -c - && \ +mv lein-pkg $LEIN_INSTALL/lein && \ +chmod 0755 $LEIN_INSTALL/lein && \ +export GNUPGHOME="$(mktemp -d)" && \ +export FILENAME_EXT=jar && \ +gpg --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys {{gpg-key}} && \ +wget -q https://codeberg.org/leiningen/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT && \ +wget -q https://codeberg.org/leiningen/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc && \ +echo "Verifying file PGP signature..." && \ +gpg --batch --verify leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT && \ +gpgconf --kill all && \ +rm -rf "$GNUPGHOME" leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc && \ +mkdir -p /usr/share/java && \ +mkdir -p /root/.lein && \ +mv leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT /usr/share/java/leiningen-$LEIN_VERSION-standalone.jar{% for dep in uninstall-deps %} && \ +{{dep}}{% endfor %} + +ENV PATH=$PATH:$LEIN_INSTALL +ENV LEIN_ROOT 1 + +# Install clojure {{clojure-version}} so users don't have to download it every time +RUN echo '(defproject dummy "" :dependencies [[org.clojure/clojure "{{clojure-version}}"]])' > project.clj \ + && lein deps && rm project.clj diff --git a/resources/templates/tools-deps.tmpl b/resources/templates/tools-deps.tmpl new file mode 100644 index 00000000..6cc3ca27 --- /dev/null +++ b/resources/templates/tools-deps.tmpl @@ -0,0 +1,19 @@ +ENV CLOJURE_VERSION={{clojure-version}} + +WORKDIR /tmp + +RUN \ +{% for dep in install-deps %}{{dep}} && \ +{% endfor %}curl -fsSLO https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh && \ +sha256sum linux-install-$CLOJURE_VERSION.sh && \ +echo "{{install-hash}} *linux-install-$CLOJURE_VERSION.sh" | sha256sum -c - && \ +chmod +x linux-install-$CLOJURE_VERSION.sh && \ +./linux-install-$CLOJURE_VERSION.sh && \ +rm linux-install-$CLOJURE_VERSION.sh && \ +clojure -e "(clojure-version)"{% for dep in uninstall-deps %} && \ +{{dep}}{% endfor %} + +# Docker bug makes rlwrap crash w/o short sleep first +# Bug: https://github.com/moby/moby/issues/28009 +# As of 2021-09-10 this bug still exists, despite that issue being closed +COPY rlwrap.retry /usr/local/bin/rlwrap diff --git a/src/docker_clojure/dockerfile.clj b/src/docker_clojure/dockerfile.clj index 7a186b39..e5260a18 100644 --- a/src/docker_clojure/dockerfile.clj +++ b/src/docker_clojure/dockerfile.clj @@ -4,7 +4,7 @@ [clojure.string :as str] [docker-clojure.dockerfile.lein :as lein] [docker-clojure.dockerfile.tools-deps :as tools-deps] - [docker-clojure.dockerfile.shared :refer [copy-resource-file! entrypoint]] + [docker-clojure.dockerfile.shared :refer [copy-resource-file! render-template]] [docker-clojure.log :refer [log]])) (defn build-dir [{:keys [base-image-tag jdk-version build-tool]}] @@ -19,40 +19,55 @@ (defn all-prereqs [dir variant] (tools-deps/prereqs dir variant)) -(defn all-contents [installer-hashes variant] - (concat - ["" "### INSTALL LEIN ###"] - (lein/install - installer-hashes - (assoc variant :build-tool-version - (get-in variant [:build-tool-versions "lein"]))) - ["" "### INSTALL TOOLS-DEPS ###"] - (tools-deps/install - installer-hashes - (assoc variant :build-tool-version - (get-in variant [:build-tool-versions "tools-deps"]))) - [""] - (entrypoint variant) - ["" "CMD [\"-M\", \"--repl\"]"])) +(defn copy-java? + "Debian variants copy the JDK in from an eclipse-temurin image; the temurin + base images already have it." + [{:keys [distro]}] + (contains? #{:debian :debian-slim} (-> distro namespace keyword))) -(defn copy-java-from-temurin-contents - [{:keys [jdk-version] :as _variant}] - ["ENV JAVA_HOME=/opt/java/openjdk" - (str "COPY --from=eclipse-temurin:" jdk-version " $JAVA_HOME $JAVA_HOME") - "ENV PATH=\"${JAVA_HOME}/bin:${PATH}\"" - ""]) +(defn entrypoint? + "JDK 16+ ships a `repl` so we install our wrapper entrypoint for it." + [{:keys [jdk-version]}] + (>= jdk-version 16)) -(defn contents [installer-hashes {:keys [build-tool distro] :as variant}] - (str/join "\n" - (concat [(format "FROM %s" (:base-image-tag variant)) - ""] - (case (-> distro namespace keyword) - (:debian :debian-slim) (copy-java-from-temurin-contents variant) - []) - (case build-tool - :docker-clojure.core/all (all-contents installer-hashes variant) - "lein" (lein/contents installer-hashes variant) - "tools-deps" (tools-deps/contents installer-hashes variant))))) +(defn for-build-tool + "Return `variant` with `:build-tool-version` set to the given tool's version. + Single-build-tool variants already carry it; the combined `latest` image + pulls each tool's version from `:build-tool-versions`." + [variant build-tool] + (cond-> variant + (nil? (:build-tool-version variant)) + (assoc :build-tool-version (get-in variant [:build-tool-versions build-tool])))) + +(defn body + "The build-tool install section(s) that go between the base image setup and + the entrypoint. The combined `latest` image stacks both behind headers." + [installer-hashes {:keys [build-tool] :as variant}] + (case build-tool + "lein" (lein/install installer-hashes variant) + "tools-deps" (tools-deps/install installer-hashes variant) + :docker-clojure.core/all + (str "\n### INSTALL LEIN ###\n" + (lein/install installer-hashes (for-build-tool variant "lein")) + "\n\n### INSTALL TOOLS-DEPS ###\n" + (tools-deps/install installer-hashes (for-build-tool variant "tools-deps"))))) + +(defn command + [{:keys [build-tool] :as variant}] + (case build-tool + "lein" (lein/command variant) + "tools-deps" (tools-deps/command variant) + :docker-clojure.core/all "CMD [\"-M\", \"--repl\"]")) + +(defn contents [installer-hashes variant] + (render-template + "templates/Dockerfile.tmpl" + {:from (:base-image-tag variant) + :copy-java (copy-java? variant) + :jdk-version (:jdk-version variant) + :body (body installer-hashes variant) + :entrypoint (entrypoint? variant) + :cmd (command variant)})) (defn shared-prereqs [dir {:keys [build-tool]}] (let [entrypoint (case build-tool diff --git a/src/docker_clojure/dockerfile/lein.clj b/src/docker_clojure/dockerfile/lein.clj index 097e1bc5..5e210a40 100644 --- a/src/docker_clojure/dockerfile/lein.clj +++ b/src/docker_clojure/dockerfile/lein.clj @@ -1,7 +1,7 @@ (ns docker-clojure.dockerfile.lein (:require [clojure.string :as str] [docker-clojure.dockerfile.shared - :refer [concat-commands entrypoint install-distro-deps + :refer [install-distro-deps render-template uninstall-distro-build-deps]])) (defn prereqs [_ _] nil) @@ -20,6 +20,10 @@ (def uninstall-build-deps (partial uninstall-distro-build-deps distro-deps)) +;; Clojure version pre-installed into lein images so users don't download it on +;; first use. +(def ^:const bundled-clojure-version "1.12.1") + (def ^:const old-key "6A2D483DB59437EBB97D09B1040193357D0606ED") (def ^:const new-key "9D13D9426A0814B3373CF5E3D8A8243577A7859F") @@ -32,54 +36,16 @@ :else old-key))) (defn install [installer-hashes {:keys [build-tool-version] :as variant}] - (let [install-dep-cmds (install-deps variant) - uninstall-dep-cmds (uninstall-build-deps variant)] - (-> [(format "ENV LEIN_VERSION=%s" build-tool-version) - "ENV LEIN_INSTALL=/usr/local/bin/" - "" - "WORKDIR /tmp" - "" - "# Download the whole repo as an archive" - "RUN set -eux; \\"] - (concat-commands install-dep-cmds) - (concat-commands - ["mkdir -p $LEIN_INSTALL" - "wget -q https://codeberg.org/leiningen/leiningen/raw/tag/$LEIN_VERSION/bin/lein-pkg" - "echo \"Comparing lein-pkg checksum ...\"" - "sha256sum lein-pkg" - (str "echo \"" (get-in installer-hashes ["lein" build-tool-version]) " *lein-pkg\" | sha256sum -c -") - "mv lein-pkg $LEIN_INSTALL/lein" - "chmod 0755 $LEIN_INSTALL/lein" - "export GNUPGHOME=\"$(mktemp -d)\"" - "export FILENAME_EXT=jar" ; used to be zip but hopefully it's always jar now? - (str "gpg --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys " - (gpg-key build-tool-version)) - "wget -q https://codeberg.org/leiningen/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT" - "wget -q https://codeberg.org/leiningen/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc" - "echo \"Verifying file PGP signature...\"" - "gpg --batch --verify leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT" - "gpgconf --kill all" - "rm -rf \"$GNUPGHOME\" leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT.asc" - "mkdir -p /usr/share/java" - "mkdir -p /root/.lein" - "mv leiningen-$LEIN_VERSION-standalone.$FILENAME_EXT /usr/share/java/leiningen-$LEIN_VERSION-standalone.jar"] - (empty? uninstall-dep-cmds)) - (concat-commands uninstall-dep-cmds :end) - (concat - ["" - "ENV PATH=$PATH:$LEIN_INSTALL" - "ENV LEIN_ROOT 1" - "" - "# Install clojure 1.12.1 so users don't have to download it every time" - "RUN echo '(defproject dummy \"\" :dependencies [[org.clojure/clojure \"1.12.1\"]])' > project.clj \\" - " && lein deps && rm project.clj"]) - - (->> (remove nil?))))) + (render-template + "templates/lein.tmpl" + {:lein-version build-tool-version + :clojure-version bundled-clojure-version + :lein-pkg-hash (get-in installer-hashes ["lein" build-tool-version]) + :gpg-key (gpg-key build-tool-version) + :install-deps (install-deps variant) + :uninstall-deps (uninstall-build-deps variant)})) (defn command [{:keys [jdk-version]}] (if (>= jdk-version 16) - ["CMD [\"repl\"]"] - ["CMD [\"lein\", \"repl\"]"])) - -(defn contents [installer-hashes variant] - (concat (install installer-hashes variant) [""] (entrypoint variant) (command variant))) + "CMD [\"repl\"]" + "CMD [\"lein\", \"repl\"]")) diff --git a/src/docker_clojure/dockerfile/shared.clj b/src/docker_clojure/dockerfile/shared.clj index 74f8350f..5e8b0161 100644 --- a/src/docker_clojure/dockerfile/shared.clj +++ b/src/docker_clojure/dockerfile/shared.clj @@ -1,15 +1,18 @@ (ns docker-clojure.dockerfile.shared - (:require [clojure.string :as str] - [clojure.java.io :as io])) + (:require [clojure.java.io :as io] + [clojure.string :as str] + [selmer.parser :as selmer] + [selmer.util :as selmer-util])) -(defn concat-commands [base cmds & [end?]] - (let [commands (if end? - (butlast cmds) - cmds)] - (concat base - (map #(str % " && \\") - commands) - (when end? [(last cmds)])))) +;; Dockerfiles aren't HTML, so don't let Selmer escape any of the values we +;; interpolate (quotes, brackets, etc. must pass through verbatim). +(selmer-util/turn-off-escaping!) + +(defn render-template + "Render the Selmer template at resource path `tmpl` with `context`, trimming + any trailing newline so callers control the surrounding whitespace." + [tmpl context] + (-> tmpl io/resource slurp (selmer/render context) str/trim-newline)) (defn get-deps [type distro-deps distro] (some->> distro namespace keyword (get distro-deps) type)) @@ -61,14 +64,3 @@ dest (io/file build-dir filename)] (->> src slurp contents-processor (spit dest)) (file-processor dest)))) - -(defn entrypoint - "This is the same for every build-tool so far, so it's in here. If that - changes move it into the build-tool-specific namespaces (or future protocol)." - [{:keys [jdk-version]}] - (if (>= jdk-version 16) - (concat - ["COPY entrypoint /usr/local/bin/entrypoint"] - [""] - ["ENTRYPOINT [\"entrypoint\"]"]) - nil)) diff --git a/src/docker_clojure/dockerfile/tools_deps.clj b/src/docker_clojure/dockerfile/tools_deps.clj index ec32ad86..354a1c3d 100644 --- a/src/docker_clojure/dockerfile/tools_deps.clj +++ b/src/docker_clojure/dockerfile/tools_deps.clj @@ -1,7 +1,7 @@ (ns docker-clojure.dockerfile.tools-deps (:require [docker-clojure.dockerfile.shared - :refer [concat-commands copy-resource-file! entrypoint - install-distro-deps uninstall-distro-build-deps]])) + :refer [copy-resource-file! install-distro-deps render-template + uninstall-distro-build-deps]])) (defn prereqs [dir _variant] (copy-resource-file! dir "rlwrap.retry" identity @@ -23,40 +23,15 @@ (def uninstall-build-deps (partial uninstall-distro-build-deps distro-deps)) -(def docker-bug-notice - ["# Docker bug makes rlwrap crash w/o short sleep first" - "# Bug: https://github.com/moby/moby/issues/28009" - "# As of 2021-09-10 this bug still exists, despite that issue being closed"]) - (defn install [installer-hashes {:keys [build-tool-version] :as variant}] - (let [install-dep-cmds (install-deps variant) - uninstall-dep-cmds (uninstall-build-deps variant)] - (-> [(format "ENV CLOJURE_VERSION=%s" build-tool-version) - "" - "WORKDIR /tmp" - "" - "RUN \\"] - (concat-commands install-dep-cmds) - (concat-commands - ["curl -fsSLO https://download.clojure.org/install/linux-install-$CLOJURE_VERSION.sh" - "sha256sum linux-install-$CLOJURE_VERSION.sh" - (str "echo \"" (get-in installer-hashes ["tools-deps" build-tool-version]) " *linux-install-$CLOJURE_VERSION.sh\" | sha256sum -c -") - "chmod +x linux-install-$CLOJURE_VERSION.sh" - "./linux-install-$CLOJURE_VERSION.sh" - "rm linux-install-$CLOJURE_VERSION.sh" - "clojure -e \"(clojure-version)\""] (empty? uninstall-dep-cmds)) - (concat-commands uninstall-dep-cmds :end) - (concat [""] docker-bug-notice - ["COPY rlwrap.retry /usr/local/bin/rlwrap"]) - (->> (remove nil?))))) + (render-template + "templates/tools-deps.tmpl" + {:clojure-version build-tool-version + :install-hash (get-in installer-hashes ["tools-deps" build-tool-version]) + :install-deps (install-deps variant) + :uninstall-deps (uninstall-build-deps variant)})) (defn command [{:keys [jdk-version]}] (if (>= jdk-version 16) - ["CMD [\"-M\", \"--repl\"]"] - [(str "CMD [\"clj\"]")])) - -(defn contents [installer-hashes variant] - (concat (install installer-hashes variant) - [""] - (entrypoint variant) - (command variant))) + "CMD [\"-M\", \"--repl\"]" + "CMD [\"clj\"]")) diff --git a/target/debian-bookworm-25/latest/Dockerfile b/target/debian-bookworm-25/latest/Dockerfile index d3017c00..d2410c35 100644 --- a/target/debian-bookworm-25/latest/Dockerfile +++ b/target/debian-bookworm-25/latest/Dockerfile @@ -70,5 +70,4 @@ COPY rlwrap.retry /usr/local/bin/rlwrap COPY entrypoint /usr/local/bin/entrypoint ENTRYPOINT ["entrypoint"] - CMD ["-M", "--repl"] diff --git a/test/docker_clojure/dockerfile_test.clj b/test/docker_clojure/dockerfile_test.clj index 6fad6dde..44390108 100644 --- a/test/docker_clojure/dockerfile_test.clj +++ b/test/docker_clojure/dockerfile_test.clj @@ -29,19 +29,21 @@ :jdk-version 11}) "LABEL ")))) (testing "lein variant includes lein-specific contents" - (with-redefs [lein/contents (constantly ["leiningen vs. the ants"])] + (with-redefs [lein/install (constantly "leiningen vs. the ants")] (is (str/includes? (contents cfg/installer-hashes {:base-image-tag "base:foo" :distro :distro/distro :build-tool "lein" - :maintainer "Me Myself"}) + :maintainer "Me Myself" + :jdk-version 11}) "leiningen vs. the ants")))) (testing "tools-deps variant includes tools-deps-specific contents" - (with-redefs [tools-deps/contents (constantly - ["Tools Deps is not a build tool"])] + (with-redefs [tools-deps/install (constantly + "Tools Deps is not a build tool")] (is (str/includes? (contents cfg/installer-hashes {:base-image-tag "base:foo" :distro :distro/distro :build-tool "tools-deps" - :maintainer "Me Myself"}) + :maintainer "Me Myself" + :jdk-version 11}) "Tools Deps is not a build tool")))))