A => .gitignore +15 -0
@@ 1,15 @@
+# Build
+.cpcache
+classes
+target
+
+# Clojure development
+.clj-kondo
+.cpcache
+.nrepl-history
+.nrepl-port
+.rebel_readline_history
+
+# Editors
+.iml
+.idea
A => .pre-commit-config.yaml +20 -0
@@ 1,20 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.3.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: local
+ hooks:
+ - id: clj-kondo
+ name: Lint Clojure, ClojureScript, and EDN files
+ language: system
+ entry: clj-kondo --lint
+ files: src/
+ - repo: local
+ hooks:
+ - id: cljfmt
+ name: Format Clojure code
+ language: system
+ entry: cljfmt --check
+ files: src/
A => Dockerfile +17 -0
@@ 1,17 @@
+FROM oracle/graalvm-ce:19.3.2
+# FROM oracle/graalvm-ce:20.1.0
+# The binary produced by the 19.3.2 image can be compressed
+# slightly more via UPX.
+
+RUN gu install native-image
+
+RUN curl -O https://download.clojure.org/install/linux-install-1.10.1.536.sh && \
+ chmod +x linux-install-1.10.1.536.sh && \
+ ./linux-install-1.10.1.536.sh
+
+WORKDIR /build
+COPY deps.edn /build
+
+RUN clojure -e nil
+
+CMD ["clojure", "-A:native-image"]
A => README.md +111 -0
@@ 1,111 @@
+# cljfmt-runner-graalvm
+
+A pre-built binary for [cljfmt-runner](https://github.com/JamesLaverack/cljfmt-runner), created with [GraalVM](https://www.graalvm.org/).
+
+## Quick Start
+
+1. Download the [latest binary](https://git.sr.ht/~eightytwo/cljfmt-runner-graalvm/releases/latest) from the releases page.
+
+2. Move the downloaded binary to a directory that's on your path, e.g. `~/.local/bin`.
+
+3. Check the formatting of some Clojure code.
+ ```shell script
+ $ pwd
+ ~/projects/hello
+
+ $ cljfmt --check
+ Checked 6 file(s)
+ 1 file(s) were incorrectly formatted
+ --- a/src/clj/hello/core.clj
+ +++ b/src/clj/hello/core.clj
+ @@ -5,5 +5,5 @@
+ (str "Hello, " text "?"))
+
+ (defn -main
+ - ([] (println (say-hello "world")))
+ + ([] (println (say-hello "world")))
+ ([text] (println (say-hello text))))
+ ```
+
+4. Fix the formatting errors.
+ ```shell script
+ $ cljfmt --fix
+ Checked 6 file(s)
+ Fixing 1 file(s)
+ ```
+
+It is also possible to include additional directories for checking:
+```shell script
+$ cljfmt --check -- -d dev
+```
+
+## What's This About?
+
+There's a lot of nice Clojure tooling for developers, such as [`cljfmt`](https://github.com/weavejester/cljfmt), which formats Clojure code idiomatically. These tools are typically run via [`clj`](https://clojure.org/guides/deps_and_cli) (or [`lein`](https://github.com/technomancy/leiningen)) and can therefore take seconds to complete. This is because the Clojure code needs to be compiled to Java bytecode which is then executed in a Java virtual machine.
+
+This isn't a huge problem if you are only running `cljfmt` once or twice. However, if you are going to run `cljfmt` frequently, such as in a [pre-commit](https://pre-commit.com/) hook, then execution time becomes really important.
+
+## How to Create a Native Binary
+
+This project uses the following to create native binaries:
+* [clj.native-image](https://github.com/taylorwood/clj.native-image) - a Clojure program for building GraalVM native images using Clojure Deps and CLI tools.
+* [oracle/graalvm-ce](https://hub.docker.com/r/oracle/graalvm-ce) - A Docker image with GraalVM installed.
+
+1. Clone this repository.
+ ```shell script
+ $ git clone https://git.sr.ht/~eightytwo/cljfmt-runner-graalvm.git
+ ```
+
+2. Change to the project directory.
+ ```shell script
+ $ cd cljfmt-runner-graalvm
+ ```
+
+3. Build the Docker image.
+ ```shell script
+ $ docker-compose build
+ Building graalvm-builder
+
+ Step 1/7 : FROM oracle/graalvm-cd:20.1.0
+ ...
+ Successfully tagged clj-native-graalvm-builder:1.0
+ ```
+
+4. Build a native binary of `cljfmt-runner`.
+ ```shell script
+ $ docker-compose run --rm graalvm-builder
+ ```
+
+The native binary will be placed in the project directory and will be called `cljfmt`. Feel free to move this to a directory on your path so you can easily format Clojure code.
+
+## Reducing the Size of the Binary
+
+You might have noticed the binary size is almost 30MB! Using the [Ultimate Packer for eXecutables](https://github.com/upx/upx) (UPX) you can drastically reduce the size.
+
+1. Download the [latest release](https://github.com/upx/upx/releases/latest) of UPX.
+
+2. Run UPX on the `cljfmt` binary.
+```shell script
+$ ./upx cljfmt
+ Ultimate Packer for eXecutables
+ Copyright (C) 1996 - 2020
+UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020
+
+ File size Ratio Format Name
+ -------------------- ------ ----------- -----------
+ 30211888 -> 7122948 23.58% linux/amd64 cljfmt
+
+Packed 1 file.
+```
+
+The packed binary is now 7MB. That's still large, but a 75% reduction in size is certainly a nice improvement!
+
+## Credits
+
+* [cljfmt](https://github.com/weavejester/cljfmt): a great tool for developers that helps avoid formatting discussions on pull requests. This project was also a great resource for learning how to deal with command line arguments in Clojure.
+
+* [cljfmt-runner](https://github.com/JamesLaverack/cljfmt-runner): making it possible to use `cljfmt` in `tools.deps` projects.
+
+* [clj.native-image](https://github.com/taylorwood/clj.native-image): providing functionality to build GraalVM native images with Clojure Deps and CLI tools. This was also a valuable resource for [learning about GraalVM's reflection config](https://github.com/taylorwood/clj.native-image/issues/3#issuecomment-434137936) which was necessary to resolve a build error. Coincidentally, this discussion was related to [a fork of cljfmt-runner](https://github.com/aviflax/cljfmt-runner/tree/native-image#building-a-native-imageexecutable) for building a native executable.
+
+* [UPX project](https://github.com/upx/upx): making it possible to not have to lug around a 30MB executable.
A => deps.edn +18 -0
@@ 1,18 @@
+;; Clojure 1.9 is used due to the 'unbalanced monitors' error that
+;; results when running a GraalVM build on Clojure 1.10.
+{:deps {org.clojure/clojure {:mvn/version "1.9.0"}
+ com.jameslaverack/cljfmt-runner
+ {:git/url "https://github.com/JamesLaverack/cljfmt-runner"
+ :sha "6383fbb0bd22a21c0edf5b699425504d9f0a958a"}
+ clj.native-image
+ {:git/url "https://github.com/taylorwood/clj.native-image.git"
+ :sha "7708e7fd4572459c81f6a6b8e44c96f41cdd92d4"}}
+ :aliases {:native-image
+ {:main-opts ["-m clj.native-image core"
+ "--initialize-at-build-time"
+ "--no-fallback"
+ "--report-unsupported-elements-at-runtime"
+ "-H:+ReportExceptionStackTraces"
+ "-H:Name=cljfmt"
+ "-H:ReflectionConfigurationFiles=graal_reflection.json"]
+ :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}}}
A => docker-compose.yml +18 -0
@@ 1,18 @@
+version: '3.4'
+services:
+ graalvm-builder:
+ image: clj-native-graalvm-builder:1.0
+ build:
+ context: .
+ network: host
+ command: ["clojure", "-A:native-image"]
+ volumes:
+ - .:/build
+ network_mode: "host"
+ stdin_open: true
+ tty: true
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "200k"
+ max-file: "10"
A => graal_reflection.json +10 -0
@@ 1,10 @@
+[
+ {
+ "name": "java.io.File",
+ "allDeclaredConstructors": true,
+ "allPublicConstructors": true,
+ "allDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allPublicFields": true
+ }
+]
A => src/core.clj +36 -0
@@ 1,36 @@
+(ns core
+ (:require [cljfmt-runner.check :as fmt-check]
+ [cljfmt-runner.fix :as fmt-fix]
+ [clojure.string :as string]
+ [clojure.tools.cli :as cli])
+ (:gen-class))
+
+(defn- abort [& msg]
+ (binding [*out* *err*]
+ (when (seq msg)
+ (apply println msg))
+ (System/exit 1)))
+
+(def ^:private cli-options
+ [["-c" "--check" "Check for any formatting errors"]
+ ["-f" "--fix" "Fix any formatting errors"]
+ ["-h" "--help" "Print this help message and exit"]])
+
+(defn- parse-opts
+ [args]
+ (let [parsed-opts (cli/parse-opts args cli-options)]
+ (if (:errors parsed-opts)
+ (abort (:errors parsed-opts))
+ parsed-opts)))
+
+(defn -main
+ [& args]
+ (let [parsed-opts (parse-opts args)
+ options (:options parsed-opts)
+ arguments (string/join (:arguments parsed-opts))]
+ (if (:help options)
+ (do (println "cljfmt-runner [OPTIONS]")
+ (println (:summary parsed-opts)))
+ (cond
+ (:check options) (fmt-check/-main arguments)
+ (:fix options) (fmt-fix/-main arguments)))))