-
Notifications
You must be signed in to change notification settings - Fork 38
Description
Hi guys.
Users have taken to the streets and are crying for a push-button way to:
- Declare Lisp and Java (Maven Central) dependencies side-by-side.
- Easily locate and load those Java dependencies into the REPL.
- Expose some
mainfunction from Lisp and bundle a single image / fatjar. - Do all this from the comfort of an ABCL repl session.
Much of what’s required for this seemingly already exists. This thread
elaborates prior art in the field, and lays out suggestions for future
development.
Prior Art
abcl-asdf
(asdf:defsystem #:abcl-telegram-bot
:description "Create telegram bots with ABCL"
:author "Alejandro Zamora Fonseca <ale2014.zamora@gmai.com>"
:license "MIT"
:version "0.0.1"
:serial t
:depends-on (:abcl-memory-compiler :alexandria)
:components ((:mvn "org.telegram/telegrambots-longpolling/8.3.0")
(:mvn "org.telegram/telegrambots-client/8.3.0")
(:file "package")
(:file "abcl-telegram-bot")))abcl-asdf extends ASDF to comprehend (:mvn ...) entries under :components. Upon
system load, it downloads these Java dependencies to ~/.m2/, the location of
which can’t be configured, even though the mvn CLI tool allows it:
mvn dependency:get \
-DgroupId=org.apache.commons \
-DartifactId=commons-text \
-Dversion=1.13.1 \
-Dmaven.repo.local=vendored/java/
Pros:
- This provides a first-class way to specify Java dependencies alongside Lisp ones.
- Downloading and classpath management is handled automatically.
Issues:
- Counter-intuitive is that these Java dependencies aren’t defined within
:depends-on. - If you haven’t already manually loaded
abcl-contribandabcl-asdfbefore
loading this system, ASDF doesn’t know what to do with the:mvnblocks and
throws an error. - There is seemingly no way to tell
abcl-asdfnot to download or not to manage
the classpath.
java:add-to-classpath
If you have a JAR somewhere on your machine, there is technically no need to go
through abcl-asdf; adding it manually to the classpath is sufficient:
(java:add-to-classpath "/home/colin/code/common-lisp/vend/vendored/java/commons-io/commons-io/2.16.1/commons-io-2.16.1.jar")
(#"toAbsolutePath" (#"current" 'org.apache.commons.io.file.PathUtils))
;; => #<sun.nio.fs.UnixPath /home/colin/code/common-lisp/ven.... {440C8006}>It’s simple enough for any tool trying to build up classpath entries to
recursively parse POM-file XML and call add-to-classpath as appropriate.
One thing I’m not sure about is whether calling add-to-classpath is still
necessary when we’ve already built a fatjar with jar cfm and a MANIFEST.MF. I
suspect not but haven’t confirmed.
asdf-jar
This provides a way to package all our Lisp sources (and dependencies!) into a
single JAR. Given:
(defpackage abcl-test
(:use :cl :arrow-macros)
(:export #:launch))
(in-package :abcl-test)
(require :java)
(defun launch ()
(->> (java:jstatic "now" "java.time.LocalDate")
(java:jcall "toString")
(format t "Date: ~a~%")))then by calling:
(require :abcl-contrib)
(require :asdf-jar)
(asdf-jar:package :abcl-test :out #p"./" :fasls t :verbose t)we get our JAR.
Issues:
- As mentioned in this issue, despite
:fasls t, subsequentasdf:load-system
calls don’t seem to respect the bundled.abclfasl files. - As mentiond in this issue, loading
asdf-jaris inconsistent and often fails.
Lisp-from-Java wrapping and jar cfm
In theory a universal entry-point runner could be written on the Java side to
run our program:
import org.armedbear.lisp.Interpreter;
public class Main
{
public static void main(String[] args) {
Interpreter i = Interpreter.createInstance();
i.eval("(require :asdf)");
i.eval("(require :abcl-contrib)");
i.eval("(require :asdf-jar)");
// Somehow refer to the child JAR within this JAR.
// i.eval("(asdf-jar:add-to-asdf \"/home/colin/code/common-lisp/abcl-test/abcl-test-all-0.0.1.jar\")");
i.eval("(asdf:load-system :abcl-test)");
i.eval("(abcl-test:launch)");
}
}We can compile this with:
javac -cp /usr/share/java/abcl.jar Main.java
And given a MANIFST.MF:
Manifest-Version: 1.0
Main-Class: Main
Class-Path: /usr/share/java/abcl.jar /usr/share/java/abcl-contrib.jar /home/colin/code/common-lisp/abcl-test/abcl-test-all-0.0.1.jar
The Class-Path here is probably wrong, but all this can be bundled together into
a single JAR with:
jar cfm app.jar MANIFEST.MF Main.class \
-C /usr/share/java/ abcl.jar \
-C /usr/share/java/ abcl-contrib.jar \
-C /home/colin/code/common-lisp/abcl-test/ abcl-test-all-0.0.1.jar
We now have a “fatjar”. Then calling java -jar app.jar actually runs! But it
currently always fails trying to load asdf-jar, as mentioned above. If we could
get past that, and work out the classpath issues, in theory we have all the
pieces (although not at all automated).
asdf:make
Briefly I will mention a few other solutions for inspiration.
asdf:make simply fails under ABCL. Given:
(defsystem "abcl-test"
:version "0.0.1"
:depends-on (:arrow-macros)
:components ((:module "src" :components ((:file "main"))))
:build-operation program-op
:build-pathname "abcl-test"
:entry-point "abcl-test:launch")We are told:
#<THREAD “interpreter” native {518FC5E}>: Debugger invoked on condition of type NOT-IMPLEMENTED-ERROR
Not (currently) implemented on ABCL: UIOP/IMAGE:DUMP-IMAGE dumping an executable
(ECL) asdf:make-build
ECL relies on a special implementation of asdf:make-build, otherwise normally
deprecated, for building its own binaries. This doesn’t require special entries
in .asd. Here is how vend is built:
(asdf:make-build :vend
:type :program
:move-here #p"./"
:epilogue-code '(vend:main))Dead simple. See here for a bigger example that sets linker flags for binding to
C (.so) dependencies.
(SBCL) sb-ext:save-lisp-and-die
SBCL doesn’t really differentiate between building an “image” and building an
executable; in the latter case there is simply a well-defined entrypoint, and
the blob can be run as-is from the terminal.
(sb-ext:save-lisp-and-die #p"aero-fighter"
:toplevel #'aero-fighter:launch
:executable t
:compression t)Note that you can’t call this from within Sly/Slime sessions, it typically must
be done in a fresh, standalone REPL (or build script).
(Clojure) Naive running
Assuming the user has Clojure installed, it’s enough to run any program (with
any mix of Clojure and Maven dependencies) by running commands like:
clojure -M -m some_namespace
Where there is a file in your project somewhere defining a namespace / module
that contains a -main:
(defn -main [& args]
(println "Hi!"))This “just works” with no extra config, and is especially convenient if you
don’t need to “distribute” to non-devs.
(Clojure) lein uberjar
The Clojure tool lein is also able to build uberjars. With a separate
project.clj:
(defproject my-project "0.1.0-SNAPSHOT"
:description "A Clojure project with Java dependencies"
:dependencies [[org.clojure/clojure "1.11.1"]
[com.fasterxml.jackson.core/jackson-databind "2.17.2"]] ; Example Java dependency
:main my-project.core
:aot [my-project.core])Then lein uberjar produces our fatjar that can be run with java -jar.
Recent Development
I have an experimental branch on vend that handles downloading through mvn to a
project-local locations, followed by POM parsing and classpath building. For the
rest of what’s needed, technically I can auto-generate a Main.java and a
Manifest file, and run various shell commands to produce the uberjar. However
we’re not quite there yet, and it would require an expansion of features on my
end to support the required configuration for ABCL projects.
The Future
Here are a few potential paths the future could take.
Status Quo: just run ABCL
Tell ABCL users to bundle a version of ABCL as a dependency of their production
app, where their “executable” becomes a wrapper around a call to abcl on their
code.
Pro: Nothing to do.
Con: Haven’t advanced the state of the art.
Implement uiop/image:dump-image
My personal bias is to not overrely on ASDF, especially where compilers have
their own first-class solution. That first-class solution will simply always be
better supported than asdf:make, etc. It’s fine to use ASDF to load systems, but
beyond that I don’t think it’s its responsibility to handle the production of
executables.
Fix asdf-jar + rely on external tooling
Perhaps it’s just a matter of making the Java wrapper shown above more
consistent, at which point vend or other tools can use all the existing
components to cobble together a runnable fatjar.
Pro: Potentially not too much work.
Con: No push-button solution from ABCL itself.
New core functionality / new contrib
Perhaps in tandem with an expansion to abcl-asdf (or a rewrite), ABCL can
provide some ext:fatjar function that:
- Compiles all FASLs and loads them into a JAR.
- Includes all specified Java deps from a customizable location (default to
~/.m2/). - Includes the ABCL jar itself (probably contrib too).
- Accepts a
:mainor:entrykeyword arg which accepts an entrypoint symbol. - Produces a
Main.classthat internally invokes the entrypoint (similar to what’s shown above).
So the function could look something like:
(ext:fatjar :my-project
:main #'my-project:launch
:java #p"/home/me/code/my-project/java-deps/")Thank you for taking the time to read and consider this. Please let me know your thoughts.