Search code examples
schemeracketraco

Racket/Scheme compile to single binary, no dependencies? FFI and static linking


Say I'm building an app in Racket.

And say eventually I want to compile that app as a single binary file that could be distributed to users, without them having Racket or any other software libs installed. I believe this is possible, yes?

Say in that app I want to use the snappy package https://docs.racket-lang.org/snappy/ which is some FFI wrappers around a C++ lib.

I already ran into a minor problem. I did (require snappy) inside DrRacket and followed the prompts and got the package installed but I get the error:

../../Applications/Racket v7.7/collects/racket/private/kw.rkt:1349:57:
ffi-lib: couldn't open "libsnappy.1.dylib" (dlopen(libsnappy.1.dylib, 6): image not found)

I can assume from this that racket-snappy expects the files for libsnappy to be on the usual unix path, but I'm on macos and mine is installed via Homebrew somewhere else. I believe the answer to that problem is here https://stackoverflow.com/a/24287418/202168

My concern is: I do not want users of my app to have to install these libs via Homebrew and fiddle with paths etc.

I am a Racket noob and know basically nothing about the compiler toolchain or C/C++ for that matter either. But I believe what I need is when I compile my Racket project to be able to have raco exe(?) "statically link" the libsnappy that's on my system and roll everything into a single binary with no dependencies.

So my question is: is this possible? If so, is it straightforward (i.e. managed via raco tools)?

I'm imagining in the worst case I have to download all the dependencies and build them from source and build my Racket project also as a library and then have some kind of skeleton C project that pulls them all in into one thing. I hope not.

I will add also... if this is easier in other Schemes (Chicken? Chez? Gambit? Guile?) then I'd be interested to know too.

Update: I found this article with some year-old anecdata of someone attempting the same thing https://taoofmac.com/space/blog/2019/06/20/2310

Based on that, and Ryan's answer below, raco distribute looks promising and I really need to try this out for myself to confirm what works.

Update again: Here is another article again confirming raco distribute should put everything into a folder with no external deps https://defn.io/2020/06/28/racket-deployment/ and here is a pointer to the docs for how to build a .dmg image for MacOS: https://docs.racket-lang.org/raco/exe-dist.html#(part._.A.P.I_for_.Bundling_.Distributions)


Solution

  • There is a partial solution using a combination of raco distribute and define-runtime-path.

    Suppose you have a program that uses libzmq, which you know is installed on your build system at /usr/lib/x86_64-linux-gnu/libzmq.so.5. You can use define-runtime-path to create a reference to that file and tell raco distribute to copy it to the distribution directory. For example, suppose that "my-app.rkt" is the following:

    #lang racket/base
    (require racket/runtime-path)
    (define-runtime-path zmq "/usr/lib/x86_64-linux-gnu/libzmq.so.5")
    (printf "zmq = ~e\n" zmq)
    

    When you run the program using racket my-app.rkt, it prints

    zmq = <path:/usr/lib/x86_64-linux-gnu/libzmq.so.5>
    

    But when you run raco exe my-app.rkt and then raco distribute MyApp my-app, then the MyApp directory will contain a copy of libzmq.so.5:

    $ find MyApp/ -type f 
    MyApp/lib/plt/my-app/exts/ert/r0/libzmq.so.5
    MyApp/lib/plt/racket3m-7.7
    MyApp/bin/my-app
    

    and if you run ./MyApp/bin/my-app, it prints

    zmq = #<path:/PATH/TO/HERE/./MyApp/bin/../lib/plt/my-app/exts/ert/r0/libzmq.so.5>
    

    You can use (ffi-lib zmq) to load the shared library. Unfortunately, that directory is not in the search path that the application will use for loading shared libraries, so existing Racket libraries that just try to load (ffi-lib "libzmq" '("5")) won't find the application's copy.

    There is another way of using define-runtime-path specifically for shared libraries, and I thought it would solve that problem, but it doesn't seem to. That seems like a bug to me, so I'll file a bug report.

    Update: I have filed a bug report about the fact that define-runtime-path's shared library ('so) mode causes raco distribute to copy the shared library outside of the application's library search path.