A surprisingly simple way to package Deno applications for Nix
Introduction
Recently, I was working on a Deno project which I wanted to package for Nix.
Usually, when packaging a piece of software for Nix,
there exists a language-specific stdenv.mkDerivation
derivative
which works to bridge the gap between between the langauge-specific package managers and Nix.
These are functions like buildNpmPackage
and buildPythonPackage
but, alas, there is no buildDenoPackage
.
Deno is particularly tricky
(as compared to for example TCL),
because it uses “URL imports”
to import directly from URLs as runtime.
Doing so is obviously not deterministic
which means that bundling Deno applications becomes a bit of a challenge.
In this post, I will go over
why none of the existing, community-driven solutions worked for me,
what I did instead,
and some of the potential drawbacks of my solution.
Existing solution
During my initial research, I found this thread disussing my exact issue:
wrapping Deno applications in Nix.
The thread settles on using deno2nix
.
deno2nix
works by parsing the lockfiles that Deno generates and generating a matching Nix derivation.
There’s a lot of work involved in what deno2nix
does;
it has to parse Deno’s lockfile format,
clean it up,
then generate a matching Nix derivation.
All of this code has potential for bugs.
Nothing illustrates this better than this issue.
It essentially boils down to Deno’s resolution algorithm setting a different User-Agent
header than what the Nix builder did.
esm.sh
was using the user-agent to send different content to Deno than to the browser
The underlying issue here is that deno2nix
is trying to replicate the exact behavior of Deno, which is a hard task.
deno2nix
also does not support NPM modules (i.e. imports using an npm:
specifier) at the time of writing.
Doing so will likely cause the amount of code in the repo to double, since NPM
packages are handled entirely differently both in the lockfile format and Deno’s resolution algorithm.
My solution
After fighting with deno2nix
for a while,
I decided to take a different approach.
Deno supports a pretty niche subcommand: deno vendor
.
This command downloads all dependencies of the given file into a folder.
This is called vendoring,
hence the name of the command.
It also generates an import map
which can be used to make Deno use these local dependencies,
rather than fetching from online.
This command is very convenient for us
because we can use it to download and fix bundles ahead of time.
To make evaluation pure, we can fix the hash of the output
(i.e. a fixed output derivation).
In case this sounds too abstract,
here’s an example.
Suppose we have a simple program which just prints a random string.
main.ts
just contains:
import { bgMagenta } from "https://deno.land/[email protected]/fmt/colors.ts";
import { generate } from "https://esm.sh/randomstring";
const s = generate();
console.log("Here is your random string: " + bgMagenta(s));
First, we’ll build the vendor directory.
We pull out the src
attribute into a separate variable,
as it is shared between both derivations.
The fact that we specify the outputHash
attribute means that this is going to be a fixed-output derivation.
As such, the builder will be allowed network access in return to guaranteeing that the output has a specific hash.
# This could of course be anywhere, like a GitHub repository.
src = ./.;
# Here we build the vendor directory as a separate derivation.
random-string-vendor = stdenv.mkDerivation {
name = "random-string-vendor";
nativeBuildInputs = [ deno ];
inherit src;
buildCommand = ''
# Deno wants to create cache directories.
# By default $HOME points to /homeless-shelter, which isn't writable.
HOME="$(mktemp -d)"
# Build vendor directory
deno vendor --output=$out $src/main.ts
'';
# Here we specify the hash, which makes this a fixed-output derivation.
# When inputs have changed, outputHash should be set to empty, to recalculate the new hash.
outputHashAlgo = "sha256";
outputHashMode = "recursive";
outputHash = "sha256-a4jEqwyp5LoORLYvfYQmymzu9448BoBV5luHnt4BbMg=";
};
Let’s try building this and taking a peek inside.
In the transcript below, you will see
that the output contains a directory hierarchy corresponding to our dependencies.
It also contains import_map.json
at the top level.
$ nix-build vendor.nix
/nix/store/…-random-string-vendor
$ tree /nix/store/…-random-string-vendor
/nix/store/…-random-string-vendor
├── deno.land
│ └── [email protected]
│ └── fmt
│ └── colors.ts
├── esm.sh
│ ├── v135
│ │ ├── @types
│ │ │ └── [email protected]
│ │ │ └── index.d.ts
│ │ ├── [email protected]
│ │ │ └── denonext
│ │ │ └── randombytes.mjs
│ │ └── [email protected]
│ │ └── denonext
│ │ └── randomstring.mjs
│ ├── [email protected]
│ └── [email protected]
└── import_map.json
Now we can build the actual application.
We are going to create a little wrapper script
which will invoke Deno with the right arguments.
We use --import-map
to have Deno use our local dependencies
and --no-remote
to force Deno not to fetch dependencies at run-time,
in case random-string-vendor
is outdated
(i.e. doesn’t include all dependencies imported by the script).
random-string = writeShellScript "random-string" ''
${deno}/bin/deno run \
--import-map=${random-string-vendor}/import_map.json \
--no-remote \
${src}/main.ts -- "$@"
'';
That’s basically all there is to it!
The great thing about this approach is
that it (by definition) uses Deno’s exact resolution algorithm.
We don’t run into trouble with esm.sh
because Deno sets the correct UA.
That’s an entire class of bugs eliminated!
Shortcomings
It’s not all sunshine and rainbows, though.
There are some significant drawbacks to this approach
which I will go over in this section.
First of all,
the vendor subcommand is woefully undercooked.
npm:
specifiers are just silently ignored.
It is outlined in this issue,
which has been open for quite some time.
In general, it doesn’t seem like this command has been getting a whole lot of love since its introduction,
probably on account of being so niche.
Nevertheless,
when Deno does finally get support for vendoring NPM modules,
this module will automatically also support them.
This is in stark contrast with deno2nix
which would require a lot of work to support npm:
specifiers.
The second major issue is that this approach doesn’t make good use of caching.
The random-string-vendor
-derivation we constructed above is essentially a huge blob;
if we change a single dependency,
the entire derivation is invalidated.
If I understand deno2nix
correctly,
it actually makes a derivation for each dependency
and then uses something akin to symlinkJoin
to combine them.
Such an approach allows individual dependencies to be cached and shared in the Nix store.
The issue of caching is tangentially related to some of the issues outlined by @volth’s Status of lang2nix approaches.
A lot of their criticism also applies here.
Conclusion
In this post I have described a simple approach to packaging Deno applications for Nix.
I much prefer it to deno2nix
simply because I understand exactly how it works.
Even then, there are some major drawbacks to using this method.
Before implementing this approach in your project,
you should consider if those trade-offs make sense for you.
Common font fallbacks
I quite like @yesiamrocks’s CSS fallback fonts repository.
It contains a lot of common CSS fallback chains.
My only gripe is that I can’t see the fonts in use.
Here, I’ve taken the liberty of converting the Markdown to some HTML with examples of each CSS chunk.
Keep in mind that if you don’t have the fonts installed on your system,
you will see the first fallback that is installed.
Any browser worth its salt will let you see this using its development tools.
For example, here’s how to do it in Firefox.
Regrettably, it is not possible to highlight failing fonts,
as doing so would allow for easy fingerprinting.
This exact issue has been discussed by the CSS Working Group.
This document is split into 3 sections:
Sans-serif fonts
Arial
To use Arial on your webpage, copy the following CSS rule.
body {
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Arial Black
To use Arial Black on your webpage, copy the following CSS rule.
body {
font-family: "Arial Black", "Arial Bold", Gadget, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Arial Narrow
To use Arial Narrow on your webpage, copy the following CSS rule.
body {
font-family: "Arial Narrow", Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Arial Rounded MT Bold
To use Arial Rounded MT Bold on your webpage, copy the following CSS rule.
body {
font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Century Gothic
To use Century Gothic on your webpage, copy the following CSS rule.
body {
font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Calibri
To use Calibri on your webpage, copy the following CSS rule.
body {
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Candara
To use Candara on your webpage, copy the following CSS rule.
body {
font-family: Candara, Calibri, Segoe, "Segoe UI", Optima, Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Avant Garde
To use Avant Garde on your webpage, copy the following CSS rule.
body {
font-family: "Avant Garde", Avantgarde, "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Helvetica
To use Helvetica on your webpage, copy the following CSS rule.
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Franklin Gothic Medium
To use Franklin Gothic Medium on your webpage, copy the following CSS rule.
body {
font-family: "Franklin Gothic Medium", "Franklin Gothic", "ITC Franklin Gothic", Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Futura
To use Futura on your webpage, copy the following CSS rule.
body {
font-family: Futura, "Trebuchet MS", Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Impact
To use Impact on your webpage, copy the following CSS rule.
body {
font-family: Impact, Haettenschweiler, "Franklin Gothic Bold", Charcoal, "Helvetica Inserat", "Bitstream Vera Sans Bold", "Arial Black", "sans serif";
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Tahoma
To use Tahoma on your webpage, copy the following CSS rule.
body {
font-family: Tahoma, Verdana, Segoe, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Segoe UI
To use Segoe UI on your webpage, copy the following CSS rule.
body {
font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Geneva
To use Geneva on your webpage, copy the following CSS rule.
body {
font-family: Geneva, Tahoma, Verdana, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Optima
To use Optima on your webpage, copy the following CSS rule.
body {
font-family: Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Gill Sans
To use Gill Sans on your webpage, copy the following CSS rule.
body {
font-family: "Gill Sans", "Gill Sans MT", Calibri, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Trebuchet MS
To use Trebuchet MS on your webpage, copy the following CSS rule.
body {
font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Lucida Grande
To use Lucida Grande on your webpage, copy the following CSS rule.
body {
font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Verdana
To use Verdana on your webpage, copy the following CSS rule.
body {
font-family: Verdana, Geneva, sans-serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Serif fonts
Big Caslon
To use Big Caslon on your webpage, copy the following CSS rule.
body {
font-family: "Big Caslon", "Book Antiqua", "Palatino Linotype", Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Didot
To use Didot on your webpage, copy the following CSS rule.
body {
font-family: Didot, "Didot LT STD", "Hoefler Text", Garamond, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Lucida Bright
To use Lucida Bright on your webpage, copy the following CSS rule.
body {
font-family: "Lucida Bright", Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Baskerville
To use Baskerville on your webpage, copy the following CSS rule.
body {
font-family: Baskerville, "Baskerville Old Face", "Hoefler Text", Garamond, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Hoefler Text
To use Hoefler Text on your webpage, copy the following CSS rule.
body {
font-family: "Hoefler Text", "Baskerville Old Face", Garamond, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Goudy Old Style
To use Goudy Old Style on your webpage, copy the following CSS rule.
body {
font-family: "Goudy Old Style", Garamond, "Big Caslon", "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Cambria
To use Cambria on your webpage, copy the following CSS rule.
body {
font-family: Cambria, Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Rockwell
To use Rockwell on your webpage, copy the following CSS rule.
body {
font-family: Rockwell, "Courier Bold", Courier, Georgia, Times, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Times New Roman
To use Times New Roman on your webpage, copy the following CSS rule.
body {
font-family: TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Perpetua
To use Perpetua on your webpage, copy the following CSS rule.
body {
font-family: Perpetua, Baskerville, "Big Caslon", "Palatino Linotype", Palatino, "URW Palladio L", "Nimbus Roman No9 L", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Bodoni MT
To use Bodoni MT on your webpage, copy the following CSS rule.
body {
font-family: "Bodoni MT", Didot, "Didot LT STD", "Hoefler Text", Garamond, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Georgia
To use Georgia on your webpage, copy the following CSS rule.
body {
font-family: Georgia, Times, "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Palatino
To use Palatino on your webpage, copy the following CSS rule.
body {
font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Rockwell Extra Bold
To use Rockwell Extra Bold on your webpage, copy the following CSS rule.
body {
font-family: "Rockwell Extra Bold", "Rockwell Bold", monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Garamond
To use Garamond on your webpage, copy the following CSS rule.
body {
font-family: Garamond, Baskerville, "Baskerville Old Face", "Hoefler Text", "Times New Roman", serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Book Antiqua
To use Book Antiqua on your webpage, copy the following CSS rule.
body {
font-family: "Book Antiqua", Palatino, "Palatino Linotype", "Palatino LT STD", Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Calisto MT
To use Calisto MT on your webpage, copy the following CSS rule.
body {
font-family: "Calisto MT", "Bookman Old Style", Bookman, "Goudy Old Style", Garamond, "Hoefler Text", "Bitstream Charter", Georgia, serif;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Monospace fonts
Lucida Console
To use Lucida Console on your webpage, copy the following CSS rule.
body {
font-family: "Lucida Console", "Lucida Sans Typewriter", monaco, "Bitstream Vera Sans Mono", monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Andale Mono
To use Andale Mono on your webpage, copy the following CSS rule.
body {
font-family: "Andale Mono", AndaleMono, monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Courier New
To use Courier New on your webpage, copy the following CSS rule.
body {
font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Monaco
To use Monaco on your webpage, copy the following CSS rule.
body {
font-family: monaco, Consolas, "Lucida Console", monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Consolas
To use Consolas on your webpage, copy the following CSS rule.
body {
font-family: Consolas, monaco, monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Lucida Sans Typewriter
To use Lucida Sans Typewriter on your webpage, copy the following CSS rule.
body {
font-family: "Lucida Sans Typewriter", "Lucida Console", monaco, "Bitstream Vera Sans Mono", monospace;
}
The following is an example of the font in use.
The quick brown fox jumped over the lazy dog.
Conclusion
Those are all the fonts @yesiamrocks included!
I’ll leave you to your decision parallysis now…
How to give Simple Voice Chat microphone permissions on MacOS
tl;dr: Click here to view a step-by-step guide
If you voice that doesn’t work
and you’re getting an error complaining about MacOS permissions,
you can execute the following code in Terminal.app
to give the Minecraft launcher the correct permissions.
After executing the code, restart the your computer.
sqlite3 "/Users/$USER/Library/Application Support/com.apple.TCC/TCC.db" <<EOF
INSERT INTO access VALUES(
'kTCCServiceMicrophone', -- service
'com.mojang.minecraftlauncher', -- client
0, -- client_type (0 = bundle id)
2, -- auth_value (2 = allowed)
3, -- auth_reason (3 = user set)
1, -- auth_version (always 1)
-- csreq:
X'fade0c00000000a80000000100000006000000060000000600000006000000020000001c636f6d2e6d6f6a616e672e6d696e6563726166746c61756e636865720000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a48523939325a454145360000',
NULL, -- policy_id
NULL, -- indirect_object_identifier_type
'UNUSED', -- indirect_object_identifier
NULL, -- indirect_object_code_identity
0, -- flags
1612407199, -- last_updated
NULL, -- pid (no idea what this does)
NULL, -- pid_version (no idea what this does)
'UNUSED', -- boot_uuid (no idea what this does)
0 -- last_reminded
);
EOF
This is confirmed to be working on the following software versions.
- MacOS 14.5 (23F79)
- Minecraft 1.21.1
- Fabric 0.16.4
- Simple Voice Chat 2.5.21
It is probably going to break slightly in future updates to MacOS.
In that case see the rest of this post.
Yesterday I wanted to play Minecraft on a server
that was using the Simple Voice Chat plugin.
However, when I joined the server,
I got a warning message about
the Minecraft not having microphone permissions.
This makes sense:
MacOS applications have to explicitly request permissionto do stuff like listening to the microphone
and the Minecraft launcher doesn’t have any reason to request that permission
so it doesn’t have it!
The recommended solution on Simple Voice Chat’s wiki is to use a Prism, a custom launcher.
I didn’t quite feel like installing and learning some random launcher just to fix this one issue
so I started looking around for other solutions.
MacOS has to be storing the permissions somewhere
and if I could just manually enter Minecraft into there,
I wouldn’t have to go through Prism.
After a bit of searching
I found this article
which explains that TCC is the mechanism by which MacOS manages permissions
and it stores all its per-user data in a file located at
/Users/$USER/Library/Application Support/com.apple.TCC/TCC.db
.
This file is actually just an SQLite database
which we can modify using a generic SQLite tool like sqlite3
.
$ sqlite3 "/Users/$USER/Library/Application Support/com.apple.TCC/TCC.db"
Executing the above will open an interactive SQL REPL.
We can see all the tables contained in the database with a special command.
sqlite> .table
access active_policy expired
access_overrides admin policies
The article also explained that the table we’re mainly interested in is called access
.
We can see its schema using .schema
.
Of note are the fields service
and client
which specify the permission and application respectively.
See the article for the meaning of the rest of the columns.
sqlite> .schema access
CREATE TABLE access (
service TEXT NOT NULL,
client TEXT NOT NULL,
client_type INTEGER NOT NULL,
auth_value INTEGER NOT NULL,
auth_reason INTEGER NOT NULL,
auth_version INTEGER NOT NULL,
csreq BLOB,
policy_id INTEGER,
indirect_object_identifier_type INTEGER,
indirect_object_identifier TEXT NOT NULL DEFAULT 'UNUSED',
indirect_object_code_identity BLOB,
flags INTEGER,
last_modified INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER)),
pid INTEGER,
pid_version INTEGER,
boot_uuid TEXT NOT NULL DEFAULT 'UNUSED',
last_reminded INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (service, client, client_type, indirect_object_identifier),
FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ON UPDATE CASCADE
);
Now all we have to do is give kTCCServiceMicrophone
permissions to com.mojang.minecraftlauncher
by inserting it in the access
table
as if the permission had been requested and granted by the user.
We can do that with the following query:
INSERT INTO access VALUES(
'kTCCServiceMicrophone', -- service
'com.mojang.minecraftlauncher', -- client
0, -- client_type (0 = bundle id)
2, -- auth_value (2 = allowed)
3, -- auth_reason (3 = user set)
1, -- auth_version (always 1)
-- csreq:
X'fade0c00000000a80000000100000006000000060000000600000006000000020000001c636f6d2e6d6f6a616e672e6d696e6563726166746c61756e636865720000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a48523939325a454145360000',
NULL, -- policy_id
NULL, -- indirect_object_identifier_type
'UNUSED', -- indirect_object_identifier
NULL, -- indirect_object_code_identity
0, -- flags
1612407199, -- last_updated
NULL, -- pid (no idea what this does)
NULL, -- pid_version (no idea what this does)
'UNUSED', -- boot_uuid (no idea what this does)
0 -- last_reminded
);
Generating a value for the csreq
column was a little tricky.
Luckily this Stackoverflow post has the answer.
It basically boils down to this:
REQ_STR=$(codesign -d -r- /Applications/Minecraft.app/ 2>&1 | awk -F ' => ' '/designated/{print $2}')
echo "$REQ_STR" | csreq -r- -b /tmp/csreq.bin
REQ_HEX=$(xxd -p /tmp/csreq.bin | tr -d '\n')
echo "X'$REQ_HEX'"
If you looked closely,
you have probably also noticed
that the schema I found has a few more columns than the one in the article.
I assume these have been added in later MacOS updates.
When constructing my INSERT
query, I just left the values as NULL
and 'UNUSED'
because that what similar rows in the table seemed to be doing.
And that’s pretty much it!
I didn’t know if/how I needed to restart TCC,
so I just rebooted my computer.
Afterwards I confirmed
that Minecraft was now showing up under the microphone permission in settings.
The obligatory meta post
The current meta seems to be making personal websites. Everybody’s doing it
and, if you are reading this, I am too. I hope this is the start of a
lasting, healthy online presence.
Another trend I’m noticing with these online spaces is the tendency for the
first (and often only) post to be about the site’s setup and such. Since I love
talking about myself, here’s a little write up about this site’s current inner
workings!
The server
First up, the hardware! This is probably the most boring part of the setup.
My site is currently running on a shitty laptop sitting in my basement. The
power cable is broken so if anyone even slightly nudges it, the computer shuts
off instantly. Not exactly Production Quality 99.99% Uptime…
It would probably have been cheaper and easier to just rent one of those
near-free VPSs somewhere but setting up this laptop was a pretty fun learning
experience. Until then, I had never tried replacing the operating system on a
computer. It was honestly pretty refreshing feeling like I was the master of
the computer and not the other way around for a change.
The server is running behind a Cloudflare proxy to provide a basic level of
security. I’ll refrain from further explanations of my networking stack due to
some pretty glaring security issues which I’d rather not elaborate on…
NixOS
An old laptop running as a server isn’t that unusual. Much more unusual is
the choice of operating system. Rather than opting for something like Ubuntu or
Arch, I went with NixOS.
Both my ““server”” and my Macbook Pro have their configurations stored
in a single monorepo. That approach definitely has its pros and cons: it’s nice
being able to share overlays and packages between the two configurations but
trying to reconcile NixOS and nix-darwin has proven to be quite a hassle. I
definitely spent waaay more time than is reasonable figuring out how to manage
such a monorepo, an issue that was not helped by Nix’s absolutely bonkers
module system. Maybe I’ll talk more about my ambivalent thoughts on NixOS and
the Nix ecosystem in some other
post.
Once I had actually gotten NixOS configured and working, setting up the actual
server was probably something like 7 LOC.
Pretty simple, since running NGINX as a reverse proxy is a pretty common use
case on NixOS.
Furthermore, if I ever decide to actually switch to a proper VPS like I
should’ve done from the start, I can just rebuild my NixOS config on that
machine! Magical!
linus.onl
Finishing off my mega-scuffed config, I obviously couldn’t go with a well
established SSG like Hugo or Jekyll.
Instead, I decided to take some inspiration from Karl
B. and write my own bespoke build script.
I decided to try using TCL for implementing this script, figuring the
language’s “everything is a string” philosophy would make it an excellent
shell-script replacement. While that definitely was the case, the script
actually ended up not relying that much on external tools as it grew.
While exploring the language, I learned that where TCL really shines is in its
metaprogramming capabilities. I used those to add a pretty cool preprocessing
phase to my post rendering pipeline: everything between a <?
and a ?>
is evaluated as TCL and embedded directly within the
post. The preprocessor works in three steps. First it takes the raw markup,
which looks like this:
# My post
Here's some *markdown* with __formatting__.
The current time is <?
set secs [clock seconds]
set fmt [clock format $secs -format %H:%M]
emit $fmt
?>.
That markup is then turned into a TCL program, which is going to generate the
final markdown, by the
parse
procedure.
emit {# My post
Here's some *markdown* with __formatting__.
The current time is }
set secs [clock seconds]
set fmt [clock format $secs -format %H:%M]
emit $fmt
emit .
That code is then evaluated in a child interpreter, created with interp create
. All invocations of the
emit
procedure are then collected by
collect_emissions
into the following result:
# My post
Here's some *markdown* with __formatting__.
The current time is 02:46.
This is the final markup which is passed through a markdown renderer to
produce the final html. This whole procedure is encapsulated in
render_markdown
.
Embedded TCL is immensely powerfull. For example, the index and
archive pages don’t recieve any special treatment from the
build system, despite containing a list of posts. How do they include the
dynamic lists, then? The list of posts that are displayed are generated by
inline TCL:
# Archive
Here's a list of all my posts. All <? emit [llength $param(index)] ?> of them!
<?
proc format_timestamp ts {
return [string map {- /} [regsub T.* $ts {}]]
}
# NOTE: Should mostly match pages/index.md
emitln <ul>
foreach post $param(index) {
lassign $post path title id created updated
set link [string map {.md .html} $path]
emitln "<li>[format_timestamp $created]: <a href=\"[escape_html $link]\">[escape_html $title]</a></li>"
}
emitln </ul>
?>
And that code sample was generated inline too!! The code above is
guaranteed to always be 100% accurate, because it just reads the post source
straight from the file system. How cool is that!?.
I quite like this approach of writing a thinly veiled program to generate the
final HTML. In the future I’d like to see if I can entirely get rid of the
markdown renderer.
P.S. Here’s a listing of the site’s source directory. Not for any particular
reason other than that I spent 20 minutes figuring out how to get the
<details>
element to work.
Directory listing
linus.onl/
├── assets
│ ├── images
│ │ └── ahmed.jpg
│ └── styles
│ ├── normalize.css
│ └── site.css
├── pages
│ ├── about.md
│ ├── archive.md
│ └── index.md
├── posts
│ ├── first-post.md
│ ├── my-lovehate-relationship-with-nix.md
│ ├── second-post.md
│ ├── the-obigatory-metapost.md
│ └── third-post.md
├── Makefile
├── README.md
├── build.tcl
├── local.vim
└── shell.nix
Conclusion
All in all, this isn’t the most exotic setup, nor the most minimal, but it’s
mine and I love it. Particularly the last bit about the build system. I love
stuff that eat’s its own tail like that.
I hope this post was informative, or that you at least found my scuffed setup
entertaining :)