From cf1eb9089e6984c338cf3aceae46dbff50bb5d2d Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Tue, 21 Nov 2017 14:32:29 +0100 Subject: [PATCH 1/7] adding jar offset rewriting and capability to use custom preambles --- README.markdown | 41 +++++++++++++++++++++++++++++++++++++++++ project.clj | 6 ++++-- src/leiningen/bin.clj | 24 +++++++++++++++++++----- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/README.markdown b/README.markdown index 9f257dd..dead783 100644 --- a/README.markdown +++ b/README.markdown @@ -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. @@ -29,6 +32,44 @@ 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 to the beginning of your uber jar. Assuming you rewrite all the offsets in the zip file (or jar file in this case) meta data, the resulting jar file is still considered valid by the [zip file specification](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) and as a consequence by any sane zip implementation including programs like unzip and perhaps more significantly by java itself. + +So we add a snippet of text to the beginning of the uber jar, rewrite the zip meta data offsets and then make the uber jar executable. Now when you try to directly execute the uber jar, the operating system will try to run the prelude snippet of text we added to the file. If this prelude 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 jar file without the need to invoke `java -jar uber.jar`. + +So to get this working we need to write a "script" which works on both windows and *nix. This is a science unto itself, but suffice to say that it is possible. The default sample script inserted 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 + +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. + ## License Copyright (C) 2012 Anthony Grimes, Justin Balthrop diff --git a/project.clj b/project.clj index 7f111c9..ea87ee5 100644 --- a/project.clj +++ b/project.clj @@ -1,7 +1,9 @@ -(defproject lein-bin "0.3.4" +(defproject lein-bin "0.3.5" :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.1" :exclusions [org.clojure/clojure]] + [org.clojure/core.incubator "0.1.4"]] :eval-in-leiningen true) diff --git a/src/leiningen/bin.clj b/src/leiningen/bin.clj index 93faf09..98e371b 100644 --- a/src/leiningen/bin.clj +++ b/src/leiningen/bin.clj @@ -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 []}}] @@ -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) @@ -52,10 +63,13 @@ Add :main to your project.clj to specify the namespace that contains your (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)) + (condp + (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"))) From 533a40deadd166080128806b76e16575a7caabc6 Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 23 Nov 2017 12:02:31 +0100 Subject: [PATCH 2/7] rewording README.md, fixing typos --- README.markdown | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index dead783..20d248b 100644 --- a/README.markdown +++ b/README.markdown @@ -33,11 +33,13 @@ You can also supply a `:bin` key like so: * `: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 to the beginning of your uber jar. Assuming you rewrite all the offsets in the zip file (or jar file in this case) meta data, the resulting jar file is still considered valid by the [zip file specification](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) and as a consequence by any sane zip implementation including programs like unzip and perhaps more significantly by java itself. +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. -So we add a snippet of text to the beginning of the uber jar, rewrite the zip meta data offsets and then make the uber jar executable. Now when you try to directly execute the uber jar, the operating system will try to run the prelude snippet of text we added to the file. If this prelude 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 jar file without the need to invoke `java -jar uber.jar`. +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 to get this working we need to write a "script" which works on both windows and *nix. This is a science unto itself, but suffice to say that it is possible. The default sample script inserted looks as follows: +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 "$@" @@ -49,6 +51,8 @@ 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 @@ -68,7 +72,7 @@ java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -XX:-OmitStackTraceInFastThr goto :eof ``` -where `myapp.core` is the name of the main class of your program. +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 From 011ec56498d073f0b08604f7c642e2e7b17a82fc Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 23 Nov 2017 12:13:52 +0100 Subject: [PATCH 3/7] fixing typo, removing unneeded dependency --- project.clj | 4 ++-- src/leiningen/bin.clj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/project.clj b/project.clj index ea87ee5..4d05636 100644 --- a/project.clj +++ b/project.clj @@ -4,6 +4,6 @@ :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[me.raynes/fs "1.4.0"] - [clj-zip-meta/clj-zip-meta "0.1.1" :exclusions [org.clojure/clojure]] - [org.clojure/core.incubator "0.1.4"]] + [clj-zip-meta/clj-zip-meta "0.1.1" :exclusions [org.clojure/clojure]]] + ;;[org.clojure/core.incubator "0.1.4"]] :eval-in-leiningen true) diff --git a/src/leiningen/bin.clj b/src/leiningen/bin.clj index 98e371b..8804e7e 100644 --- a/src/leiningen/bin.clj +++ b/src/leiningen/bin.clj @@ -54,8 +54,8 @@ 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)))) @@ -63,9 +63,9 @@ Add :main to your project.clj to specify the namespace that contains your (println "Creating standalone executable:" (str binfile)) (io/make-parents binfile) (with-open [bin (FileOutputStream. binfile)] - (condp + (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)) + (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) From bfd5136a58ce70a6debb11ab00ad461d5d692069 Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 23 Nov 2017 12:14:32 +0100 Subject: [PATCH 4/7] removing commented out code --- project.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/project.clj b/project.clj index 4d05636..40e5ca5 100644 --- a/project.clj +++ b/project.clj @@ -5,5 +5,4 @@ :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[me.raynes/fs "1.4.0"] [clj-zip-meta/clj-zip-meta "0.1.1" :exclusions [org.clojure/clojure]]] - ;;[org.clojure/core.incubator "0.1.4"]] :eval-in-leiningen true) From b12c95eec93b4df756f1b1e3e25bf3451df1915a Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 23 Nov 2017 12:18:25 +0100 Subject: [PATCH 5/7] minor code formatting change --- src/leiningen/bin.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/leiningen/bin.clj b/src/leiningen/bin.clj index 8804e7e..71de8da 100644 --- a/src/leiningen/bin.clj +++ b/src/leiningen/bin.clj @@ -65,8 +65,8 @@ Add :main to your project.clj to specify the namespace that contains your (with-open [bin (FileOutputStream. binfile)] (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)) + (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) From 1d89b366e9806026cf97530c5167dcfb8e066fd6 Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 11 Jan 2018 15:58:22 +0100 Subject: [PATCH 6/7] fixing dependency on clj-zip-meta --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 40e5ca5..7304fea 100644 --- a/project.clj +++ b/project.clj @@ -4,5 +4,5 @@ :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[me.raynes/fs "1.4.0"] - [clj-zip-meta/clj-zip-meta "0.1.1" :exclusions [org.clojure/clojure]]] + [clj-zip-meta/clj-zip-meta "0.1.2-SNAPSHOT" :exclusions [org.clojure/clojure]]] :eval-in-leiningen true) From 9115d09f5b90e0f8761abe831c8685d033268278 Mon Sep 17 00:00:00 2001 From: Matias Bjarland Date: Thu, 11 Jan 2018 16:00:15 +0100 Subject: [PATCH 7/7] changing to snapshot version as dependencies require it --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 7304fea..877f442 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject lein-bin "0.3.5" +(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"