├── README.md └── nativity.clj /README.md: -------------------------------------------------------------------------------- 1 | # nativity 2 | 3 | A small script for turning babashka scripts into native binaries 4 | 5 | ## Requirements 6 | 7 | - JVM (because it depends on [clj.native-image](https://github.com/taylorwood/clj.native-image)) 8 | - babashka 9 | - GraalVM 10 | 11 | ## Usage 12 | 13 | From the nativity directory: `bb nativity.clj path/to/script` 14 | or even just `./nativity.clj path/to/script` 15 | 16 | ### Modes 17 | you can specify which mode to run on by using the `./nativity.clj path/to/script -m implicit` it will default to "untouched" 18 | 19 | ##### Untouched 20 | Untouched will leave the file completely alone, make a copy into the src directory and create the deps.edn file 21 | for example: 22 | 23 | ``` clojure 24 | #!/usr/bin/env bb 25 | (ns which 26 | (:require [clojure.java.io :as io]) 27 | (:gen-class)) 28 | 29 | (defn where [executable] 30 | (let [path (System/getenv "PATH") 31 | paths (.split path (System/getProperty "path.separator"))] 32 | (loop [paths paths] 33 | (when-first [p paths] 34 | (let [f (io/file p executable)] 35 | (if (and (.isFile f) 36 | (.canExecute f)) 37 | (.getCanonicalPath f) 38 | (recur (rest paths)))))))) 39 | 40 | (defn -main [& args] 41 | (when-first [executable args] 42 | (println (where executable)))) 43 | 44 | (when-not (System/getProperty "babashka.main") 45 | (when (find-ns 'babashka.classpath) (apply -main *command-line-args*))) 46 | ``` 47 | This can compile as is because it has: 48 | * Ns form with :gen-class and explicit requires (`(ns which 49 | (:require [clojure.java.io :as io]) 50 | (:gen-class))`) 51 | * an entry point (`defn -main [& args]`) 52 | * no in-line require forms (`(require ....)`) 53 | 54 | ##### Directed 55 | This mode cuts out parts of your script and puts it into a main function for you: 56 | for example assume you have this script: 57 | ``` clojure 58 | #!/usr/bin/env bb 59 | (ns which 60 | (:require [clojure.java.io :as io]) 61 | (:gen-class)) 62 | 63 | (defn where [executable] 64 | (let [path (System/getenv "PATH") 65 | paths (.split path (System/getProperty "path.separator"))] 66 | (loop [paths paths] 67 | (when-first [p paths] 68 | (let [f (io/file p executable)] 69 | (if (and (.isFile f) 70 | (.canExecute f)) 71 | (.getCanonicalPath f) 72 | (recur (rest paths)))))))) 73 | 74 | (when-first [executable *command-line-args*] 75 | (println (where executable))) 76 | ``` 77 | and you run `./nativity.clj which.clj -m directed -w 17-18` it will wrap lines 17 to 18 in the main entry point (in this case those lines would be the last 2 at the end), making a compilable script. 78 | 79 | ##### Implicit 80 | This mode is for when you made a script using the implicit requires that babashka has for one-liners. 81 | Be warned that this mode is very inefficient in so many ways. 82 | for example: 83 | ``` clojure 84 | #!/usr/bin/env bb 85 | (defn where [executable] 86 | (let [path (System/getenv "PATH") 87 | paths (.split path (System/getProperty "path.separator"))] 88 | (loop [paths paths] 89 | (when-first [p paths] 90 | (let [f (io/file p executable)] 91 | (if (and (.isFile f) 92 | (.canExecute f)) 93 | (.getCanonicalPath f) 94 | (recur (rest paths)))))))) 95 | (when-let [executable (first *command-line-args*)] 96 | (println (where executable))) 97 | ``` 98 | This one will require implicit mode and so it should be run `./nativity which.clj -m implicit -d io` (look below to understand the `-d` flag) 99 | 100 | ### Other Options 101 | ``` shellsession 102 | Short Long Default Description 103 | -d, --deps LIST [] Specify the dependency list for implicit mode (comma separated values, example: "io,str,json,edn") 104 | -n, --name FILENAME Override default file name for the binary (will default to the input file name ) 105 | -c, --clean Clean up the src and deps.edn files 106 | -m, --mode MODE untouched Choose the processing mode. Currently available: implicit, untouched 107 | --no-compile Don't run binary compilation step 108 | --namespace NAMESPACE-NAME If your main function is in a namespace different from your file name you can override with this. It will also use this to name the namespace in implicit mode 109 | -w, --wrap RANGE [0 0] Determine the lines you want to want to wrap with main (Only directed mode) 110 | ``` 111 | #### Require specific things for implicit mode 112 | You can specify which of the built in bb dependencies to require by using the `./nativity.clj path/to/script -d io,str,json`. 113 | If you use `-d all` it will include all of them. 114 | 115 | #### Rename binary 116 | if you want your binary to have a different name: 117 | `./nativity.clj path/to/script -n name-of-binary` 118 | 119 | #### Clean files 120 | With this flag you can make it automatically remove the generated src and deps.edn files: 121 | 122 | `./nativity.clj path/to/script -c` 123 | -------------------------------------------------------------------------------- /nativity.clj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns nativity 4 | (:require [clojure.string :refer [join split split-lines] :as str] 5 | [clojure.java.io :refer [make-parents delete-file]] 6 | [clojure.tools.cli :as cli]) 7 | (:import [java.lang ProcessBuilder$Redirect]) 8 | (:gen-class)) 9 | 10 | 11 | (defn dependency-parser [input-string] (map keyword (split input-string #","))) 12 | 13 | 14 | (defn slice-parser [input-string] 15 | (let [[start end] (map #(Integer/parseInt %) (split input-string #"-"))] 16 | [(dec start) end] 17 | )) 18 | 19 | 20 | (def cli-options 21 | [["-d" "--deps LIST" "Specify the dependency list for implicit mode ( comma seperated values, example: \"io,str,json,edn\")" 22 | :default [] 23 | :parse-fn dependency-parser] 24 | ["-n" "--name FILENAME" "Override default file name for the binary (will default to the input file name)"] 25 | ["-c" "--clean" "Clean up the src and deps.edn files"] 26 | ["-m" "--mode MODE" "Choose the processing mode. Currently available: implicit, untouched, directed" 27 | :default "untouched"] 28 | ["" "--no-compile" "Don't run binary compilation step" 29 | :default false] 30 | ["-j" "--namespace NAMESPACE" "If your main function is in a namespace different from your file name you can override with this. It will also use this to name the namespace in implicit mode"] 31 | ["-w" "--wrap RANGE" "Determine the lines you want to want to wrap with main (Only directed mode)" 32 | :default [0, 0] 33 | :parse-fn slice-parser] 34 | ]) 35 | 36 | ;shamelessly copied 37 | (defn- shell-command 38 | ([args] (shell-command args nil)) 39 | ([args {:keys [:throw?] 40 | :or {throw? true}}] 41 | (let [pb (let [pb (ProcessBuilder. ^java.util.List args)] 42 | (doto pb 43 | (.redirectInput ProcessBuilder$Redirect/INHERIT) 44 | (.redirectOutput ProcessBuilder$Redirect/INHERIT) 45 | (.redirectError ProcessBuilder$Redirect/INHERIT))) 46 | proc (.start pb) 47 | exit-code (.waitFor proc)] 48 | (when (and throw? 49 | (not (zero? exit-code))) 50 | (throw (ex-info "Got non-zero exit code" {:status exit-code}))) 51 | {:exit exit-code}))) 52 | 53 | 54 | (defn generate-deps-file [name-space native-image-name] 55 | {:deps {'cheshire {:mvn/version "5.10.0"} 56 | 'org.clojure/tools.cli {:mvn/version "1.0.194"} 57 | 'org.clojure/clojure {:mvn/version "1.10.2-alpha1"} 58 | 'org.clojure/core.async {:mvn/version "1.1.587"} 59 | 'com.cognitect/transit-clj {:mvn/version "1.0.324"} 60 | 'bencode {:mvn/version "0.2.5"} 61 | 'org.clojure/data.csv {:mvn/version "1.0.0"}} 62 | :aliases {:native-image 63 | {:main-opts [(str "-m clj.native-image " name-space) 64 | "--initialize-at-build-time " 65 | ;; optional native image name override 66 | (str "-H:Name=" native-image-name) 67 | "-H:+ReportExceptionStackTraces"] 68 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"] 69 | :extra-deps 70 | {'clj.native-image 71 | {:git/url "https://github.com/taylorwood/clj.native-image.git" 72 | :sha "7708e7fd4572459c81f6a6b8e44c96f41cdd92d4"}}}}}) 73 | 74 | 75 | (def dependency-map 76 | '{ :str [clojure.string :as str] 77 | :set [clojure.set :as set] 78 | :edn [clojure.edn :as edn] 79 | :shell [clojure.java.shell :as shell] 80 | :io [clojure.java.io :as io] 81 | :async [clojure.core.async :as async] 82 | :stacktrace [clojure.stacktrace] 83 | :test [clojure.test] 84 | :pprint [clojure.pprint :as pprint] 85 | :cli [clojure.tools.cli :as cli] 86 | :csv [clojure.data.csv :as csv] 87 | :json [cheshire.core :as json] 88 | :transit [cognitect.transit :as transit] 89 | :bencode [bencode.core :as bencode]}) 90 | 91 | 92 | (defn specific-dependencies [keylist] 93 | (cond 94 | (some #{:all} keylist) (conj (vals dependency-map) :require) 95 | (empty? keylist) nil 96 | :else (conj (vals (select-keys dependency-map keylist)) :require))) 97 | 98 | 99 | (def compile-to-native-command ["clj" "-A:native-image" "--no-fallback"] ) 100 | 101 | 102 | (defn generate-with [src-gen-fn {:keys [file-path name-space deps-file no-compile src-output-path] :as options}] 103 | (println "reading file") 104 | (let [main-file (slurp file-path)] 105 | (println "making directory") 106 | (make-parents src-output-path) 107 | 108 | (println "making modified script-file") 109 | ; (println (src-gen-fn main-file options)) 110 | (spit src-output-path (src-gen-fn main-file options)) 111 | 112 | (println "making deps file") 113 | (spit "deps.edn" (str deps-file)) 114 | 115 | (if-not (:no-compile options) 116 | (do 117 | (println "compiling native binary") 118 | (shell-command compile-to-native-command))))) 119 | 120 | 121 | (defn implicit-generation [main-file {:keys [name-space dependency-keys]}] 122 | (str 123 | (apply list (remove nil? 124 | (list 'ns (symbol name-space) 125 | '(:gen-class) 126 | (specific-dependencies dependency-keys)))) 127 | "(defn -main [& *command-line-args*] " main-file " )" 128 | )) 129 | 130 | 131 | (defn untouched-generation [main-file _] main-file) 132 | 133 | 134 | (defn directed-generation [main-file {wrap :wrap}] 135 | (let [lines-of-main-file (split-lines main-file) 136 | [start-line, stop-line] wrap 137 | inject-to-main (take (- stop-line start-line) (drop start-line lines-of-main-file)) 138 | initial-part (take start-line lines-of-main-file) 139 | final-part (drop stop-line lines-of-main-file) 140 | entry-point (flatten ["(defn -main [& *command-line-args*] " inject-to-main ")"])] 141 | (join "\n" (flatten [initial-part entry-point final-part]))) 142 | ) 143 | 144 | 145 | (defn -main [& *command-line-args*] 146 | (let [command-line-input (cli/parse-opts *command-line-args* cli-options) 147 | options (:options command-line-input) 148 | file-path (first (:arguments command-line-input)) 149 | file (-> file-path (split #"/") last) 150 | file-name (-> file (split #"\.") first) 151 | name-space (or (:namespace options) file-name) 152 | native-image-name (or (:name options) file-name) 153 | dependency-keys (:deps options) 154 | deps-file (generate-deps-file name-space native-image-name) 155 | src-output-path (str "src/" name-space ".clj") 156 | options (merge options {:name-space name-space :file-path file-path :deps-file deps-file :src-output-path src-output-path})] 157 | ; (println options) 158 | (case (:mode options) 159 | "implicit" (generate-with implicit-generation options) 160 | "untouched" (generate-with untouched-generation options) 161 | "directed" (generate-with directed-generation options) 162 | (generate-with untouched-generation options)) 163 | 164 | (if (:clean options) 165 | (do 166 | (println "removing source file") 167 | (delete-file src-output-path) 168 | (println "removing deps.edn file") 169 | (delete-file "deps.edn"))))) 170 | 171 | 172 | (when-not (System/getProperty "babashka.main") 173 | (when (find-ns 'babashka.classpath) (apply -main *command-line-args*))) 174 | --------------------------------------------------------------------------------