Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# lein-bin

[![Current Version](https://img.shields.io/clojars/v/lein-bin.svg)](https://clojars.org/lein-bin)
[![License](https://img.shields.io/badge/License-EPL%201.0-green.svg)](https://opensource.org/licenses/EPL-1.0)

A Leiningen plugin for producing standalone console executables that
work on OS X, Linux, and Windows.

Expand Down Expand Up @@ -29,6 +32,48 @@ You can also supply a `:bin` key like so:
* `:bin-path`: If specified, also copy the file into `bin-path`, which is presumably on your $PATH.
* `:bootclasspath`: Supply the uberjar to java via `-Xbootclasspath/a` instead of `-jar`. Sometimes this can speed up execution, but may not work with all classloaders.

## Advanced Usage
Under the hood this plugin adds a snippet of text (the "preamble") to the beginning of your uber jar. Assuming you rewrite some internal offsets in the jar file, the resulting jar is still considered valid by the [zip file specification](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) and as a consequence, by any sane jar/zip implementation including programs like unzip and perhaps more significantly by java itself.

Let's repeat that to make sure we grok the idea: it's possible to add random data to the beginning of a jar file and still have the jar be valid in the eyes of java and other tools.

So we add a snippet of text to the beginning of the uber jar, rewrite the offsets within the uber jar, and then make the uber jar executable. Now when you try to directly execute the uber jar (as you would a normal executable file), the operating system will try to run the preamble script we added to the beginning for the jar file. If this preamble snippet is written in a way such that it is considered a valid shell/bat script on both windows and linux/osx, then we have just created ourselves a true executable and portable jar file without the need to invoke `java -jar uber.jar`.

To get this working we need to write a "script" which works on both windows and linux/osx. This is a science unto itself, but suffice to say that it is possible. The default hard coded script used by this plugin looks as follows:

```
:;exec java %s -jar $0 "$@"
@echo off\r\njava %s -jar %%1 \"%%~f0\" %%*\r\ngoto :eof
```

where:

* on windows machines only the second line is executed and the first one is seen as a comment
* on *nix machines the first line is executed and since the `exec` command relinquishes control from the current process and replaces it with the `java -jar ...` one, the second line is never executed

What this script does is it executes `java -jar` on itself, or rather, on the jar file the script is contained in.

For advanced usage or to take advantage of startup accelerators such as [drip](https://github.com/ninjudd/drip), you can include a custom preamble script in place of the above snippet by using the `:preamble-script` key like so:

```clojure
:bin {:name "runme"
:bin-path "~/bin"
:preamble-script "custom-preample.txt"}
```

an example preamble for using drip if drip exists and otherwise fall back to java could look as follows:

```bash
:; hash drip >/dev/null 2>&1 # make sure the command call on the next line has an up to date worldview
:;if command -v drip >/dev/null 2>&1 ; then CMD=drip; else CMD=java; fi
:;$CMD -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:-OmitStackTraceInFastThrow -client -Xbootclasspath/a:"$0" myapp.core "$@"; exit $?
@echo off
java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:-OmitStackTraceInFastThrow -client -Xbootclasspath/a:%1 myapp.core "%~f0" %*
goto :eof
```

where `myapp.core` is the name of the main class of your program. The above only works for drip on linux/osx, mostly because at the time of writing, I did not have a windows machine to test the script on.

## License

Copyright (C) 2012 Anthony Grimes, Justin Balthrop
Expand Down
5 changes: 3 additions & 2 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
(defproject lein-bin "0.3.4"
(defproject lein-bin "0.3.6-SNAPSHOT"
:description "A leiningen plugin for generating standalone console executables for your project."
:url "https://github.com/Raynes/lein-bin"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[me.raynes/fs "1.4.0"]]
:dependencies [[me.raynes/fs "1.4.0"]
[clj-zip-meta/clj-zip-meta "0.1.2-SNAPSHOT" :exclusions [org.clojure/clojure]]]
:eval-in-leiningen true)
28 changes: 21 additions & 7 deletions src/leiningen/bin.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[leiningen.jar :refer [get-jar-filename]]
[leiningen.uberjar :refer [uberjar]]
[me.raynes.fs :as fs]
[clojure.java.io :as io])
[clojure.java.io :as io]
[clj-zip-meta.core :as zm])
(:import java.io.FileOutputStream))

(defn- jvm-options [{:keys [jvm-opts name version] :or {jvm-opts []}}]
Expand All @@ -29,6 +30,16 @@
(defn write-boot-preamble! [out flags main]
(.write out (.getBytes (boot-preamble flags main))))

(defn write-custom-preamble! [project out flags]
(let [path (get-in project [:bin :preamble-script])
file (clojure.java.io/as-file path)]
(if (.exists file)
(do
(println "> writing custom preamble...")
(io/copy file out))
(println "> ERROR: custom preamble file" path "not found!"))))


(defn ^:private copy-bin [project binfile]
(when-let [bin-path (get-in project [:bin :bin-path])]
(let [bin-path (fs/expand-home bin-path)
Expand All @@ -43,19 +54,22 @@ Add :main to your project.clj to specify the namespace that contains your
-main function."
[project]
(if (:main project)
(let [opts (jvm-options project)
target (fs/file (:target-path project))
(let [opts (jvm-options project)
target (fs/file (:target-path project))
binfile (fs/file target
(or (get-in project [:bin :name])
(str (:name project) "-" (:version project))))
jarfile (uberjar project)]
(println "Creating standalone executable:" (str binfile))
(io/make-parents binfile)
(with-open [bin (FileOutputStream. binfile)]
(if (get-in project [:bin :bootclasspath])
(write-boot-preamble! bin opts (:main project))
(write-jar-preamble! bin opts))
(cond
(get-in project [:bin :preamble-script]) (write-custom-preamble! project bin opts)
(get-in project [:bin :bootclasspath]) (write-boot-preamble! bin opts (:main project))
:else (write-jar-preamble! bin opts))
(io/copy (fs/file jarfile) bin))
(fs/chmod "+x" binfile)
(copy-bin project binfile))
(copy-bin project binfile)
(println "> re-aligning zip offsets...")
(zm/repair-zip-with-preamble-bytes binfile))
(println "Cannot create bin without :main namespace in project.clj")))