├── .gitignore ├── LICENSE ├── README.md ├── deps.edn └── src └── clj └── native_image.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | pom.xml 3 | pom.xml.asc 4 | *.log 5 | *.lein-* 6 | project.clj 7 | *.iml 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Taylor Wood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj.native-image 2 | 3 | Build [GraalVM](https://www.graalvm.org) native images using [Clojure Deps and CLI tools](https://clojure.org/guides/deps_and_cli). 4 | 5 | This should be useful for creating lightweight, native CLI executables using Clojure and `deps.edn`. 6 | See [clj.native-cli](https://github.com/taylorwood/clj.native-cli) for a starter project template. 7 | 8 | _This project depends on tools.deps.alpha and should be considered alpha itself._ 9 | 10 | ## Prerequisites 11 | 12 | - [Clojure CLI tools](https://clojure.org/guides/getting_started#_clojure_installer_and_cli_tools) 13 | - [GraalVM](https://www.graalvm.org/downloads/) 14 | 15 | **NOTE:** As of GraalVM 19.0.0, `native-image` is no longer included by default: 16 | > Native Image was extracted from the base GraalVM distribution. Currently it is available as an early adopter plugin. To install it, run: `gu install native-image`. After this additional step, the `native-image` executable will be in the `bin` directory, as for the previous releases. 17 | 18 | ``` 19 | ➜ $GRAALVM_HOME/bin/gu install native-image 20 | Downloading: Component catalog from www.graalvm.org 21 | Processing component archive: Native Image 22 | Downloading: Component native-image: Native Image from github.com 23 | Installing new component: Native Image licence files (org.graalvm.native-image, version 19.0.0) 24 | ``` 25 | 26 | ## Usage 27 | 28 | Assuming a project structure like this: 29 | ``` 30 | . 31 | ├── deps.edn 32 | └── src 33 | └── core.clj 34 | ``` 35 | 36 | In your `deps.edn` specify an alias with a dependency on `clj.native-image`: 37 | ```clojure 38 | {:aliases {:native-image 39 | {:main-opts ["-m" "clj.native-image" "core" 40 | "--initialize-at-build-time" 41 | ;; optional native image name override 42 | "-H:Name=core"] 43 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"] 44 | :extra-deps 45 | {clj.native-image/clj.native-image 46 | {:git/url "https://github.com/taylorwood/clj.native-image.git" 47 | :sha "7708e7fd4572459c81f6a6b8e44c96f41cdd92d4"}}}}} 48 | ``` 49 | 50 | Where `core.clj` is a class with `-main` entrypoint, for example: 51 | ```clojure 52 | (ns core 53 | (:gen-class)) 54 | (defn -main [& args] 55 | (println "Hello, World!")) 56 | ``` 57 | 58 | From your project directory, invoke `clojure` with the `native-image` alias, specifying the main namespace 59 | (`core` in example above): 60 | ``` 61 | ➜ clojure -A:native-image 62 | Loading core 63 | Compiling core 64 | Building native image 'core' with classpath 'classes:src:etc.' 65 | 66 | classlist: 1,944.26 ms 67 | 8<---------------------- 68 | [total]: 38,970.37 ms 69 | ``` 70 | Note: Either `GRAALVM_HOME` environment variable must be set, or GraalVM's `native-image` path must be passed as an argument, 71 | and any [additional arguments](https://www.graalvm.org/docs/reference-manual/aot-compilation/#image-generation-options) 72 | will be passed to `native-image` e.g.: 73 | ``` 74 | ➜ clojure -A:native-image --verbose 75 | ``` 76 | 77 | You can now execute the native image: 78 | ``` 79 | ➜ ./core 80 | Hello, World! 81 | ``` 82 | 83 | See [this Gist](https://gist.github.com/taylorwood/23d370f70b8b09dbf6d31cd4f27d31ff) for another example. 84 | 85 | ### Example Projects 86 | 87 | There are example deps.edn projects in the [lein-native-image](https://github.com/taylorwood/lein-native-image) repo: 88 | - [jdnsmith](https://github.com/taylorwood/lein-native-image/blob/master/examples/http-api) - CLI JSON-to-EDN transformer 89 | - [http-api](https://github.com/taylorwood/lein-native-image/blob/master/examples/http-api) - simple HTTP API server 90 | - [clojurl](https://github.com/taylorwood/clojurl) - cURL-like tool using clojure.spec, HTTPS, hiccup 91 | 92 | ## Caveats 93 | 94 | The `--no-server` flag is passed to `native-image` by default, to avoid creating orphaned build servers. 95 | 96 | Also see [caveats](https://github.com/taylorwood/lein-native-image#caveats) section of lein-native-image. 97 | 98 | ## References 99 | 100 | [GraalVM Native Image AOT Compilation](https://www.graalvm.org/docs/reference-manual/aot-compilation/) 101 | 102 | This project was inspired by [depstar](https://github.com/healthfinch/depstar). 103 | 104 | ## Contributing 105 | 106 | You'll need Clojure CLI tooling and GraalVM installed to test locally. 107 | Just change the source of the `clj.native-image` dependency to a `:local/root` instead of `:git/url`. 108 | 109 | Issues, PRs, and suggestions are welcome! 110 | 111 | ## License 112 | 113 | Copyright © 2018 Taylor Wood. 114 | 115 | Distributed under the MIT License. 116 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | org.clojure/tools.deps.alpha {:mvn/version "0.9.755"} 4 | org.clojure/tools.namespace {:mvn/version "1.0.0"}}} 5 | -------------------------------------------------------------------------------- /src/clj/native_image.clj: -------------------------------------------------------------------------------- 1 | (ns clj.native-image 2 | "Builds GraalVM native images from deps.edn projects." 3 | (:require [clojure.java.io :as io] 4 | [clojure.string :as cs] 5 | [clojure.tools.deps.alpha :as deps] 6 | [clojure.tools.namespace.find :refer [find-namespaces-in-dir]]) 7 | (:import (java.io BufferedReader File))) 8 | 9 | (defn native-image-classpath 10 | "Returns the current tools.deps classpath string, minus clj.native-image and plus *compile-path*." 11 | [] 12 | (as-> (System/getProperty "java.class.path") $ 13 | (cs/split $ (re-pattern (str File/pathSeparatorChar))) 14 | (remove #(cs/includes? "clj.native-image" %) $) ;; exclude ourselves 15 | (cons *compile-path* $) ;; prepend compile path for classes 16 | (cs/join File/pathSeparatorChar $))) 17 | 18 | (def windows? (cs/starts-with? (System/getProperty "os.name") "Windows")) 19 | 20 | (defn merged-deps 21 | "Merges install, user, local deps.edn maps left-to-right." 22 | [] 23 | (let [{:keys [install-edn user-edn project-edn]} (deps/find-edn-maps)] 24 | (deps/merge-edns [install-edn user-edn project-edn]))) 25 | 26 | (defn sh 27 | "Launches a process with optional args, returning exit code. 28 | Prints stdout & stderr." 29 | [bin & args] 30 | (let [arg-array ^"[Ljava.lang.String;" (into-array String (cons bin args)) 31 | process (-> (ProcessBuilder. arg-array) 32 | (.redirectErrorStream true) ;; TODO stream stderr to stderr 33 | (.start))] 34 | (with-open [out (io/reader (.getInputStream process))] 35 | (loop [] 36 | (when-let [line (.readLine ^BufferedReader out)] 37 | (println line) 38 | (recur)))) 39 | (.waitFor process))) 40 | 41 | (defn exec-native-image 42 | "Executes native-image (bin) with opts, specifying a classpath, 43 | main/entrypoint class, and destination path." 44 | [bin opts cp main] 45 | (let [cli-args (cond-> [] 46 | (seq opts) (into opts) 47 | cp (into ["-cp" cp]) 48 | main (conj main) 49 | ;; apparently native-image --no-server isn't currently supported on Windows 50 | (not windows?) (conj "--no-server"))] 51 | (apply sh bin cli-args))) 52 | 53 | (defn prep-compile-path [] 54 | (let [compile-path (io/file *compile-path*)] 55 | (doseq [file (-> compile-path (file-seq) (rest) (reverse))] 56 | (io/delete-file file)) 57 | (.mkdir compile-path))) 58 | 59 | (defn native-image-bin-path [] 60 | (let [graal-paths [(str (System/getenv "GRAALVM_HOME") "/bin") 61 | (System/getenv "GRAALVM_HOME")] 62 | paths (lazy-cat graal-paths (cs/split (System/getenv "PATH") (re-pattern (File/pathSeparator)))) 63 | filename (cond-> "native-image" windows? (str ".cmd"))] 64 | (first 65 | (for [path (distinct paths) 66 | :let [file (io/file path filename)] 67 | :when (.exists file)] 68 | (.getAbsolutePath file))))) 69 | 70 | (defn- munge-class-name [class-name] 71 | (cs/replace class-name "-" "_")) 72 | 73 | (defn build [main-ns opts] 74 | (let [[nat-img-path & nat-img-opts] 75 | (if (some-> (first opts) (io/file) (.exists)) ;; check first arg is file path 76 | opts 77 | (cons (native-image-bin-path) opts))] 78 | (when-not nat-img-path 79 | (binding [*out* *err*] 80 | (println "Could not find GraalVM's native-image!") 81 | (println "Please make sure that the environment variable $GRAALVM_HOME is set") 82 | (println "The native-image tool must also be installed ($GRAALVM_HOME/bin/gu install native-image)") 83 | (println "If you do not wish to set the GRAALVM_HOME environment variable,") 84 | (println "you may pass the path to native-image as the second argument to clj.native-image")) 85 | (System/exit 1)) 86 | (when-not (string? main-ns) 87 | (binding [*out* *err*] (println "Main namespace required e.g. \"script\" if main file is ./script.clj")) 88 | (System/exit 1)) 89 | 90 | (let [deps-map (merged-deps) 91 | namespaces (mapcat (comp find-namespaces-in-dir io/file) (:paths deps-map))] 92 | (prep-compile-path) 93 | (doseq [ns (distinct (cons main-ns namespaces))] 94 | (println "Compiling" ns) 95 | (compile (symbol ns))) 96 | 97 | (System/exit 98 | (exec-native-image 99 | nat-img-path 100 | nat-img-opts 101 | (native-image-classpath) 102 | (munge-class-name main-ns)))))) 103 | 104 | (defn -main [main-ns & args] 105 | (try 106 | (build main-ns args) 107 | (finally 108 | (shutdown-agents)))) 109 | --------------------------------------------------------------------------------