- Copy over the
Makefile
to your Clojure project. - Run
make help
to see what you can do.
[ A short thread on what using Metaclj looks like: <link> (<date>) ]
- Quickstart
- Introduction
- The PHONY section of the Makefile
- The common variables section of the Makefile
- Set Bash as the shell for executing commands
- The default goal section of the Makefile
- The help target to explain what you can do with the Makefile
- The Main Opts and REPL targets for the Clojure CLI tool
- The Enrich section of the Makefile
- The clj-kondo section of the Makefile
- The zprint section of the Makefile
- The .gitignore section of the Makefile
- The CI/CD section of the Makefile
- The section to upgrade Clojure libraries in the current project
- The section to build the Clojure project
- The section to deploy the Clojure project to production
- The section to clean existing artifacts from the Clojure project
Whenever I start a new Clojure project, I run through a series of steps to standardize the project’s developer experience. This project is an attempt to short-circuit that, and also to note all the steps down for my own reference in the future.
To make a change to Metaclj’s Makefile, edit this file and then
type C-c C-v C-t
(M-x org-babel-tangle
) to publish the changes.
A target in a Makefile is meant to be an actual file that the
“execution” of the target creates. Sometimes, we want “actions” as
target – think make check
or make build
, we do not expect a file
called check
or a file called build
to be created. We call these
targets as phony targets, and informing make
helps it avoid checking
for the existence of these files.
Our PHONY targets are as follows:
- Installing tools (generally you only need to do this once)
install-antq
(See: The section to upgrade Clojure libraries in the current project)install-kondo-configs
(Installing linting configuration. See: The clj-kondo section of the Makefile)install-zprint-config
(Installing formatting configuration. See: The zprint section of the Makefile)install-gitignore
(Installing a good gitignore file. See: The .gitignore section of the Makefile)
- Running our Clojure project
repl
(Starting the Clojure REPL. See: The Main Opts and REPL targets for the Clojure CLI tool)repl-enrich
(See: The Enrich section of the Makefile)
- Testing our Clojure project
check
(Checking for code-standards in CI/CD. See: The section to check that the code is good)test
(Running Polylith tests. See: The section to test that the code is working correctly)test-coverage
(Running Clofidence)
- Upgrading libraries and tooling
upgrade-libs
(Upgrading all the dependencies. See: The section to upgrade Clojure libraries in the current project)install-kondo-configs
(See: The clj-kondo section of the Makefile)
- Deploying our Clojure project to Production
build
(See: <Creating Artifacts section>)serve
(See: <Running Artifacts section>)deploy
(See: <Deploying Artifacts section>)
- Deleting artifacts and going back to default state
clean
(See: <Cleaning Artifacts section>)
.PHONY: install-antq install-kondo-configs install-zprint-config install-gitignore repl-enrich repl check-cljkondo check-tagref check-zprint-config check-zprint check test test-all test-coverage upgrade-libs build serve deploy clean-projects clean
These are variables we use in some of the other Makefile
targets.
You can ignore them for now and we’ll review them when we use them.
HOME := $(shell echo $$HOME)
HERE := $(shell echo $$PWD)
CLOJURE_SOURCES := $(shell find . -name '**.clj')
# Set bash instead of sh for the @if [[ conditions,
# and use the usual safety flags:
SHELL = /bin/bash -Eeu
The .DEFAULT_GOAL
is what runs when we only run the make
command
with no directive. In our case, we want this to start a Clojure REPL
in our project. (See: <REPL section>)
.DEFAULT_GOAL := repl
Here, we use awk
to filter out all the targets which have a doc-string. This is the “public API” of the Makefile, so to speak.
/^[a-zA-Z0-9_-]+:.*##/
is a pattern that matches lines starting with
a target name (letters, numbers, underscores, or hyphens) followed by
a colon, followed by ##
somewhere in the line. This finds Makefile
target definitions.
In the print command:
%-25s
formats the target name left-aligned in 25 characterssubstr($$1, 1, length($$1)-1)
takes the target name (first field) without the trailing colonsubstr($$0, index($$0,"##")+3)
extracts everything after ## (the comment)
help: ## A brief explanation of everything you can do
@awk '/^[a-zA-Z0-9_-]+:.*##/ { \
printf "%-25s # %s\n", \
substr($$1, 1, length($$1)-1), \
substr($$0, index($$0,"##")+3) \
}' $(MAKEFILE_LIST)
Here we define the aliases that we select when we run make repl
, and
the repl
target.
If you wish to override these aliases, you can do so by defining an
environment variable called DEPS_MAIN_OPTS
(for example, in the
.envrc
file, See: <12-factor App Configuration section>)
To understand what these aliases do and how to install them in your own project, see: <Clojure CLI Aliases section>.
# The Clojure CLI aliases that will be selected for main options for `repl`.
# Feel free to upgrade this, or to override it with an env var named DEPS_MAIN_OPTS.
# Expected format: "-M:alias1:alias2"
DEPS_MAIN_OPTS ?= "-M:dev:test:logs-dev:cider-storm"
repl: ## Launch a REPL using the Clojure CLI
clojure $(DEPS_MAIN_OPTS);
mx.cider/enrich-classpath
is a tool to download and add Java sources
for your Clojure projects. With this tool, you can M-.
into Java
sources too, which is an incredibly powerful tool when you want to
inspect the source-code of the functions and libraries you are using.
This section of the Makefile
is taken from the Enrich documentation
itself. The only drawback here is that I have to manually bump the
Enrich version periodically.
# The enrich-classpath version to be injected.
# Feel free to upgrade this.
ENRICH_CLASSPATH_VERSION="1.19.3"
# Create and cache a `clojure` command. deps.edn is mandatory; the others are optional but are taken into account for cache recomputation.
# It's important not to silence with step with @ syntax, so that Enrich progress can be seen as it resolves dependencies.
.enrich-classpath-repl: Makefile deps.edn $(wildcard $(HOME)/.clojure/deps.edn) $(wildcard $(XDG_CONFIG_HOME)/.clojure/deps.edn)
cd $$(mktemp -d -t enrich-classpath.XXXXXX); clojure -Sforce -Srepro -J-XX:-OmitStackTraceInFastThrow -J-Dclojure.main.report=stderr -Sdeps '{:deps {mx.cider/tools.deps.enrich-classpath {:mvn/version $(ENRICH_CLASSPATH_VERSION)}}}' -M -m cider.enrich-classpath.clojure "clojure" "$(HERE)" "true" $(DEPS_MAIN_OPTS) | grep "^clojure" > $(HERE)/$@
# Launches a repl, falling back to vanilla Clojure repl if something went wrong during classpath calculation.
repl-enrich: .enrich-classpath-repl ## Launch a repl enriched with Java source code paths
@if grep --silent "^clojure" .enrich-classpath-repl; then \
echo "Executing: $$(cat .enrich-classpath-repl)" && \
eval $$(cat .enrich-classpath-repl); \
else \
echo "Falling back to Clojure repl... (you can avoid further falling back by removing .enrich-classpath-repl)"; \
clojure $(DEPS_MAIN_OPTS); \
fi
clj-kondo
is the goto linter tool of the Clojure community. Run
make install-kondo-configs
regularly to ensure that the latest
clj-kondo
configuration is downloaded for all the libraries you use
in the project. Having this configuration makes the programming
experience significantly richer as it teaches clj-kondo
about the
custom code introduced by your dependencies.
.clj-kondo:
mkdir .clj-kondo
install-kondo-configs: .clj-kondo ## Install clj-kondo configs for all the currently installed deps
clj-kondo --lint "$$(clojure -A:dev:test:cider:build -Spath)" --copy-configs --skip-lint
zprint
is my favorite formatting tool from the Clojure world. I
install it using bbin
(<described here>) and use it in all my
projects for automatically formatting the code as I write it. I never
think about indentation anymore, I just let zprint
do it’s magic.
zprint
is extremely aggressive, which is why I do not use it the
default formatting tool. If I did, every time I edit code in an
external library that I do not own, I’d trigger massive indentation
changes in the library. But I definitely add it to every project I
write / maintain myself.
Run make install-zprint-config
to add the relevant configuration to
the project.
check-zprint-config:
@echo "Checking (HOME)/.zprint.edn..."
@if [ ! -f "$(HOME)/.zprint.edn" ]; then \
echo "Error: ~/.zprint.edn not found"; \
echo "Please create ~/.zprint.edn with the content: {:search-config? true}"; \
exit 1; \
fi
@if ! grep -q "search-config?" "$(HOME)/.zprint.edn"; then \
echo "Warning: ~/.zprint.edn might not contain required {:search-config? true} setting"; \
echo "Please ensure this setting is present for proper functionality"; \
exit 1; \
fi
.zprint.edn:
@echo "Creating .zprint.edn..."
@echo '{:fn-map {"with-context" "with-meta"}, :map {:indent 0}}' > $@
.dir-locals.el:
@echo "Creating .dir-locals.el..."
@echo ';;; Directory Local Variables -*- no-byte-compile: t; -*-' > $@
@echo ';;; For more information see (info "(emacs) Directory Variables")' >> $@
@echo '((clojure-dart-ts-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojure-jank-ts-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojure-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojure-ts-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojurec-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojurec-ts-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojurescript-mode . ((apheleia-formatter . (zprint))))' >> $@
@echo ' (clojurescript-ts-mode . ((apheleia-formatter . (zprint)))))' >> $@
install-zprint-config: check-zprint-config .zprint.edn .dir-locals.el ## Install configuration for using the zprint formatter
@echo "zprint configuration files created successfully."
It’s good to have a default .gitignore file that just works. That’s
what make install-gitignore
does.
.gitignore:
@echo "Creating a .gitignore file"
@echo '# Artifacts' > $@
@echo '**/classes' >> $@
@echo '**/target' >> $@
@echo '**/.artifacts' >> $@
@echo '**/.cpcache' >> $@
@echo '**/.DS_Store' >> $@
@echo '**/.gradle' >> $@
@echo 'logs/' >> $@
@echo '' >> $@
@echo '# 12-factor App Configuration' >> $@
@echo '.envrc' >> $@
@echo '' >> $@
@echo '# User-specific stuff' >> $@
@echo '.idea/**/workspace.xml' >> $@
@echo '.idea/**/tasks.xml' >> $@
@echo '.idea/**/usage.statistics.xml' >> $@
@echo '.idea/**/shelf' >> $@
@echo '.idea/**/statistic.xml' >> $@
@echo '.idea/dictionaries/**' >> $@
@echo '.idea/libraries/**' >> $@
@echo '' >> $@
@echo '# File-based project format' >> $@
@echo '*.iws' >> $@
@echo '*.ipr' >> $@
@echo '' >> $@
@echo '# Cursive Clojure plugin' >> $@
@echo '.idea/replstate.xml' >> $@
@echo '*.iml' >> $@
@echo '' >> $@
@echo '/example/example/**' >> $@
@echo 'artifacts' >> $@
@echo 'projects/**/pom.xml' >> $@
@echo '' >> $@
@echo '# nrepl' >> $@
@echo '.nrepl-port' >> $@
@echo '' >> $@
@echo '# clojure-lsp' >> $@
@echo '.lsp/.cache' >> $@
@echo '' >> $@
@echo '# clj-kondo' >> $@
@echo '.clj-kondo/.cache' >> $@
@echo '' >> $@
@echo '# Calva VS Code Extension' >> $@
@echo '.calva/output-window/output.calva-repl' >> $@
@echo '' >> $@
@echo '# Metaclj tempfiles' >> $@
@echo '.antqtool.lastupdated' >> $@
@echo '.enrich-classpath-repl' >> $@
install-gitignore: .gitignore ## Install a meaningful .gitignore file
@echo ".gitignore added/exists in the project"
As part of CI/CD, I want automated linter-formatter checks, tests to run and builds to happen. Here we create Makefile targets to help us with this.
This section runs three checks:
- Tagref (See: <section explaining tagref>)
- Clj-Kondo (See: The clj-kondo section of the Makefile)
- Zprint (See: The zprint section of the Makefile)
Run the command -=make check=
check-tagref:
tagref
check-cljkondo:
clj-kondo --lint .
check-zprint:
zprint -c $(CLOJURE_SOURCES)
check: check-tagref check-cljkondo check-zprint ## Check that the code is well linted and well formatted
@echo "All checks passed!"
I use polylith
as my goto Clojure framework. The testing commands in
my Makefile run the appropriate polylith commands. I’m going to add a
non-polylith based testing target as well, in the near future.
The target test-coverage
uses Clofidence for coverage tracking (See: <clofidence installation instructions>)
Run the command make test
test-all:
clojure -M:poly test :all
test-coverage:
clojure -X:dev:test:clofidence
test: ## Run Poly tests for the code
clojure -M:poly test
I use antq
for managing dependencies. This target installs and runs
antq, which upgrades all the libraries in the current project.
Run the command make upgrade-libs
install-antq:
@if [ -f .antqtool.lastupdated ] && find .antqtool.lastupdated -mtime +15 -print | grep -q .; then \
echo "Updating antq tool to the latest version..."; \
clojure -Ttools install-latest :lib com.github.liquidz/antq :as antq; \
touch .antqtool.lastupdated; \
else \
echo "Skipping antq tool update..."; \
fi
.antqtool.lastupdated:
touch .antqtool.lastupdated
upgrade-libs: .antqtool.lastupdated install-antq ## Install all the deps to their latest versions
clojure -Tantq outdated :check-clojure-tools true :upgrade true
<TBD>
build: check ## Build the deployment artifact
@echo "Run deps-new build commands here!"
<TBD>
deploy: build ## Deploy the current code to production
@echo "Run fly.io deployment commands here!"
<TBD>
clean-projects:
rm -rf projects/*/target/public
clean: clean-projects ## Delete any existing artifacts