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 gcc
2 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:
- Use
#lang racket/base
as your main flavour andrequire
additional modules as needed. - 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
) - 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.