~eightytwo/cljfmt-runner-graalvm

0828899facde6b3cc9cb848b993554f07896b99b — eightytwo 3 years ago
Initial commit
8 files changed, 245 insertions(+), 0 deletions(-)

A .gitignore
A .pre-commit-config.yaml
A Dockerfile
A README.md
A deps.edn
A docker-compose.yml
A graal_reflection.json
A src/core.clj
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)))))