This is just going to be a short entry where I’m going to demonstrate (and marvel about) Racket’s ability to turn scripts into small (native) executables.

But in case you’re unfamiliar with Racket: It’s a Lisp dialect (a Scheme derivative, to be precise) that comes with a lot of features such as, amongst others, the ability to be “compiled” to native executables.

To demonstrate this, I threw together a small “greet me” script:

#lang racket

(define usernames (current-command-line-arguments))

(define (print-usernames usernames-list)
  (for ([username usernames-list])
    (printf "Hello, ~a!\n" username)))

(if (< 0 (vector-length usernames))
    (print-usernames (vector->list usernames))
    (println "Hello!"))

Let’s call the script as a, well, Racket script:

$ racket greetings.rkt
"Hello!"
$ racket greetings.rkt Racketeer
Hello, Racketeer!
$ racket greetings.rkt Racketeer Lisper
Hello, Racketeer!
Hello, Lisper!

Looking good, let’s compile and test it:

$ raco exe -o greetings greetings.rkt 
$ ll
...
-rwxr-xr-x 1 restlessbytes restlessbytes  12M Nov  2 17:50 greetings*
-rw-rw-r-- 1 restlessbytes restlessbytes  290 Nov  2 17:47 greetings.rkt
$ ./greetings Dr4gonSlay0r73
Hello, Dr4gonSlay0r73!

Compilation was successful and it still works - hooray, let’s ship it!

But wait a minute! The title of this article is about creating small executables; however, 12M isn’t exactly small!

Can we do better? You might have guessed it but yes, we can!

A Racket program is defined by its use of “languages” which is a (fancy) way of controlling which modules / libraries / macros are available to us. In the script above we’re using #lang racket as our language but that one comes packed with a ton of modules and features, most of which we’re never going to use anyways.

Idealy, we would like a very bare-bones version of Racket that includes only a handful of modules, so that we’d be able to only load1 modules that we actually need. And, lo and behold, there’s indeed such a lightweight language called racket/base - who would’ve thought!

So let’s replace #lang racket with #lang racket/base and re-create our executable:

$ raco exe -o greetings greetings.rkt
$ ll
...
-rwxr-xr-x 1 restlessbytes restlessbytes 1,9M Nov  2 18:00 greetings*
-rw-rw-r-- 1 restlessbytes restlessbytes  295 Nov  2 18:00 greetings.rkt
...
$ ./greetings Dr4gonSlay0r73
Hello, Dr4gonSlay0r73!

Hah - our executable is now only 1.9M and still works as expected! But this isn’t the end of the line, we can reduce the size even further:

Racket’s build and packaging tool raco has an option called “demodularize” which, I quote: “produce[s] a whole program from a single module” - let’s use that:

$ raco demod greetings.rkt 
$ raco exe -o greetings greetings_rkt_merged.zo 
$ ll
...
-rwxr-xr-x 1 restlessbytes restlessbytes 204K Nov  2 18:08 greetings*
-rw-rw-r-- 1 restlessbytes restlessbytes  295 Nov  2 18:00 greetings.rkt
-rw-rw-r-- 1 restlessbytes restlessbytes 155K Nov  2 18:07 greetings_rkt_merged.zo
...
$ ./greetings Dr4gonSlay0r73
Hello, Dr4gonSlay0r73!

Holy-moly! We’re now down to 204K from our initial 12M - that’s roughly 60 times smaller!

Can we go even smaller? No, not really but, like I said: 204K is already impressively small! Just for comparison, a C program that does the same thing compiled with gcc2 produces executables with sizes between 15K and 25K - but that’s C we’re talking about!

Conclusion

Racket isn’t just a neat language suitable for writing small scripts - it’s also surprisingly good at turning those scripts into small and efficient native(!) executables.

Let me sum up the process here real quick:

  1. Use #lang racket/base as your main flavour and require additional modules as needed.
  2. Demodularize your script with raco demo before you create a native executable from it (that produces an intermediate comilation file called <your-script>_rkt_merged.zo)
  3. Create an executable from your *_merged.zo file

So the next time you’re cursing bash or are about to throw your computer out of the window because of some stupid C errors (NB: please don’t do it), consider using Racket - but be careful: you might like it!

Bonus: De-modularized scripts with racket instead of racket/base

“How big would the executable from our initial script be if we didn’t switch to racket/base?”

Good question! Let’s check that:

$ raco demod greetings.rkt
$ raco exe -o greetings greetings_rkt_merged.zo 
$ ll
...
-rwxr-xr-x 1 restlessbytes restlessbytes 2,2M Nov  2 18:17 greetings*
-rw-rw-r-- 1 restlessbytes restlessbytes  290 Nov  2 18:15 greetings.rkt
-rw-rw-r-- 1 restlessbytes restlessbytes 2,1M Nov  2 18:16 greetings_rkt_merged.zo

2.2M instead of 12M - not exactly small but also nothing to worry about.


  1. require in Racket speak.↩︎

  2. gcc version: 9.4.0 (on Ubuntu 20.04)↩︎