Skip to content

Instantly share code, notes, and snippets.

@borkdude
Last active September 23, 2021 12:39
Show Gist options
  • Save borkdude/c34e8e44eb5b4a6ca735bf8a86ff64fa to your computer and use it in GitHub Desktop.
Save borkdude/c34e8e44eb5b4a6ca735bf8a86ff64fa to your computer and use it in GitHub Desktop.
#!/usr/bin/env bb
;; Ported from https://gist.github.com/pyr/d5e17af9c572b681a57de52895437298 to babashka
;; klein aims to be a small joker script to mimick
;; most of leiningen's default behavior while minimizing
;; divergence from standard facilities provided by
;; tools.deps
;; This is built as a single file script to simplify
;; deployment and will avoid requiring any code beyond
;; facilities provided by joker itself
;; klein is configured in the same deps.edn file as your
;; project and tries to provide sensible defaults
;;
;; configuration can be provided in at the `:klein/config`
;; key
;;
;; {:paths ["src"]
;; :deps {org.clojure/clojure {:mvn/version "1.10.2"}
;;
;; :klein/config
;; {:artifact spootnik/foo
;; :version :git
;; :description "Trying out klein"
;; :main foo.core
;; :clean-targets ["pom.xml"]}}
(require '[babashka.deps :as deps]
'[babashka.fs :as fs]
'[babashka.process :as p]
'[clojure.java.shell :refer [sh]]
'[clojure.pprint :as pp]
'[clojure.string :as str]
'[clojure.tools.cli :as cli])
;; Global configuration
(def version
"0.1.0-alpha1")
(def argdefs
"Argument definitions for klein. `:in-order true` is passed to allow
tasks to use `parse-opts` again if need be."
[["-h" "--help" "Print this help or help for a specific task" :id :help?]])
(def tasks
"List of known top-level tasks."
{:clean {:desc "Clean transient files for a project"
:need-project? true}
:repl {:desc "Start a repl in the current project"
:need-project? true}
:deps {:desc "Show dependency tree for a project"
:need-project? true}
:jar {:desc "Build JAR for a project"
:need-project? true}
:uberjar {:desc "Build standalone JAR for a project"
:need-project? true}
:test {:desc "Run project test suite"
:need-project? true}
:run {:desc "Run project main function"
:need-project? true}
:pprint {:desc "Show runtime data and exit"
:need-project? true}
:help {:desc "Show usage and exit"}
:version {:desc "Show version and exit"}})
(def default-clean-targets ["target"])
(defmulti run-task
"This function is called once a command line invocation has been resolved
into a valid klein task.
All tasks receive a map of the following structure:
{:id task-id ;; #{:clean :repl :deps :jar ...}
:arguments ;; map of extra arguments given
:summary ;; summary of the task
:project} ;; The content of the project's deps.edn file"
:id)
(defn usage!
"Show usage and exit"
[summary errors]
(let [error? (some? errors)]
(binding [*out* (if error? *err* *out*)]
(when error?
(println "errors:")
(doseq [e errors]
(println " " e))
(println ""))
(println "usage: klein [options] task")
(println "available tasks:")
(doseq [[k v] tasks]
(printf " %-10s %s\n" (name k) (:desc v)))
(println "global options:")
(println summary)
(System/exit (if error? 1 0)))))
(defn die!
"Show error on stderr and exit"
[msg]
(binding [*out* *err*]
(println msg)
(System/exit 1)))
(defn clojure-task
"Invoke clojure with args."
[{:keys [main? alias args]}]
(let [extra (when (some? alias) {:aliases {:klein alias}})]
(some-> (deps/clojure (cond->> args
(some? extra)
(concat ["-Sdeps"
(pr-str extra)
(if main? "-M:klein" "-X:klein")])))
p/check)
nil))
(defn match-task
"Find task by name in the main task map"
[tname]
(first
(for [[k v] tasks :when (= tname (name k)) :let [t (assoc v :id k)]]
t)))
(defn parent-dir
"Yield the parent of the current dir, if any"
[path]
(fs/parent path))
(defn git-version
"Infer project version from git"
[]
(let [{:keys [exit out]} (apply sh "git" "describe" "--always"
"--dirty" "--tags")]
(if (or (not (zero? exit))
(str/blank? out))
(die! "cannot infer version from git")
(str/trim out))))
(defn update-version
"Allow inferring version from git, if the provided
version is :git"
[{:keys [version] :as project}]
(cond
(nil? version) (die! "no project version supplied")
(= :git version) (assoc project :version (git-version))
:else project))
(defn find-project-in-dir
"Try to find a project file in the provided path"
[path]
(let [filepath (fs/path path "deps.edn")]
(when (fs/exists? filepath)
(-> filepath
fs/file
slurp
read-string
:klein/config
update-version
(assoc :basedir (str (-> path
fs/absolutize
fs/normalize)))))))
(defn find-project
"Walk up the directory hierarchy, looking for a deps.edn file"
[]
(loop [path (fs/path ".")]
(when-not (nil? path)
(or (find-project-in-dir path)
(recur (parent-dir path))
(die! "no project file found, cannot continue")))))
(defmethod run-task :clean
[{:keys [project]}]
(let [clean-targets (:clean-targets project)
replace-targets? (:replace-targets? project)
basedir (:basedir project)
targets (cond-> clean-targets (not replace-targets?)
(concat default-clean-targets))
dirs (map (partial format "%s/%s" basedir) targets)]
(when (seq targets)
(doseq [dir dirs]
(fs/delete-tree dir)))))
(defmethod run-task :repl
[_]
(clojure-task
{:interactive? true}))
(defmethod run-task :run
[{:keys [project]}]
(clojure-task
{:args ["-M" "-m" (:main project)]}))
(defmethod run-task :jar
[{:keys [project]}]
(let [jar-name (or (:jar-name project)
(format "target/%s-%s.jar"
(name (:artifact project))
(:version project)))]
(clojure-task
{:alias {:replace-deps {'seancorfield/depstar
{:mvn/version "2.0.165"}}
:ns-default 'hf.depstar}
:args ["jar" :jar jar-name]})))
(defmethod run-task :uberjar
[{:keys [project]}]
(let [jar-name (or (:jar-name project)
(format "target/%s-%s-standalone.jar"
(name (:artifact project))
(:version project)))
version (:version project)
group-id (namespace (:artifact project))
artifact-id (name (:artifact project))
main (when-let [class (:main project)]
[":main-class" (str class) ":aot" "true"
":sync-pom" "true" ":group-id" group-id
":artifact-id" artifact-id ":version" (pr-str version)])]
(clojure-task
{:alias {:replace-deps {'seancorfield/depstar
{:mvn/version "2.0.165"}}
:ns-default 'hf.depstar}
:args (concat ["uberjar" ":jar" jar-name] main)})))
(defmethod run-task :test
[{:keys [arguments]}]
(clojure-task
{:alias {:extra-paths ["test"]
:extra-deps {'com.cognitect/test-runner
{:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "209b64504cb3bd3b99ecfec7937b358a879f55c1"}}
:main-opts ["-m" "cognitect.test-runner"]}
:args arguments
:main? true}))
(defmethod run-task :deps
[{:keys [arguments]}]
(clojure-task
{:args (concat ["-X:deps" "tree"] arguments)}))
(defmethod run-task :help
[{:keys [summary]}]
(usage! summary nil))
(defmethod run-task :version
[_]
(println version))
(defmethod run-task :pprint
[{:keys [project]}]
(pp/pprint project))
(let [{:keys [options] :as res} (cli/parse-opts *command-line-args* argdefs
:in-order true)
arguments (:arguments res)
errors (:errors res)
summary (:summary res)
task (match-task (first arguments))]
(when (or (some? errors) (empty? arguments) (nil? task) (:help? options))
(usage! summary errors))
(let [project (find-project)]
(when (and (:need-project? task) (nil? project))
(binding [*out* *err*]
(println "could not find project root")
(System/exit 1)))
(run-task (assoc task
:arguments (rest arguments)
:summary summary
:project project))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment