Skip to content
Draft
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
29 changes: 27 additions & 2 deletions bb.edn
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{:tasks {build {:doc "Build uberjar"
{:paths ["src/babashka"]
:tasks {build {:doc "Build uberjar"
;; FIXME: Consider if it would be better to handle Cljs build in separate classpath
:task (clojure "-T:shadow-cljs:frontend:backend:build uberjar")}
outdated {:doc "Check depdendencies"
Expand All @@ -19,4 +20,28 @@
:task (shell "npx @svgr/cli --out-dir target/gen/svg -- src/svg")}
icons {:doc "Generate React component JS files for svg files"
:depends [-svgr]
:task (shell "npx babel target/gen/svg --out-dir src/js/icons")}}}
:task (shell "npx babel target/gen/svg --out-dir src/js/icons")}

db-up {:doc "Start the database"
:task (shell "docker compose -p example-project up -d")}
db-down {:doc "Stop the database"
:task (shell "docker compose -p example-project down -d")}

native-image-agent {:doc "Run backend with native-image-agent to collect metadata for building a native image"
:requires ([native-image :as n])
:task (n/run-with-agent)}
native-image {:doc "Build native image"
:task (shell "clojure -T:build:shadow-cljs native-image")}
native-image-build {:doc "Builds uberjar, then native-image and runs the app for verification"
:task (do
(shell "bb build")
(shell "bb native-image-agent")
(shell "bb native-image")
(shell {:extra-env {"CONFIG_EDN" "resources/config.edn"
"HTTP_PORT" 3333}}
"target/app exit-after-start"))}

dev {:doc "Start backend development environment"
:task (do
(run 'db-up)
(shell "clojure -A:backend:dev:repl:build:shadow-cljs"))}}}
69 changes: 62 additions & 7 deletions build.clj
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
(ns build
(:require [shadow.cljs.devtools.api :as shadow]
[clojure.tools.build.api :as b]))
(:require [babashka.process :as p]
[babashka.process.pprint]
[clojure.tools.build.api :as b]
[shadow.cljs.devtools.api :as shadow]))

(def version "0.0.1-SNAPSHOT")
(def class-dir "target/classes")
(def uber-file "target/app.jar")

Expand All @@ -13,6 +14,7 @@
(defn clean [_]
(b/delete {:path "target"}))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn uberjar [_]
(clean nil)
(b/copy-dir {:src-dirs ["src/clj" "src/cljc" "resources"]
Expand All @@ -21,14 +23,67 @@
;; Build frontend:
(shadow/release :app)

#_
(b/compile-clj {:basis @basis
:ns-compile '[backend.main]
:class-dir class-dir})
:class-dir class-dir
:compile-opts {:direct-linking true}})

(b/uber {:class-dir class-dir
:uber-file uber-file
;; :main 'backend.main
:main 'backend.main
:basis @basis})

(println "Uberjar:" uber-file))


#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn native-image [_]
(println "Compiling GraalVM feature classes")
(b/javac {:src-dirs ["src/java"]
:class-dir "target/native-image-configuration/classes"
:basis @basis
:javac-opts ["--add-exports" "org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED"
"-Xlint:deprecation"]})

(println "Building native image")
(p/shell "native-image"

;; Clojure namespaces
"--features=clj_easy.graal_build_time.InitClojureClasses"

;; Buddy support
"--add-exports" "org.graalvm.nativeimage/org.graalvm.nativeimage.impl=ALL-UNNAMED"
"--features=graalvm.features.BouncyCastleFeature"

;; Logback related classes
"--initialize-at-build-time=ch.qos.logback"
"--initialize-at-build-time=ch.qos.logback.classic.Logger"
"--initialize-at-build-time=org.xml.sax"

;; Don't allow to fall back to launching a VM
"--no-fallback"

;; To make shutdown hooks work
"--install-exit-handlers"

"--enable-monitoring"

"--emit" "build-report=native-image-build-report.html"

"--enable-sbom=export"

"-H:+UnlockExperimentalVMOptions"
"-H:IncludeResources=swagger-ui/.*" ;; TODO: Should create META-INF/native-image/metosin/ring-swagger-ui/native-image.properties

"-H:+PrintClassInitialization"

"-cp" (str
;; From training run with native-image-agent
"target/native-image-configuration"
;; Compiled GraalVM feature classes, buddy support
":target/native-image-configuration/classes")

"--native-image-info"

"-jar" "target/app.jar"
"-o" "target/app")
(println "native image created: target/app"))
31 changes: 26 additions & 5 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
;; Hugsql
hikari-cp/hikari-cp {:mvn/version "3.1.0"}
migratus/migratus {:mvn/version "1.5.8"}
ring/ring-jetty-adapter {:mvn/version "1.12.2"}
#_#_ring/ring-jetty-adapter {:mvn/version "1.12.2"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.35.1"}
metosin/ring-swagger-ui {:mvn/version "5.17.14"}
metosin/ring-http-response {:mvn/version "0.9.4"}
metosin/muuntaja {:mvn/version "0.6.10"}
Expand All @@ -30,15 +31,30 @@
;; org.apache.logging.log4j/log4j-to-slf4j {:mvn/version "2.23.1"}
;; org.slf4j/jul-to-slf4j {:mvn/version "2.0.16"}
;; org.slf4j/jcl-over-slf4j {:mvn/version "2.0.16"}
org.clojure/tools.logging {:mvn/version "1.3.0"}}}
org.clojure/tools.logging {:mvn/version "1.3.0"}

;; For native-image, used by --features=clj_easy.graal_build_time.InitClojureClasses
com.github.clj-easy/graal-build-time {:mvn/version "1.0.5"}

;; For showing how to make buddy work in native-image
buddy/buddy-sign {:mvn/version "3.5.351"}
buddy/buddy-core {:mvn/version "1.11.423"}}}

:dev
;; Include test folder here for REPL,
;; using test alias with iced command would also apply main-opts
{:extra-paths ["dev/clj" "target/dev" "test/clj"]}

:repl
{:extra-deps {}}
{:extra-deps {cider/cider-nrepl {:mvn/version "0.50.2"}
Copy link
Member Author

Choose a reason for hiding this comment

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

I kind of disagree with including user specific REPL tools in the project configuration.

Usually, cider-nrepl middleware is added by the editor when launching the REPL process. With lein tools like hashp were easy to add to the ~/.lein/profiles.clj but I guess it isn't as easy with ~/.clojure/deps.edn because you'd still need to refer to a alias created there to enable those deps... hmm.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, agree too, was just for my own convenience here.

Use specific aliases need to be activated, so if a project has say a bb dev task bundled for easy startup, maybe there could be options in the task to specify those aliases, or actually, I wonder if babashka supports user-specific task files, since this user-specific customization needs kind of two things 1) deps.edn 2) starting with custom arguments...

nrepl/nrepl {:mvn/version "1.3.0"}
com.bhauman/rebel-readline {:mvn/version "0.1.4"}

hashp/hashp {:mvn/version "0.2.2"}}
:main-opts ["-m" "nrepl.cmdline"
"--middleware" "[cider.nrepl/cider-middleware]"
"--interactive"
"--repl-fn" "rebel-readline.main/-main"]}

:test
{:extra-paths ["test/clj"]
Expand All @@ -64,8 +80,13 @@
org.slf4j/slf4j-simple {:mvn/version "2.0.16"}}}

:build
{:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}}
:ns-default build}
{:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}

babashka/process {:mvn/version "0.5.22"}
babashka/fs {:mvn/version "0.5.22"}}

:ns-default build
:jvm-opts ["-XX:-OmitStackTraceInFastThrow" "-Dclojure.main.report=stderr"]}

:outdated
{:deps {com.github.liquidz/antq {:mvn/version "RELEASE"}
Expand Down
3 changes: 2 additions & 1 deletion dev/clj/user.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns user
(:require [integrant.repl :refer [clear go halt prep init reset reset-all]]
(:require [hashp.core]
[integrant.repl :refer [clear go halt prep init reset reset-all]]
[integrant.repl.state :as state]
[migratus.core :as migratus]))

Expand Down
109 changes: 109 additions & 0 deletions src/babashka/native_image.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
(ns native-image
(:require [babashka.fs :as fs]
[babashka.http-client :as http]
[babashka.process :as p]
[babashka.wait :as wait]
[cheshire.core :as json]
[clojure.string :as str]
[clojure.walk :as walk]))

(defn start-backend [{:keys [http-port extra-env timeout-ms command]}]
(p/shell "bb db-up")
(let [command (if (string? command)
[command]
command)
backend-process (apply p/process
{:inherit true
:extra-env extra-env}
command)]
(when (= :timeout (wait/wait-for-port "localhost" http-port
{:timeout timeout-ms
:default :timeout}))
(p/destroy-tree backend-process)
(throw (Exception. (str "Backend didn't start in " timeout-ms "ms"))))
backend-process))

(defn run-training [http-port]
(let [base-url (format "http://localhost:%s" http-port)
test-requests [;; Database access
[200 (str base-url "/api/todo")]
;; Buddy
[200 (str base-url "/api/buddy-test")]]]
(doall (keep (fn [[expected-status url]]
(let [response (http/get url {:throw false})]
(when (not= expected-status (:status response))
response)))
test-requests))))

(defn check-failures [failures]
(when (not (zero? (count failures)))
(println "Training requests failed:")
(doseq [failure failures]
(prn failure))
(System/exit 1)))

(defn remove-clojure-name [coll clojure-names keeps]
(remove (fn [{:keys [type glob]}]
(let [s (or glob
(when (and type
(string? type))
type))]
(if s
(some #(and (or (.startsWith s %)
(.contains s %))
(not (some (fn [k]
(.contains % k))
keeps)))
clojure-names)
false)))
coll))

(defn post-process-metadata
"Removes Clojure relates entries from the metadata file, idea is to keep only entries from dependencies

Apparently, keeping Clojure entries leads into problems with code that modifies dynamic variables such as *warn-on-reflection*"
[filename]
(fs/copy filename (str filename ".orig"))
(let [data (json/parse-string (slurp filename) true)
clojure-names-atom (atom #{"clojure"})]
(walk/postwalk (fn [o]
(when (and (string? o)
(or (.endsWith o ".clj")
(.endsWith o ".cljc")))
(let [clojure-name (str/replace o
(if (.endsWith o ".clj")
".clj"
".cljc")
"")]
(swap! clojure-names-atom conj clojure-name (str/replace clojure-name "/" "."))))
o)
data)
(let [new-data (-> data
(update :reflection #(remove-clojure-name % @clojure-names-atom #{"jetty9"})) ;; To support ring-jetty9-adapter
(update :resources #(remove-clojure-name % @clojure-names-atom #{}))
(update :jni #(remove-clojure-name % @clojure-names-atom #{})))]
(spit filename (json/generate-string new-data {:pretty true})))))

;; Do a training run to gather metadata via native-image-agent
;; Note that should not refer to any unnecessary test resources on the classpath during training,
;; since those will get included into the metadata
(defn run-with-agent []
(let [http-port 3322
tracing-process (start-backend {:http-port http-port
:extra-env {"CONFIG_EDN" "resources/config.edn"
"HTTP_PORT" http-port}
:timeout-ms 5000
:command ["java" (str "-agentlib:native-image-agent="
"config-output-dir=target/native-image-configuration/META-INF/native-image")
"-jar" "target/app.jar"]})
failures (run-training http-port)]
(p/destroy-tree tracing-process)
(check-failures failures)
;; Wait for native-image-agent to create the tracing file
(when (= :timeout (wait/wait-for-path "target/native-image-configuration/META-INF/native-image/reachability-metadata.json"
{:timeout 5000
:default :timeout}))
(println "Reachability metadata file not created")
(System/exit 1)))

(post-process-metadata "target/native-image-configuration/META-INF/native-image/reachability-metadata.json"))
17 changes: 12 additions & 5 deletions src/clj/backend/main.clj
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
(ns backend.main
(:require [aero.core :as aero]
[backend.routes]
[clojure.java.io :as io]
[clojure.tools.logging :as log]
[cognitect.transit]
[hikari-cp.core :as hikari-cp]
[integrant.core :as ig]
[next.jdbc.date-time]
[reitit.ring.middleware.exception]
[ring.adapter.jetty :as jetty]))
[ring.adapter.jetty9 :as jetty]
#_[ring.adapter.jetty :as jetty])
(:gen-class))

(set! *warn-on-reflection* true)
Copy link
Member Author

Choose a reason for hiding this comment

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

I guess it is good idea to set this in any ns that does any JVM interop. I wonder if there is a way to enable this for the whole project, or is that something we would want.


(defmethod aero.core/reader 'ig/ref
[_opts _tag value]
(ig/ref value))

(defn system-config []
(aero/read-config (io/resource "config.edn")))
(aero/read-config (or (System/getenv "CONFIG_EDN")
"config.edn")))
Comment on lines +21 to +22
Copy link
Member Author

Choose a reason for hiding this comment

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

I think adding the env var here does make sense, but AFAIK removing the io/resource call does break the case of reading the file from classpath.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think this might be a mistake even, was trying to avoid putting the config.edn into the resulting native image, since classpath resource loading is captured via the agent run, but here the config is actually a aero config, which just bundles defaults and instructs reading from environment variables, so we actually want the file in the native binary too...

Though, I think in the past I've found use in being able to specify alternative aero config.edn, that can be loaded outside the classpath, but maybe this is not useful in an example template.


(defmethod ig/init-key :adapter/jetty [_ {:keys [port routes] :as jetty-opts}]
(log/infof "Starting Jetty server on http://localhost:%s" port)
Expand Down Expand Up @@ -48,5 +52,8 @@
(catch Throwable t
(log/error t "Failed to start system"))))

(defn -main []
(run-system (system-config)))
(defn -main [& args]
(run-system (system-config))
(when (= "exit-after-start" (first args))
(log/info "Exiting")
(System/exit 0)))
30 changes: 28 additions & 2 deletions src/clj/backend/routes.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(ns backend.routes
(ns backend.routes
(:require [backend.api.todo :as todo]
[clojure.edn :as edn]
[clojure.java.io :as io]
Expand All @@ -15,7 +15,10 @@
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[ring.util.http-response :as resp]))
[ring.util.http-response :as resp]
[buddy.sign.jwt :as jwt]
buddy.sign.util
[buddy.core.keys :as buddy-keys]))

(def muuntaja-instance
(m/create (-> m/default-options
Expand Down Expand Up @@ -66,10 +69,33 @@
[:h1 "Loading, please wait..."]]]
[:script {:type "text/javascript" :src (str "/js/" (main-js-file))}]]]))

(def buddy-test-route
["/buddy-test" {:get (fn [_]
(try
(let [id-token "eyJraWQiOiJVd3VIdERJeXVYTGs5NytmUlVHeTIzM3pNRnVCMkZPc2ErcURBMUt4OGNRPSIsImFsZyI6IlJTMjU2In0.eyJhdF9oYXNoIjoiQkNkbC16MEowRXkzb25DU1cwUndLZyIsInN1YiI6IjZlNWEwNTE4LTNkNzctNDA2NS05MjVkLWUzZGM3YTU4MjkyYiIsImNvZ25pdG86Z3JvdXBzIjpbImFkbWluIl0sImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9XNDVGSUU0SzciLCJjb2duaXRvOnVzZXJuYW1lIjoia2ltbW9AcmVwbGlhbmNlLmNvbSIsImF1ZCI6ImVyMHFkbGwwODdncnJhY20wM2tsMmU0OGEiLCJldmVudF9pZCI6IjkzMDhiMDU5LTk2Y2EtNGQwNi05NzNmLWI5YzcwNGM3NmVmNCIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNjY1NzMzODQyLCJleHAiOjE2NjU3NjI2NDIsImlhdCI6MTY2NTczMzg0MiwianRpIjoiZTAyZGE1ZDctMjZlZC00OGJlLWEwMDMtMzJhMGMwMzM1ZDI5IiwiZW1haWwiOiJraW1tb0ByZXBsaWFuY2UuY29tIn0.p54_PyPrK0z_g87FikyKh4pkTpMeITP27TPyeBzc8dSCbNuShR8epGpw2Ag1rcBaKS1Tcig2qicnJdHlCHuG6nkVvhjnu4I3DjH45b1Nx3neV5ii5t6YZ1GNTipzqypVd_Y9pdSxYCZYrwA442LzLSLKgzJlYD5sDB91iL0KAmcYx5JLHJ0Tpm__BILt-SHm3s3nRhbYe6isrSHeOdr19NIFOL5S5UYYS641Tlz5MQyEFHQ9tmSCMv3BudnaaImacpRQVy-uu5zseP5Db9BGjPtCoRHnpYpF8YZC9fLj2hY3AMiBBEkmfXhIh6dUnPahC8bwGaDk36OWnMVw1hxbyA"
jwt-key {:alg "RS256",
:e "AQAB",
:kid "UwuHtDIyuXLk97+fRUGy233zMFuB2FOsa+qDA1Kx8cQ=",
:kty "RSA",
:n "qos6mdmypWvK74e7z9-JT5x-0ua4Sjjo5_uV-AKdkx3s7n_oAV7TfYBIYaFDfRxkTDIfWIy2yQUqUlkkxLHn7MiEF0m6mP7Wwb0tdSxgYOF9TmciPDGnxjSAgdi6E8sb0jHYnN79U0DJI-mpxB1v79MFwR6erxDROikQVtecnj-4vVhhjxc6q098HrrOIRMZIXvyEhdkbB_UTr6u8-OYAAHX2GuDgkmnX6rpUQLOqv66WxSsebWvj88UJZWqQ6KoG9gV2KvzxvwiO0gV0ePnzZ6p8oiEqhK6sxDBksXDY9vASQwW1xDcLMY_iOSJ-YpaeFiTD33Oevkyv0nRzLhe5w",
:use "sig"}]
(jwt/unsign id-token
(buddy-keys/jwk->public-key jwt-key)
{:alg :rs256,
:aud "er0qdll087grracm03kl2e48a",
:iss "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_W45FIE4K7"}))
(catch Exception e
(let [{:keys [type cause] :as data} (ex-data e)]
(assert (= :validation type))
(assert (= :exp cause))
(prn data))))
{:status 200})}])

(defn app [env]
(ring/ring-handler
(ring/router
["/api"
buddy-test-route
["/todo"
[""
{:summary "Return a list of todo items"
Expand Down
Loading