(λ (x) (create x) '(knowledge))

Did I Create Enough Static?

How I learned to stop worrying, and love the .exe · July 5, 2020

Site Update!

This post comes a few days late, I was trying to get it out on the 1st, but I've put off for far too long some QoL functions that are needed for the site. All of that to say, there is now an RSS feed for lambdacreate! It can be found here! That might not be useful for many people, but it's useful, and it forced me to divest my handler functions from my post data. I've also spent some time working on getting comments for the blog posts, but it'll likely require some changes to the docker composition to allow for an actual database back end, but that and code highlighting should be coming to the blog in the near future!

All that said, to the actual post!


Windows is such a pervasive environment, it's nearly impossible to escape from it. I've replaced every system in my home with Linux, my phone runs Linux, my servers run Linux. Heck even my computers and servers at work run Linux, yet I still find myself managing Windows endpoints. The endless drudgery of bodged down GUI heavy systems where you must click through a never ending series of window panes, just to change a screen time out setting, and lack of a decent shell interface, makes Windows one of my least favorite OS's. And that's not even introducing any "Stallman was right" into the mix. I can safely say I will likely never fully escape that, so I can only embrace it. In all of its unwieldy glory, I must accept that I will have to target Windows.

So since I've accepted that necessary evil, this is the part where we break out powershell and SCCM, and start talking about managing things the Microsoft way, right?


We can't have any of that. I would not could not write powershell scripts, not here, not there, not anywhere. I do not like it Microsoft. I've tried it, and I simply cannot find a way to enjoy it. So I must do the only thing I know how to.

Build a docker build system to compile exe binaries with mingw using Fennel's fancy new --compile-binary arg! As usual, I'm embracing the hard way and being stubborn for a few reasons.

  • I dislike powershell, a lot. (sorry, not sorry).
  • Windows shell sucks too, and giant batch scripts are nightmares.
  • I need a small simple bootstrapping system for work (one click, no mental overhead).

The short version is that my company needs a way to distribute a management engine to all of their Windows endpoints. Currently there's no easy/non-time consuming way to do that. I could write a big powershell script to do what I need, but I'd have to relearn a scripting language I frankly abhor. It would be much easier if I could distribute an executable, written in Fennel. Initially I was going to distribute a fennel --compile'd lua file, but the source would be readable by anyone, I'd have to install Lua on 99% of the endpoints. It just wasn't a great idea.

However! Fennel's --compile-binary function works more or less the same as lua-static does, slurping up all of the lua dependencies and building a self contained embedding lua runtime. Fennel is packaged this way for Alpine, so is toAPK! And we can do the exact same thing for Windows, without ever leaving the comfort of Linux. To do that you need to utilize the mingw compilers to compile your fennel program.

CC=i686-w64-mingw32-gcc fennel --compile-binary bootstrap.fnl bootstrap lua-5.3.5/src/liblua.a /usr/include/lua5.3

This handy little function works because fennel's internal compile calls the following as the compile option, while generate C code from the provide lua. So passing a CC arg just works as is.

compile-command [cc "-Os" ; optimize for size lua-c-path (table.concat native " ") static-lua rdynamic "-lm" (if ldl? "-ldl" "") "-o" (.. executable-name bin-extension) "-I" lua-include-dir (os.getenv "CC_OPTS")]]

Since I'm on Alpine and only a few mingw resources are available I had to resort to building on Debian. This isn't really a big deal, whatever tools get the job done, at the end of the day I wanted to build a container anyways so this could persist as part of my CI/CD pipeline. To that point, here's the basis of said mingw builder.

FROM debian:latest LABEL maintainer "Will Sinatra " RUN apt-get update ;\ apt-get upgrade ;\ apt-get install gcc-mingw-w64-base gcc-mingw-w64-i686 mingw-w64 mingw-w64-common mingw-w64-i686-dev mingw-w64-tools build-essential gcc bash sudo lua5.3 lua5.3-dev curl --yes;\ apt-get clean COPY build.sh /usr/local/bin/build.sh COPY fennel /usr/local/bin/fennel #Debian does not package Fennel COPY sudoers /etc/sudoers RUN useradd build ;\ chmod +x /usr/local/bin/build.sh ;\ chmod 440 /etc/sudoers ;\ mkdir /buildstuff VOLUME /buildstuff USER build WORKDIR /buildstuff CMD [ "/usr/local/bin/build.sh" ]

Once we've got the builder set with our necessary base we build out a build.sh file. There's likely a better way to do this, but I like my poor-man's makefile. Inside of the build context we need to apt install any additional dependencies such as devel libraries, cross compile our Lua version to get a Windows compliant Lua lib, and use said lib + mingw to build the .exe. It's honestly no more complex than compiling on Linux for Linux if you have the tools on hand.

#!/bin/bash curl https://www.lua.org/ftp/lua-5.3.5.tar.gz | tar xz make -C lua-5.3.5 mingw CC=i686-w64-mingw32-gcc CC=i686-w64-mingw32-gcc fennel --compile-binary bootstrap.fnl bootstrap lua-5.3.5/src/liblua.a /usr/include/lua5.3

Obviously that docker build system isn't great, it liters the directory you call it on with files. It is however flexible enough to allow you to define clean up steps, or even to compile custom modules as needed during the build process. Once the build ran the resultant .exe was less than a megabyte for a full salt stack bootstrapping system. And while I loathe powershell you can in fact call it from within lua with os.execute/io.popen and then pipe commands into it with :write, same goes for the DOS shell. All of which is fairly simple, but for the curious you can do nifty things like this:

(fn wincurl [url out-file] (local pshell (io.popen "powershell -command -" "w")) (pshell:write (.. "Invoke-WebRequest -Uri " url " -OutFile " out-file)) (pshell:close))

Obviously when you're working with lua, or even fennel, as a scripting language you can't escape using whatever shell the host provides. And if you're trying to target a Windows system, then your choices are powershell if you want anything complex and sane. At least wrapping it all inside of Fennel keeps it familiar for me, rather than trying to use a scripting language I already know I have no interest in. But hey, it isn't all that bad, at least I can embrace the .exe and distribute my salt stack in a nice neat package!