Skip to content

Tooling configuration for brand new Clojure projects

License

Notifications You must be signed in to change notification settings

unravel-team/metaclj

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tooling configuration for brand new Clojure projects

Quickstart

  1. Copy over the Makefile to your Clojure project.
  2. Run make help to see what you can do.

[ A short thread on what using Metaclj looks like: <link> (<date>) ]

Table of Contents

Introduction

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.

The PHONY section of the Makefile

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:

  1. 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)
  2. 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)
  3. 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)
  4. 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)
  5. Deploying our Clojure project to Production
    • build (See: <Creating Artifacts section>)
    • serve (See: <Running Artifacts section>)
    • deploy (See: <Deploying Artifacts section>)
  6. 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

The common variables section of the Makefile

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 as the shell for executing commands

# Set bash instead of sh for the @if [[ conditions,
# and use the usual safety flags:
SHELL = /bin/bash -Eeu

The default goal section of the Makefile

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

The help target to explain what you can do with the Makefile

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 characters
  • substr($$1, 1, length($$1)-1) takes the target name (first field) without the trailing colon
  • substr($$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)

The Main Opts and REPL targets for the Clojure CLI tool

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);

The Enrich section of the Makefile

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

The clj-kondo section of the Makefile

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

The zprint section of the Makefile

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."

The .gitignore section of the Makefile

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"

The CI/CD section of the Makefile

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.

The section to check that the code is good

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!"

The section to test that the code is working correctly

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

The section to upgrade Clojure libraries in the current project

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

The section to build the Clojure project

<TBD>

build: check    ## Build the deployment artifact
	@echo "Run deps-new build commands here!"

The section to deploy the Clojure project to production

<TBD>

deploy: build    ## Deploy the current code to production
	@echo "Run fly.io deployment commands here!"

The section to clean existing artifacts from the Clojure project

<TBD>

clean-projects:
	rm -rf projects/*/target/public

clean: clean-projects    ## Delete any existing artifacts

About

Tooling configuration for brand new Clojure projects

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published