Leiningen middleware which computes the "version" at build-time - from the ambient git context (think latest tag).
Normally, Leiningen projects explicitly provide a version
within the project.clj
file, as the 2nd argument to defproject
like this:
(defproject my-app "3.4.5" ;; <--- "3.4.5" is the version
...)
But, when using this Leiningen middleware, your defproject
will not contain an explicit version, and instead will contain a placeholder string, "lein-git-inject/version" like this:
(defproject my-app "lein-git-inject/version" ;; <--- the version is now a placholder string
...)
Then, at build time, this middleware will:
the computed version
.the computed version
As an added bonus, it also facilitates embedding the computed version
(and certain other build-time values)
within your ClojureScript application, making it readily available at run-time for purposes like logging.
Imagine you are at the command line in a git repo, and you execute:
$ git describe --tags --dirty=-dirty
If the latest tag in your branch was v1.0.4
, this command might output something like:
v1.0.4-3-g975b-dirty
which encodes four (hyphen separated) values which we refer to as "the ambient git context":
This middleware creates the computed version
from these four "ambient" values by applying two rules:
the computed version
will just be the latest tag (eg: 1.0.4
)the computed version
will be the tag suffixed with -<ahead-count>-<short-ref>-SNAPSHOT
, e.g. 1.0.4-3-g975b-SNAPSHOT
Note: only part of the latest tag is used (just 1.0.4
, not the full string v1.0.4
) but that's explained in the next section.
So far, we have said the computed version
is created using the "latest tag". While that is often true, it is not the whole story, which is acually as follows:
#"^v(\d+\.\d+\.\d+)$"
v1.2.3
(the string "v"
followed by a semver, "N.N.N"
)the computed version
.So, this middleware will traverse backwards through the history of the current commit looking for a tag which has the right structure (matches the regex), and when it finds one, it is THAT tag which is used to create the computed version
- it is that tag against which the "ahead count" will be calculated, etc.
Please be aware of the following:
the computed version
will be git-version-tag-not-found
git
executable. If this executable is not in the PATH, then you'll see messages on stderr
and the computed version
will be git-command-not-found
lein release
will massage the version
in your defproject
in unwanted ways unless you take specific actions to stop it (see the "Annotated Example" below)The two-step narative presented above says this middleware:
the computed version
defproject
with the computed version
While that's true, it is a simplification. The real steps are:
the computed version
is just oneEDN
in
the defproject
, looking for four special string values and, where they are found, it will replace them with the associated computed value from step 1.So, the special string "lein-git-inject/version" will be replaced anywhere it is found within the defproject
EDN, and not just if it appears as the defproject
version argument.
When you consider this second step, keep in mind that Leiningen middleware runs
very early in the Lein build pipeline. So early, in fact, that it can alter the EDN
of your defproject
before it is interpreted by Lein.
The four special strings supported - referred to as substitution keys
- are:
substituion key | example replacement |
---|---|
"lein-git-inject/version" | "12.4.1-2-453a730-SNAPSHOT" |
"lein-git-inject/build-iso-date-time" | "2019-11-18T00:05:02.273361" |
"lein-git-inject/build-iso-date-week" | "2019-W47-2" |
"lein-git-inject/user-name" | "Isaac" |
Note #1: to debug these substitutions, I'd recommend adding the lein-pprint plugin, so you can use lein pprint
to see the entire project map after the substitutions have taken place.
Note #2: the substitution keys are strings, even though
keywords seem like a more idiomatic choice. Why? Turns out that when you are using the Cursive IDE,
the 2nd argument to defproject
(the version!) can't be a keyword.
Only a string can go there because Cursive does its own inspection of your project.clj
independently of Lein and it doesn't like a keyword there, as the 2nd argument.
So string keys were necessary. And there is less cognitive load if there
is only one way to do something - so we reluctantly said "no" to allowing keyword keys too.
This middleware provides a way to embed any of these four build-time values into our ClojureScript application. This is often a very useful outcome - these values are useful at runtime for display and logging purposes. And it can be achieved in an automated, DRY way.
The trick is to place the substitution keys into specific places within the defproject
- ones which control
the actions of the ClojureScript compiler. We want to take advantage of the :closure-defines
feature feature of the ClojureScript complier which permits us to "set" values for defs
at compile time.
Below, the Annotated Example demonstrates how to achive this outcome using shadow-clj.
Here's how to write your project.clj
to achieve the three steps described above...
;; On the next line, note that the version (2nd argument of defproject) is a
;; substitution key which will be replaced by `the computed version` which is
;; built from `the ambient git context`, using `the method`.
(defproject day8/lein-git-inject-example "lein-git-inject/version"
...
:plugins [[day8/lein-git-inject "0.0.5"] ;; <--- you must include this plugin
[lein-shadow "0.1.7"]]
:middleware [leiningen.git-inject/middleware] ;; <-- you must include this middleware
;; Embedding
;; If you are using the shadow-clj compiler and lein-shadow, the shadow-cljs
;; configuration is put here in project.clj. Below is an example of how to
;; combine this middleware with a `:clojure-define` in order to
;; inject build-time values into your application, for later run-time use.
;;
;; You'll notice the use of the substitution key "lein-git-inject/version".
;; At build time, this middleware will replace that keyword with `the computed version`.
;; In turn, that value is used within a `:clojure-define` to bind it
;; to a var, via a `def` in your code (called `version` within the namespace `some.namespace`).
:shadow-cljs {:builds {:app {:target :browser
:release {:compiler-options {:closure-defines {some.namespace.version "lein-git-inject/version"}}}}}}
;; Note: by default, lein will change the version in project.clj when you do a `lein release`.
;; To avoid this (because you now want the version to come from the git context at build time),
;; explicitly include the following steps to avoid using the default release process provided by lein.
:release-tasks [["vcs" "assert-committed"]
["deploy"]]
;; Optional configuration
;; Here is where you can supply an alternative regex to identify `version tags`.
;; When designing your own textual structure for "version tags", remember that
;; git tags are git references and that there are rules about well formedness.
;; For example, you can't have a ":" in a tag. See https://git-scm.com/docs/git-check-ref-format
;; The regex you supply has two jobs:
;; 1. to "match" a version tag
;; 2. to return one capturing group which extracts the text within the tag which is to
;; be used as the version. In the example below, the regex will match the tag "version/1.2.3"
;; but it will capture the "1.2.3" part and it is THAT part which will be used in the computed version.
:git-inject {
:version-pattern #"^version\/(.*)$" }
)
Copyright © 2019 Mike Thompson
Derived from cuddlefish © 2018 Reid "arrdem" McKenzie
Derived from lein-git-version © 2017 Reid "arrdem" McKenzie
Derived from lein-git-version © 2016 Colin Steele
Derived from lein-git-version © 2011 Michał Marczyk
Distributed under the Eclipse Public License, the same as Clojure.
Can you improve this documentation? These fine people already did:
Mike Thompson, Isaac Johnston & Gregg8Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close