Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
#!/usr/bin/env joker
;; -*- mode: clojure -*-
;; 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 '[joker.tools.cli :as cli]
'[joker.os :as os]
'[joker.string :as str]
'[joker.pprint :as pp])
;; 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)
(os/exit (if error? 1 0)))))
(defn die!
"Show error on stderr and exit"
[msg]
(binding [*out* *err*]
(println msg)
(os/exit 1)))
(defn checked-exec
"Invoke a command"
[{:keys [cmd interactive? args]}]
(let [result (os/exec (name cmd)
(cond-> {:args (map str args)
:stdout *out*
:stderr *err*}
interactive?
(assoc :stdin *in*)))]
(when-not (zero? (:exit result))
(os/exit (:exit result)))))
(defn clojure-task
"Invoke clojure with args. "
[{:keys [interactive? main? alias args]}]
(let [extra (when (some? alias) {:aliases {:klein alias}})]
(checked-exec
{:cmd (if interactive? :clj :clojure)
:args (cond->> args
(some? extra)
(concat ["-Sdeps"
(pr-str extra)
(if main? "-M:klein" "-X:klein")]))
:interactive? interactive?})))
(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]
(let [parent (drop-last (str/split path #"/"))]
(when (seq parent)
(str/join "/" parent))))
(defn git-version
"Infer project version from git"
[]
(let [{:keys [exit out]} (os/exec "git"
{:args ["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 (str path "/deps.edn")]
(when (os/exists? filepath)
(-> filepath
slurp
read-string
:klein/config
update-version
(assoc :basedir path)))))
(defn find-project
"Walk up the directory hierarchy, looking for a deps.edn file"
[]
(loop [path (os/cwd)]
(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)
(checked-exec {:cmd :rm :args (concat ["-rf"] dirs)}))))
(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")
(os/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