Reading List
The most recent articles from a list of feeds I subscribe to.
gokrazy is really cool
Making NixOS modules for fun and (hopefully) profit
Making NixOS modules for fun and (hopefully) profit
Good morning everyone! Say this happens to you: you've been coding nonstop on something you want to share with your friends and it works on your MacBook. You want it to stay up when your MacBook goes to sleep or you get on a plane or something, and all you have to do is the easy task of putting it into production. It's just simple, right?
Just add a Dockerfile, they say! So you do that and then you have a Docker image that you can push to your target machine and then you find out that you can't just push it from machine to machine, you have to push it to a registry.
So you make an account on the Docker hub only to find out that their rate limits are very aggressive so you have to move to something like GHCR and aggressively cache all your images there so you don't run afoul of the comically small Docker Hub rate limits which will block your attempts to deploy it to your cloud provider of choice.
So you do that and you pull this on a VM running on someone else's computer, and then you need to figure out the other fun part:
You need to configure nginx. Of course it uses its own bespoke configuration language that no other program on the planet uses (this is an unfortunately common pattern in our industry) so it's even more googling for that. But then you realize you need to configure the real final boss of the internet:
DNS. It's never DNS until it's always DNS. So you install the artist formerly known as Terraform, lego, and provision your DNS and HTTPS certificates (because of course nginx doesn't just natively have this support in anno dominium two thousand and twenty three like any sensible HTTP reverse proxy should). And then you're finally done. It's taken you an hour to hack up the service and a whole 8 hours to research and implement everything to deploy it. This is madness. Why do we have to put up with this?
The koolaid runs deep in the cloud too, if you're not careful you'll end up accidentally making an entire event sourcing platform with an unrealistic amount of complexity to manage something as simple as a tamagochi. You're just trying to make an HTTP service show up on the internet, you don't need to know what an ALB, EKS, ECS, IAM, or PFA is.
Of course, complain about this online and a certain tangerine community funded by big YAML will decry that you should use Kubernetes to simplify all this down to "simple" and "easy to understand" things, conveniently ignoring that they use a string templating language for structured data.
There has got to be something simpler, right? What if you didn't have to deal with nearly any of that? What if you could just push and run your binary on a home server and then access it? No dealing with the cloud. No dealing with security groups or IAM or DNS or HTTPS or any of the slings and arrows of outrageous investment. What if you could just describe the state of the system you want instead of going three layers deep into a side of devops hell that you will never return from unscathed?
This is the real value of NixOS. Today I'm going to show you how to turn an arbitrary Go program into a NixOS service and then I'll expose it to the world thanks to Tailscale Funnel. This means you can link it to your group chat of friends and restore balance to the force. Or whatever it is you zoomers do in group chats.
All that said, let me introduce myself. I'm Xe Iaso, I write that one blog that you keep finding when you google Nix and NixOS stuff. I'm a writer, gamer, philosopher of chaos magick, and have a critical blogging addiction.
Today I'm going to cover a few core things so you can make your own NixOS modules: I'm going to cover what a NixOS module is and why you should care, the parts of one, how to make your own, and then I'm going to tempt the demo gods by doing a live deployment to a virtual machine on my MacBook.
Before I get started though, let's get some exercise in. Raise your hand if this is your first exposure to Nix and/or NixOS.
(About half the room raises their hands)
Alright, thanks.
Raise your hand if you've ever used it before.
(The other half of the room raises their hands)
That's about what I expected.
How about if you have it installed on a server at home?
(The same people raise their hands)
Okay, okay, I see.
How about if you're one of the lucky few where your employer uses it in production?
(Only a few of those people raise their hands)
Oh, wow, okay. That makes sense. You can lower your hands now.
Just so we're on the same page, Nix is a package manager that lets you declare all of the inputs for a program and get the same output from the build process.
func Build(
inputs []Package,
steps BuildInfo,
) (
output Package,
err Error,
)
One of the main ways that Nix stands out from the crowd is the idea that package builds are functions. They effectively take in inputs, use them against some instructions, and then either return a package or the build fails due to an error. Because there were no other options at the time, Nix uses its own programming language also named Nix to define packages.
Remember, Nix was the result of lamentations at the state of software and this was the result.
To help you understand, I've put up this helpful diagram. It uses rainbow comic sans so you know it's legit. Nixpkgs the standard library uses Nix the language, but it is not NixOS the operating system. I like to think about NixOS like this:
NixOS is the natural consequence of using Nix to build Linux systems. You can think about NixOS as a bunch of prebaked snippets of configuration that you can combine info a running system that does what you want. Each of those snippets is called a module. Nixpkgs (the standard library in Nix land) ships with a bunch of them that do things from compiling systemd to configuring Tailscale for you. Here's a simple NixOS module from my homelab:
{ config, pkgs, ... }:
{
services.prometheus.exporters.node.enable = true;
}
A NixOS module is a function that takes the current state of the world and returns things to change in it. The module I'm showing here is from my homelab, specifically the part that enables the prometheus node exporter so that I can report when machines suddenly go offline or their hard drives are going bad. This is a very simple example. When you import it, it always takes effect. There's no flags to enable it or disable it. This is fine for my usecase however, because I want my homelab cluster to always be monitored. Things get a lot more fun when you add options into the mix:
{ lib, config, pkgs, ... }:
with lib;
let cfg = config.within.vim;
in {
options.within.vim.enable = mkEnableOption "Enables Within's vim config";
config = mkIf cfg.enable {
home.packages = [ pkgs.vim ];
home.file.".vimrc".source = ./vimrc;
};
}
Compare it to this module, this is a dot file management module that sets up my vimrc on my machines. I have the option within.vim.enable
, and if that is set to true, the vim configuration is dropped in place. If it's not set to true, it won't put the vim configuration in the system. NixOS modules have options and configuration outputs. Options let you customize the configuration to meet your spacebar heating needs.
{ ... }:
{
imports = [ ./vim ];
within.vim.enable = true;
}
To use this, you'd add the path to the file to an imports
output of the module, then add a within.vim.enable = true
statement inside your home-manager configuration.
The state of the world is the input, and any new changes are the outputs. This lets you build a Linux system exactly the way you want to. It's just a new and interesting way to write a function.
services.nginx.virtualHosts."xeiaso.net" = {
locations."/" = {
proxyPass = "http://unix:${toString cfg.sockPath}";
proxyWebsockets = true;
};
forceSSL = cfg.useACME;
useACMEHost = "xeiaso.net";
extraConfig = ''
access_log /var/log/nginx/xesite.access.log;
'';
};
Above all else: you can configure programs like nginx directly in your NixOS configuration without having to learn how to write nginx config, saving you from having to configure every single program on your system in its own bespoke ways.
(Pause)

Of course, things become a lot more fun when you can build your own NixOS modules that have your own programs running on your own machines. Let's do that with an example program that shows quotes from the legendary British science fiction author Douglas Adams.
This is what the end result will look like. It'll be the quotes on a screen that refreshes every time you press F5. This will let you spread the undeniable wisdom of the late and great Douglas Adams, author of the five part trilogy The Hitchhiker's Guide to the Galaxy.
So overall, the infrastructure setup will look like this: my MacBook and the VM are both connected to each other with Tailscale. When I enable Tailscale Funnel, the VM is going to have its HTTPS port opened up to the public internet so that you can visit this service running on a VM, on my MacBook, on conference Wi-Fi. Let's hope the demo gods are in our favour!
By the way, Tailscale's gonna take care of the DNS and Let's Encrypt problems for us. No having to figure that out in the conference!
{
description = "Douglas Adams quotes";
# Nixpkgs / NixOS version to use.
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }:
let # ...
in
{
packages = ...;
nixosModules.default = ...;
devShell = ...;
checks.x86_64-linux = ...;
}
}
I've opened a VS Code session with an "empty" flake configuration. Nix flakes let you create a set of packages, development environments, NixOS modules, and even end to end integration tests. To start, this flake will import nixpkgs:
# Nixpkgs / NixOS version to use.
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
And then it declares a devShell for all of the developer dependencies:
devShell = forAllSystems (system:
let pkgs = nixpkgsFor.${system};
in with pkgs;
mkShell {
buildInputs =
[ go_1_21 gotools go-tools gopls nixpkgs-fmt nodejs yarn ];
});
This is relevant because I want you to imagine a world where your compilers aren't in your shell by default. This devShell configuration adds the packages relevant to the project to the development environment. This is a Go project with some CSS managed by Tailwind, so it's got the Go compiler, some Go development tools, npm, and yarn. A pretty normal set of things really.
To enter the development environment, run nix develop
. I'm going to run yarn start:css
in another shell to rebuild when any of the template files change.
Now that we have that, let's see how we would add a package to the flake. One of the flake output kinds is Nix packages, so we make an output named packages and paste in some boilerplate to get a Go package working:
packages = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.buildGo121Module {
pname = "douglas-adams-quotes";
inherit version;
src = ./.;
vendorSha256 = null;
};
});
The package would look something like this. This forAllSystems / nixpkgsFor hack is something you can work around with flake-utils, but for right now I'm doing everything manually. This is basically a bunch of predefined copies of nixpkgs for all the supported architectures, much like there's a devshell for every supported architecture. Either way, we get a Go module built into a package, and we define the dependency hash as null because this is only using the standard library. It's called default in the flake because it's best practice to name your package that.
Just to test it, you can run nix build
to build the default package in that flake.nix file:
nix build
Perfect! It builds! The binary is in ./result/bin/ and we can run it wherever we want.
$ ./result/bin/douglas-adams-quotes --help
Usage of ./result/bin/douglas-adams-quotes:
-addr string
listen address (default ":8080")
-slog-level string
log level (default "INFO")
If it didn't work we wouldn't get this far!
nixosModules.default = { config, lib, pkgs, ... }:
with lib;
let
cfg = config.xe.services.douglas-adams-quotes;
in
{
options.xe.services.douglas-adams-quotes = {
enable = mkEnableOption "Enable the Douglas Adams quotes service";
logLevel = mkOption {
type = with types; enum [ "DEBUG" "INFO" "ERROR" ];
example = "DEBUG";
default = "INFO";
description = "log level for this application";
};
port = mkOption {
type = types.port;
default = 8080;
description = "port to listen on";
};
package = mkOption {
type = types.package;
default = self.packages.${pkgs.system}.default;
description = "package to use for this service (defaults to the one in the flake)";
};
};
config = mkIf cfg.enable {
systemd.services.douglas-adams-quotes = {
description = "Douglas Adams quotes";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = "yes";
ExecStart = "${cfg.package}/bin/douglas-adams-quotes --slog-level=${cfg.logLevel} --addr=:${toString cfg.port}";
Restart = "on-failure";
RestartSec = "5s";
};
};
};
};
We defined the devShell to build the program development. We defined the package to build the software, and now we'll define the module to tell NixOS how to run the software. This is a basic NixOS module. It's defined inline to the flake for now, moving it to its own file is an exercise for the reader.
Like I said before, a NixOS module is a function that takes in the state of the world and returns new additions to the state of the world. This NixOS module provides some options under xe.services.douglas-adams-quotes
and then if the module is enabled, it creates a new systemd service to run it in. We're in the future, so we can use fancy things like DynamicUser to avoid having to run this service as root.
options.xe.services.douglas-adams-quotes = {
enable = mkEnableOption "Enable the Douglas Adams quotes service";
logLevel = ...;
port = ...;
package = ...;
};
The real fun part comes when you define options for the service. Every one of these options correlates to CLI flags so you can change various options on the fly. It's good practice to map any non-secret configuration settings to options so that users can have easy escape hatches for changing things like the HTTP bind port or log level to get debug output. Secrets are a more complicated thing due to how Nix works, so we're not going to talk about those today.
So we have everything we need now. We have development environment configuration, a package build, and finally a NixOS module to get the service running. The last step is to push it into prod. I have a NixOS virtual machine set up for this on my MacBook, but you may want to run this somewhere else, such as in Hyper-V on your gaming tower. Or maybe the cloud, I won't judge!
Now we get to the fun part, enabling the NixOS module. I'm going to use the VS Code Tailscale extension to SSH in and open up the files in my VM, so lemme do that real quick.
Let's peer into my VM's deployment flake and see what we can do to deploy it. This is a brand new, never opened VM, the only thing I did was set up a flake in /etc/nixos/flake.nix that imports the autogenerated configuration from the installer. This allows us to import things like the Douglas Adams Quotes service into the VM.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, ... }: {
nixosConfigurations.douglas-adams = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
modules = [
./configuration.nix
];
};
};
}
Here's what the file looks like. I import nixos unstable, and then I create a nixos configuration for a machine named douglas-adams. This "modules" block has a list of NixOS module filenames or literal expressions. This lets you import NixOS modules from other flakes and define your own NixOS modules on the fly.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.daquotes = {
url = "github:Xe/douglas-adams-quotes";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, daquotes, ... }: {
nixosConfigurations.douglas-adams = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
modules = [
daquotes.nixosModules.default
./configuration.nix
({ pkgs, ... }: {
xe.services.douglas-adams-quotes = {
enable = true;
};
})
];
};
};
}
To make it import this, first we add a new input that points to the Douglas Adams quotes flake. This then gets threaded into the outputs function, we import the module, and finally enable it on the system.
(Pause)
That's it. That's how you enable the service on the VM. Now all that's left is SSHing in and running a nixos-rebuild to enable it.
I just right-click on the douglas-adams node, ssh in with Tailscale handling the auth, and then run the magic nixos-rebuild command: nixos-rebuild switch --flake /etc/nixos
and hit enter.
And now the source code gets pulled, the package gets built, the service gets created and then we can see that the process is running. Let's prove that it's working:
[root@douglas-adams:/]# curl http://localhost:8080/quote.json | jq
{
"quote":"The story so far: In the beginning the Universe was created. This has made a lot of people very angry and been widely regarded as a bad move.",
"person":"Douglas Adams",
"source":"The Restaurant at the End of the Universe",
"id":2
}
Et voila. But, we aren't stopping there. I also enabled serving it with Tailscale Funnel so that you can see it on your phones:
tailscale serve https / http://localhost:8080
tailscale funnel 443 on
Get your phones out, I'm gonna be showing a QR Code:
Scan this QR code. You can trust me, right? It's not gonna be a Rick Roll. I'm not that mean. When you do, you'll connect to my VM on my laptop on the conference wifi, yet still exposed to the public internet.
(Pause for audience to discover that it does in fact work, applause)
In conclusion, NixOS modules aren't hard. It's just options to configure systemd or nginx or even Tailscale. It's a function that takes in the state of the world and returns new parts to add to the whole. This gives you a nearly infinite level of composition and logistical freedom to implement whatever you want. Every systemd option is exposed as a NixOS flag. Your programs can become services trivially. It's just that easy. I promise.
But, now you know how to make your own NixOS modules for fun and (hopefully) profit!
Before we get this talk wrapped up, I just want to thank everyone on this list for helping me make this talk shine. Thanks everyone!
(Pause for applause)
And with that, I've been Xe! Thank you so much for coming to this talk. I hope you've had a good conference and I'll be wandering around in case you have any questions. I've posted a summary of the code samples on my blog at xeiaso.net so you can look into my code some more.
Oh by the way, if you're looking, Tailscale is hiring. I know it's probably rare to see someone like this at a Linux conference but if you're a Windows expert please let me know. That role has been so hard to fill.
I try to answer every question I can, but if I don't get to you, please email dynamicuser at xeserv dot us and I'll reply to your questions as soon as I can.
Thanks again to the All Systems Go organizers for having me here and I hope you continue having a good day. Be well!
There was a question about encrypted secrets in NixOS. I suggest using agenix to have age-encrypted secrets in your NixOS configs. It has you encrypt things against SSH host public keys for your machines. It's a bit of a hack, but it works well enough that it's what I use in prod for my own stuff. This really needs to be solved upstream with proper handling of secret values at the Nix level.
/nix/store
is world-readable. Depending on your threat model and if your NixOS configs are open source, this can be fine. If your threat model includes public NixOS configs, this becomes less fine; especially when CI is brought into the mix. You wouldn't want someone to figure out what your secrets are in your CI flow and then exfiltrate Tailscale authkeys or something, that could be bad!Reaching the Unix Philosophy's Logical Extreme with Webassembly
YouTube link (please let me know if the iframe doesn't work for you)
Good morning Berlin! How're you doing this fine morning? I'm Xe and today I'm gonna talk about something that I'm really excited about:
WebAssembly. WebAssembly is a compiler target for an imaginary CPU that your phones, tablets, laptops, gaming towers and even watches can run. It's intended to be a level below JavaScript to allow us to ship code in maintainable languages. Today I'm gonna be talking about fun ways you can take advantage of WebAssembly, but first we need to talk about the other main part of this subject:
Unix. Unix is the sole survivor of the early OS wars. It's not really that exciting from a computer science standpoint other than it was where C became popular and it uses file and filesystem API calls to interface with a lot of hardware.
Dealing with some source code files? Discover them in the filesystem in your home directory and write to them with the file API.
Dealing with disks? Discover them in the filesystem and manage them with the file API.
Design is rooted in philosophy, and Unix has a core philosophy that all the decisions stem from. This is usually quoted as "everything is a file" but what does that even mean? How does that handle things that aren't literally files?
(Pause)
And wait, who's this Xe person?
Like the nice person with that microphone said, I'm Xe. I'm the person that put IPv6 packets into S3 and I work at Tailscale doing developer relations. I'm also the only person I know with the job title of Archmage. I'm a prolific blogger and I live in Ottawa in Canada with my husband.
I'm also a philosopher. As a little hint for anyone here, when someone openly introduces themselves as a philosopher, you should know you're in for some fun.
Speaking of fun, I know you got up early for this talk because it sold itself as a WebAssembly talk, but I'm actually going to break a little secret with you. This isn't just a WebAssembly talk. This is an operating systems talk because most of the difficulties with using WebAssembly in the real world are actually operating systems issues. In this talk I'm going to start with the Unix philosophy, talk about how it relates to files, and then I'm gonna circle back to WebAssembly. Really, I promise.
So, going back to where we were with Unix, what does it mean for everything to be a file? What is a file in the first place?
In Unix, what we call "files" are actually just kernel objects we can make a bunch of calls to. And in a very Unix way, file handles aren't really opaque values; they are just arbitrary integers that just so happen to be indices into an array that lives in the struct your kernel uses to keep track of open files in that process. That is the main security model for access to files when running untrusted code in Linux processes.
So with these array indices as arguments to some core system calls you can do some basic calls such as-
(Pause)
Actually, now that I think about it, we just spent half an hour sitting and watching that lovely talk on the Go ecosystem. Let's do a little bit of exercise. Get that blood flowing!
So how many of you can raise your hands? Keep them up, let's get those hands up!
(Pause)
Alright, alright, keep them up.
How many of you have seen one of these 3d printed save icon things in person? If you have, keep your hand up. If not, put it down.
(Pause)
How many of you have used one of them in school, at work, or even at home? If you have, keep it up, if not, put it down.
(Pause)
Alright, thanks again! One more time!
How about one of these audio-only VHS tapes? Keep it up or put it down.
Alright, for those of you with your hands up, it's probably time to schedule that colonoscopy. Take advantage of that socialized medicine! You can put your hands down now, I don't want to be liable.
(Audience laughs)
For the gen-zed in the crowd that had no idea what these things are, a cassette tape was what we used to store music on back when there were 9 planets.
(Audience laughs)
So when I say files, let's think about these. Cassette tapes. Cassette tapes have the same basic usage properties as files.
To start, you can read from files and play music from a cassette tape in all that warm analog goodness. You can write to files and record audio to a cassette tape. Know the term "mixtape"? That's where it comes from. You can also open files and insert a cassette tape into a tape player. When you're done with them, you can close files and remove tapes from a tape player. And finally you can fast-forward and rewind the tape to find the song you want. Imagine that Gen Z, imagine having to find your songs on the tape instead of skipping right to them.
And these same calls work on log files, hard drives, and more. These 5 basic calls are the heart of Unix that everything spills out from, and this basic model gets you so far that it's how this little OS you've never heard of called Plan 9 works.
But what about things that don't directly map to files? What about network sockets? Network sockets are the abstraction that Unix uses to let applications connect to another computer over a network like the internet. You can open sockets, you can close them, you can read from them, you can write to them. But are they files?
Turns out, they are! In Unix you use mostly the same calls for dealing with network sockets that you do for files. Network sockets are treated like one of these things: an AUX cable to cassette tape adaptor. This was what we used to use in order to get our MP3 players, CD players, Gameboys, and smartphones connected up to the car stereo. This isn't a bit, we actually used these a lot. Yes, we actually used these. I used one extensively when I was delivering pizzas in high school to get the turn by turn navigation directions read out loud to me. We had no other options before Bluetooth existed. It was our only compromise.
(Audience laughs)
How about processes? Those are known to be another hallmark of the Unix philosophy. The Unix philosophy is also understood to be that programs should be filters that take the input and spruce it up for the next stage of the pipeline. Under the hood, are those files?
Yep! Turns out they're three files: input from the last program in the chain, output to the next program in the chain, and error messages to either a log file or operator. All those pipelines in your shell script abominations that you are afraid to touch (and somehow load-bearing for all of production for several companies) become data passing through those three basic files.
It's like an assembly line for your data, every step gets its data fed from the output of the last one and then it sends its results to the input of the next one. Errors might to go an operator or a log sink like the journal, but it goes down the chain and lets you do whatever you want. Really, it's a rather elegant design, there's a reason it's lasted for over 50 years.
So you know how I promised that I'm gonna relate all this back to WebAssembly? Here's when. Now that we understand what Unix is, let's talk about what WebAssembly by itself isn't.
WebAssembly is a CPU that can run pure functions and then return the results. It can also poke the outside world in a limited capacity, but overall it's a lot more like this in practice:
A microcontroller. Sure you can use microcontrollers to do a lot of useful things (especially when you throw in temperature sensors and maybe even a GSM shield to send text messages), but the main thing that microcontrollers can't easily do is connect to the Internet or deal with files on storage devices. Pedantically, this is something you can do, but every time it'll need to be custom-built for the hardware in question. There's no operating system in the mix, so everything needs to have bespoke code. Without an operating system, there's no network stack or even processes. This makes it possible, but moderately difficult to reuse existing code from other projects. If you want to do something like run libraries you wrote in Go on the frontend, such as your peer to peer VPN engine and all of its supporting code, you'd need to either do a pile of ridiculous hacks or you'd just need there to be something close to an operating system. Those hacks would be fairly reliable, but I think we can do better.
(Pause)
Turns out, you don't need an operating system to fill most of the gaps that are left when you don't have one. In WebAssembly, we have something to fill this gap:
WASI. WASI is the WebAssembly System Interface. It defines some semantics for how input, output, files, filesystems, and basic network sockets should be implemented for both the guest program and the host environment. This acts like enough of an "operating system" as far as programming languages care. When you get the Go or Rust compiler to target WASI, they'll emit binaries that can run just about anywhere with a WASI runtime.
So circling back on the filesystem angle, one of the key distinctions with how WASI implements filesystem access compared to other operating systems is that there's no expectation for running processes to have access to the host filesystem, or even any filesystem at all. It is perfectly legal for a WASI module to run without filesystem access. More critically for the point I'm trying to build up to though, there are a few files that are guaranteed to exist:
Standard input, output, and error files. And, you know what this means? This means we can circle back to the Unix idea of WebAssembly programs being filters. You can make a WebAssembly program take input and emit output as one step in a longer process. Just like your pipelines!
As an example, I have an overcomplicated blog engine that includes its own dialect of markdown because of course it does. After getting nerd sniped by Amos, I rewrote it all in Rust; but when I did that, I separated the markdown parser into its own library and made a little command-line utility for it. I compiled that to WebAssembly with WASI and now I think I'm one of the only people to have successfully distributed a program over the fediverse: the library that I use to convert markdown to HTML, with the furry avatar templates that orange websites hate and all.
Just to help hammer this all in, I'm going to show you some code I wrote between episodes of anime and donghua. I wrote a little "echo server" that takes a line of input, runs a WebAssembly program on that line of input fed into standard in, and then returns the response from standard out. The first program I'm gonna show off is going to be a "reply with the input" program. Then, I'm going to switch it over to my markdown library I mentioned and write out a message to get turned into HTML. I'm going to connect to it with another WebAssembly program that has a custom filesystem configuration that lets you use the network as a filesystem because WASI's preview 1 API doesn't support making outgoing network connections at the time of writing. If sockets really are just files, then why can't we just use the network stack as a filesystem?
Now, it's time, let's show off the power of WebAssembly. But first, the adequate prayers are needed: Demo gods, hear my cries. Bless my demo!
On the right hand side I have a terminal running that WebAssembly powered echo server I mentioned. Just to prove I didn't prerecord this, someone yell out something for me to type into the WebAssembly program on the left.
(Pause for someone to shout something)
Cool, let's type it in:
(Type it in and hit enter)
See? I didn't prerecord this and that lovely member of the audience wasn't a plant to make this easier on me.
(Audience laughs)
You know what, while we're at it, let's do a little bit more. I have another version of this set up where it feeds things into that markdown->HTML parser I mentioned. If I write some HTML into there:
(Type it in and hit enter)
As you can see, I get the template expanded and all of the HTML goodness has come back to haunt us again. Even though the program on the right is written in Go:
(I press control-backslash to cause the go runtime to vomit the stack of every goroutine, attempting to prove that there's nothing up my sleeve)
It's able to run that Rust program like it's nothing.
(Applause)
Thank Klaus that all that worked. I'm going to put all the code for this on my GitHub page in my x repo.
This technique of embedding Rust programs into Go programs is something I call crabtoespionage. It lets you use the filter property of Unix programs as functions in your Go code. This is how you Rustaceans in crowd can sneak some Rust into prod without having to make sacrifices to the linker gods. I know there's at least one of you out there. Commit the .wasm bytes from rustc or cargo to your repo and then you can still build everything on a Mac, Plan 9, or even TempleOS, assuming you have Go running there.
Most of the heavy lifting in my examples is done with Wazero, it's a library for Go programs that is basically a WebAssembly VM and some hooks for WASI implemented for you. The flow for embedding Rust programs into Go looks like this:
- First, extract the subset of the library you want and make it a standalone program. This makes it easy to test things on the command line. Use arguments and command line flags, they're there for a reason.
- Next, build that to WASI and fix things until it works. You'll have to figure out how to draw the rest of the owl here. Some things may be impossible depending on facts and circumstances. Usually things should work out.
- Then import wazero into your program and set everything up by using the embed directive to hoist the WebAssembly bytes into your code. Set up the filesystems you want to use, and your runtime config and finally:
- Then make a wrapper function that lets you go from input to output et voila!
You've just snuck Rust into production. This is how I snuck Rust into prod at work and nobody is the wiser.
(Pause)
Wait, I just gave it away, no, oops. Sorry! I had no choice. Mastodon HTML is weird. The Go HTML library is weirder.
There's a few libraries on GitHub that use this basic technique for more than just piping input to output, they use it to embed C and C++ libraries into Go code. In the case of the regular expressions package, it can be faster than package regexp in some cases. Including the WebAssembly overhead. It's incredible. There's not even that many optimizations for WebAssembly yet!
No C compiler required! No cross-compiling GCC required! No satanic sacrifices to the dark beings required! It's magic, just without the spell slots.
So while we're on this, let's take both aspects of this to their logical conclusions. What about plugins for programs? There's plenty of reasons customers would want to have arbitrary plugin code running, and also plenty of reasons for you to fear having to run arbitrary customer plugin code. If we can run things in an isolated sandbox and then define our own filesystem logic: what if we expose application state as a filesystem? Trigger execution of the plugin code based on well-defined events that get piped to standard input. Make open calls fetch values from an API or write new values to that same API.
This is how the ACME editor for Plan 9 works. It exposes internal application state as a filesystem for plugins to manipulate to their pleasure.
So, to wrap all of this up:
- When you're dealing with Unix, you're dealing with files, be they source code, hard drives, or network sockets.
- Like anything made around a standards body, even files themselves are lies and anything can be a file if it lies enough in the right way.
- Understanding that everything is founded on these lies frees you from the expectation of trying to stay consistent with them. This lets you run things wherever without having to have a C compiler toolchain for win32 handy.
- Because everything is based on these lies, if you control what lies are being used, you actually end up controlling the truths that users deal with. When you free yourself from the idea of having to stay consistent with previous interpretation of those lies, you are free to do whatever you want.
How could you use this in your projects to do fantastic new things? The ball's in your court Creators.
(Applause)
With all that said, here's a giant list of everyone that's helped me with this talk, the research I put into the talk, and uncountable other things. Thank you so much everyone.
(Thunderous applause)
And with that, I've been Xe! Thank you so much for coming out to Berlin. I'll be wandering around if you have any questions for me, but if I miss it, please do email me at crabtoespionage@xeserv.us. I'll reply to your questions, really. My example code is in the conferences folder of my experimental repo github.com/Xe/x. Otherwise, please make sure to stay hydrated and enjoy the conference! Be well!
How to use Tailwind CSS in your Go programs
When I work on some of my smaller projects, I end up hitting a point where I need more than minimal CSS configuration. I don't want to totally change my development flow to bring in a bunch of complicated toolkits or totally rewrite my frontend in React or something, I just want to make things not look like garbage. Working with CSS by itself can be annoying.
Remember: ignorance is the default state.
I've found a way to make working with CSS a lot easier for me. I've been starting to use Tailwind in my personal and professional projects. Tailwind is a CSS framework that makes nearly every CSS behavior its own utility class. This means that you can make your HTML and CSS the same file, minimizing context switching between your HTML and CSS files. It also means that you can build your own components out of these utility classes. Here's an example of what it ends up looking like in practice:
<div class="bg-blue-500 text-white font-bold py-2 px-4 rounded">Button</div>
This is a button that has a blue background, white text, is bold, has a padding of 2, and has a rounded border. This looks like a lot of CSS to write for a button, but it's all in one place and can be customized for every button. This is a lot easier to work with than having to context switch between your HTML and CSS files.
px-2
, it's padding on the X axis by two what?One of the biggest downsides is that Tailwind's compiler is written in JavaScript and distributed over npm. This is okay for people that are experienced JavaScript Touchers, but I am not one of them. Usually when I see that something requires me to use npm, I just close the tab and move on. Thankfully, Tailwind is actually a lot easier to use than you'd think. You really just have to install the compiler (either with npm or as a Nix package) and then run it. You can even set up a watcher to automatically rebuild your CSS file when you change your HTML templates. It's a lot less overhead than you think.
Assumptions
To make our lives easier, I'm going to assume the following things about your project:
- It is written in Go.
- You are using
html/template
for your HTML templates. - You have a
static
folder that has your existing static assets (eg: https://alpinejs.dev for interactive components).
Setup
If you are using Nix or NixOS
Add the tailwindcss
package to your flake.nix
's devShell
:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: {
devShell = nixpkgs.mkShell {
nativeBuildInputs = [ self.nixpkgs.tailwindcss ];
};
};
}
Then you should be able to use the tailwindcss
command in your shell. Ignore the parts about installing tailwindcss
with npm
, but you may want to use npm
as a script runner or to install other tools. Any time I tell you to use npx tailwindcss
, just mentally replace that with tailwindcss
.
First you need to install Tailwind's CLI tool. Make sure you have npm
/nodejs
installed from the official website.
Then create a package.json
file with npm init
:
npm init
Once you finish answering the questions (realistically, none of the answers matter here), you can install Tailwind:
npm install --dev --save tailwindcss
Now you need to set up some scripts in your package.json
file. You can do this by hand, or you can use npm
's built-in script runner to do it for you. This lets you build your website's CSS with commands like npm run build
or make your CSS automatically rebuild with npm run watch
. To do this, you need to add the following to your package.json
file:
{
// other contents here, make sure to add a trailing comma.
"scripts": {
"build": "tailwindcss build -o static/css/tailwind.css",
"watch": "tailwindcss build -o static/css/tailwind.css --watch"
}
}
npm run watch
in another terminal while you're working on your website. This will automatically rebuild your CSS file when you change your HTML templates.Next you need to make a tailwind.config.js
file. This will configure Tailwind with your HTML teplate locations as well as let you set any other options. You can do this by hand, or you can use Tailwind's built-in config generator:
npx tailwindcss init
Then open it up and configure it to your liking. Here's an example of what it looks like when using Iosevka Iaso:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./tmpl/*.html"], // This is where your HTML templates / JSX files are located
theme: {
extend: {
fontFamily: {
sans: ["Iosevka Aile Iaso", "sans-serif"],
mono: ["Iosevka Curly Iaso", "monospace"],
serif: ["Iosevka Etoile Iaso", "serif"],
},
},
},
plugins: [],
};
<link rel="stylesheet" href="https://cdn.xeiaso.net/static/pkg/iosevka/family.css" />
If you aren't serving your static assets in your Go program already, you can use Go's standard library HTTP server and go:embed
:
//go:embed static
var static embed.FS
// assuming you have a net/http#ServeMux called `mux`
mux.Handle("/static/", http.FileServer(http.FS(static)))
This will bake your static assets into your Go binary, which is nice for deployment. Things you can't forget are a lot more robust than things you can forget.
Finally, add a //go:generate
directive to your Go program to build your CSS file when you run go generate
:
//go:generate npm run build
When you change your HTML templates, you can run go generate
to rebuild your CSS files.
Finally, make sure you import your Tailwind build in your HTML template:
<link rel="stylesheet" href="/static/css/tailwind.css" />
Now you can get started with using Tailwind in your HTML templates! I hope this helps.