Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a command-line executable in Clojure

Tags:

java

ruby

clojure

I have written a program in Clojure and I'd like to execute it on the command-line without specifically invoking java on the command-line (such as java -jar). I want a single executable file, such as myprogram, that accepts any arguments and runs my program. Here are a couple things that might make this easier:

  1. It's OK to assume Java is installed and that java is in the path.
  2. Although a solution that works on Windows would be a great plus, you can assume that this is all being done on a UNIX-like operating system such as Mac OS X or Ubuntu.
  3. It's OK to invoke Java in a script of some kind.
  4. It's OK to use some other language, such as Ruby, Python or Perl, which the user may or may not have installed. All-bash would be cool because I can assume people have that.
  5. It's OK if I have to use some kind of tool to build a binary that will execute, but I am not looking for a .app or .exe file that expects to operate with a GUI interface (so for example, Oracle's appbundler is not what I'm looking for here).

I have gotten pretty far down this path with one approach, but I have to wonder if there's a better way.

FOR REFERENCE ONLY: What I've Tried Already

I'll describe my approach below, but any answers don't need to follow this approach at all.

What I have done is create a lein plugin called makescript that generates an uberjar, base64 encodes it, and places it inside a Ruby script in a so-called heredoc variable, like this:

# ...ruby script...
BASE64_JAR = <<-JAR_BOUNDARY
# [...base64 encoded file here...]
JAR_BOUNDARY

You should then be able to run the ruby script. It will take the BASE64_JAR variable, unencode it, place it in a temporary file, and execute it by invoking java -jar <filename>.

The problem I'm having with this approach is that Ruby's base64 library and Clojure's clojure.data.codec.base64 libraries seem to be producing different strings to represent the jar, and a string encoded by Clojure does not decode to the original file if I use Ruby. This may have something to do with the encoding of the strings themselves (UTF-8-related? maybe) between the two languages. Here are repl/irb sessions that illustrate the disconnect:

repl=> (def jar-contents (slurp "../target/myproj-0.1.0-SNAPSHOT-002-standalone.jar"))
repl=> (count jar-contents) ;; => 9433328
repl=> (def a-str (String. (clojure.data.codec.base64/encode (.getBytes jar-contents)) "UTF-8"))
repl=> (count a-str)        ;; => 23265576

irb> f = File.open("target/pwgen-0.1.0-SNAPSHOT-002-standalone.jar", "r").read()
irb> p f.length   # => 9657639
irb> b = Base64.encode64(f)
irb> p b.length   # => 13564973

Note the raw sizes are close but not the same, but the encoded versions are much different.

Although this is puzzling and I'd like to know why this happens, I think I can bypass the problem by having makescript just generate the uberjar and pass the path to another Ruby script, which then will base64 encode (and later decode, also using Ruby) the standalone JAR. The question still stands: is there a better, simpler way? Am I missing something obvious, or is this really as hard as it seems?

like image 945
door_number_three Avatar asked Nov 01 '13 20:11

door_number_three


3 Answers

If you really want to do it, you can uuencode the jar file (or any other binary file) in a bash executable, see this for an example: Embed a Executable Binary in a shell script

like image 90
lbalazscs Avatar answered Oct 16 '22 13:10

lbalazscs


It sounds like you're just trying to bootstrap your app from a script using cleaner syntax. This might be most easily done like so:

create a new bash script called myprogram:

#!/usr/bin/bash
# pass whatever command line args you have down through the script
java -jar myjar.jar

give it execute permissions

chmod +x myprogram

run it

./myprogram (with whatever params)

If you want to get rid of the ./ you'll need to move things around so the script gets picked up by your PATH.

Keep in mind that you're not creating a platform specific binary executable. Doing so would pretty much defeat the purpose of using the jvm in the first place. You'd just be invoking it through an extra layer of indirection.

like image 2
yamafontes Avatar answered Oct 16 '22 13:10

yamafontes


Now that GraalVM has matured a little bit, you may want to consider compiling your application to a binary executable.

There's a good tutorial of how to accomplish that here.

like image 2
piercebot Avatar answered Oct 16 '22 14:10

piercebot