<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss-styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>NotAShelf&apos;s Blog</title><description>Personal notes on Linux, Nix, NixOS, System Administration and Programming</description><link>https://notashelf.dev</link><language>en-us</language><lastBuildDate>Sun, 21 Jun 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://notashelf.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>Nix&apos;s Substituter List Is Not a Routing Table</title><link>https://notashelf.dev/posts/nix-cache-proxy</link><guid isPermaLink="true">https://notashelf.dev/posts/nix-cache-proxy</guid><description>Optimizing Nix&apos;s Binary Cache Model</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nix&apos;s substituter model is one of those designs that is &lt;em&gt;almost&lt;/em&gt; right, but
isn&apos;t &lt;em&gt;exactly&lt;/em&gt; there. It is simple in itself: you list a few binary caches in
&lt;code&gt;nix.conf&lt;/code&gt;, the daemon walks them in order, and if the path you want is anywhere
on the internet a build doesn&apos;t have to happen on your poor laptop from 2001.
The binary cache system is often listed as a &lt;em&gt;strength&lt;/em&gt; of NixOS , but it&apos;s
actually a &lt;em&gt;strength of Nix&lt;/em&gt;, and a bare minimum for something like NixOS to
work for its users. Which is to say that it&apos;s actually perfectly fine until you
have a large enough multi-cache setup for your configuration&apos;s dependencies, or
in rarer cases, your projects dependencies. By which I mean multiple binary
cache instances because you decided to fetch massive C++ and Rust projects from
the internet that need to be built, on your system.&lt;/p&gt;
&lt;p&gt;In the case of a rather complex setup, the first cache in your substituter list
is almost always &lt;code&gt;https://cache.nixos.org&lt;/code&gt;. It is fast, it is global, and it has
the binary substitutions for your system to be built in a few minutes. It does
not, however, have your overlay&apos;s packages. The second tends to be something
like the &lt;code&gt;nix-community&lt;/code&gt; cache, because you usually pull something really useful
&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-cache-proxy#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; from the nix-community organization or just hope it has some overlay
packages that you need in your obscure hardware setup. In less common cases you
also add a third party project&apos;s Cachix (or a similar 3rd party) and
occasionally, if you&apos;re technical (or dedicated) enough, your homelab&apos;s private
cache. In such a setup, every narinfo lookup walks that &lt;em&gt;rather large&lt;/em&gt; list.
Every &lt;code&gt;nix-shell -p hello&lt;/code&gt; becomes a serialized scan across four hosts on three
continents because Nix has no concept of &lt;em&gt;which substituter is most likely to
answer for this path&lt;/em&gt;. It just asks them all, in the order you wrote them down,
one after the other.&lt;/p&gt;
&lt;h2&gt;The Shape of the Problem&lt;/h2&gt;
&lt;p&gt;To explain why a proxy is the right answer, you---or rather, I---have to be
honest about what Nix&apos;s substituter logic &lt;em&gt;is&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A loop over &lt;code&gt;substituters&lt;/code&gt;, in order.&lt;/li&gt;
&lt;li&gt;For each: &lt;code&gt;HEAD /&amp;#x3C;hash&gt;.narinfo&lt;/code&gt;. If 200, fetch and use it. If 404, continue.&lt;/li&gt;
&lt;li&gt;No concurrency. No latency tracking. No memory of which cache won &lt;em&gt;last&lt;/em&gt; time
you asked for this hash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The substituter list is a &lt;em&gt;preference&lt;/em&gt;, not a &lt;em&gt;routing table&lt;/em&gt;. There is a
&lt;code&gt;priority&lt;/code&gt; field, but it is a static integer chosen at config time. It does not
know that &lt;code&gt;cache.nixos.org&lt;/code&gt; is 40 ms away on your home connection and 800 ms
away from the office VPN. It does not know that your private cache is the only
one that has the path. It does not know anything, because there is nothing to
know. The daemon is stateless on every request. For a tool that prides itself on
being a model of declarative purity, the network layer underneath is still
static and request-local.&lt;/p&gt;
&lt;h2&gt;ncro, Briefly&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/manic-systems/ncro&quot;&gt;ncro&lt;/a&gt;---Nix Cache Route Optimizer, pronounced &lt;em&gt;Necro&lt;/em&gt;---is a small HTTP proxy
that sits between &lt;code&gt;nix-daemon&lt;/code&gt; and your substituters. It is about three thousand
&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-cache-proxy#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; lines of clean, performant Rust code. It does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On a narinfo lookup, it &lt;em&gt;races&lt;/em&gt; all configured upstreams in parallel with
&lt;code&gt;HEAD&lt;/code&gt; and remembers which one won.&lt;/li&gt;
&lt;li&gt;On a NAR fetch, it streams the body straight through to the client. No disk.
No buffer. No NAR ever lives on the proxy.&lt;/li&gt;
&lt;li&gt;It keeps a small, bounded SQLite table of route decisions so a restart
doesn&apos;t force it to relearn the entire world.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is the whole product. It is deliberately &lt;em&gt;not&lt;/em&gt; another
&lt;a href=&quot;https://github.com/kalbasit/ncps&quot;&gt;&lt;code&gt;ncps&lt;/code&gt;&lt;/a&gt;, which mirrors caches to disk and
gives you all the cache-invalidation grief that comes with mirroring caches to
disk. ncro does not retain payload data once it is streamed through. ncro does,
however, fully leverage its position as a &lt;em&gt;proxy&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;What&apos;s Actually In It&lt;/h2&gt;
&lt;p&gt;The interesting parts are the ones the &lt;a href=&quot;https://github.com/manic-systems/ncro/blob/main/docs/architecture.md#architecture&quot;&gt;architecture diagram&lt;/a&gt; (which you might
or might not have paid attention to) doesn&apos;t show:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The race.&lt;/strong&gt; ncro&apos;s router groups candidates by &lt;code&gt;priority&lt;/code&gt;, then for each
priority tier spawns a &lt;code&gt;FuturesUnordered&lt;/code&gt; of &lt;code&gt;HEAD&lt;/code&gt; requests and breaks on
the first success. The tier loop is what lets you say &quot;prefer my private
cache, but only if it answers---otherwise fall through to cache.nixos.org&quot;
without writing any of that logic into Nix itself. There&apos;s a deadline pinned
to the select loop so a single hung upstream can&apos;t stall the entire lookup.
Failures are classified as &lt;em&gt;not found&lt;/em&gt;, &lt;em&gt;network error&lt;/em&gt;, and &lt;em&gt;timeout&lt;/em&gt;
because &quot;every upstream returned 404&quot; and &quot;every upstream&apos;s TCP handshake
died&quot; deserve different answers to the client.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The cache, in two layers.&lt;/strong&gt; A moka &lt;code&gt;Cache&lt;/code&gt; sits in front of SQLite, with a
1024-entry capacity and TTL bound to the route TTL. SQLite underneath, with
&lt;code&gt;narinfo_bytes&lt;/code&gt; stored alongside the route so a hot path doesn&apos;t even need a
second upstream fetch. Eviction is throttled to fire every hundred writes by
abusing &lt;code&gt;AtomicU64::fetch_add&lt;/code&gt;&apos;s pre-increment semantics. This is a detail
that bit me in review because &lt;code&gt;count % 100 == 0&lt;/code&gt; fires on the &lt;em&gt;first&lt;/em&gt; write,
when the counter is zero. The fix was one character, but the impact was real:
latency metrics were skewed until this edge case was corrected.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Health, with EMA.&lt;/strong&gt; $L_t = \alpha R_t + (1 - \alpha) L_{t-1}$,
$\alpha = 0.3$. The first sample bypasses the smoothing. Otherwise, the first
probe permanently anchors to whatever junk was in &lt;code&gt;ema_latency&lt;/code&gt; at startup.
Consecutive failures bin the upstream into Active / Degraded / Down with
multiplicative backoff (x1, x4, x10) so a dead cache stops costing you probe
traffic. Sorting falls back to &lt;code&gt;priority&lt;/code&gt; only when two upstreams are within
10% of each other, which is the only honest interpretation of &quot;ties&quot; when
you&apos;re dealing with network latency that jitters by milliseconds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inflight dedup.&lt;/strong&gt; If two clients ask for the same narinfo at the same time,
only one race happens; the other waits on the mutex and reads the LRU on the
way out. The cleanup path uses &lt;code&gt;remove_if&lt;/code&gt; with &lt;code&gt;Arc::ptr_eq&lt;/code&gt; because a
&lt;em&gt;naïve&lt;/em&gt; &lt;code&gt;remove&lt;/code&gt; will happily delete &lt;em&gt;someone else&apos;s&lt;/em&gt; goddamned &lt;code&gt;Arc&lt;/code&gt; that
landed in the slot between the time your guard read it and the time it
dropped. This avoids a TOCTOU class bug in the dedup map.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Signatures.&lt;/strong&gt; ed25519 via &lt;code&gt;ed25519-dalek&lt;/code&gt;, public keys parsed from Nix&apos;s
&lt;code&gt;name:base64(key)&lt;/code&gt; format, the fingerprint reconstructed in the exact
&lt;code&gt;1;StorePath;NarHash;NarSize;refs&lt;/code&gt; shape that &lt;code&gt;nix store sign&lt;/code&gt; writes. If you
configure a key for an upstream, narinfos that don&apos;t verify are rejected
before they hit the cache. This is the part you cannot skip: a proxy that
strips signature checks is a proxy that turns one compromised upstream into
&lt;em&gt;every&lt;/em&gt; machine pulling through you.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What I Did Not Build&lt;/h2&gt;
&lt;p&gt;There is no NAR cache. There is no mirror. There is no &quot;warm-up&quot; job. ncro will
happily serve the same 200 MB closure ten times in a row by streaming it from
upstream ten times, and that is the &lt;em&gt;correct&lt;/em&gt; behaviour for a router. The
optimization is in &lt;strong&gt;which upstream you ask&lt;/strong&gt;, not in &lt;strong&gt;avoiding the upstream
entirely&lt;/strong&gt;. The moment you start storing NARs you are signing up for
free-disk-space alerts, content-addressed-but-not-really cache poisoning, and a
maintenance surface that has nothing to do with what you actually wanted, which
was for &lt;code&gt;nix build&lt;/code&gt; to stop spending 400 ms on DNS for a cache that never has
the path. There is also no mesh by default. There &lt;em&gt;is&lt;/em&gt; an optional gossip layer
with signed UDP packets for the trusted-peer case, but it is off, and it should
be off unless you have a clear trust and threat model for peer exchange. I plan
to expand upon this in the future.&lt;/p&gt;
&lt;h2&gt;Was It Worth Writing?&lt;/h2&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;The proxy is small enough that I can keep the whole thing in my head, which I
cannot say about most software I &lt;em&gt;actually depend on&lt;/em&gt;. It solves one problem,
the problem it set out to solve, and it does not try to grow into a content
cache or a CI artifact store or a peer-to-peer Nix mesh---even though all three
are tempting and each would complicate the design substantially. Nix&apos;s
substituter logic could support this kind of behavior, but in practice it has
remained a static ordered loop for years. A dedicated proxy is a practical way
to add dynamic routing without patching the daemon itself.&lt;/p&gt;
&lt;p&gt;Whether or not this interest you, I have really enjoyed working on ncro. Perhaps
your needs are similar to mine, or it interests you too. In which case, why not
&lt;a href=&quot;https://github.com/manic-systems/ncro#quick-start&quot;&gt;give it a try&lt;/a&gt;?&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;em&gt;Cough&lt;/em&gt; &lt;a href=&quot;https://github.com/nix-community/nh&quot;&gt;nh&lt;/a&gt; &lt;em&gt;cough&lt;/em&gt;. It&apos;s useful,
it&apos;s large, and it&apos;s annoying to build if you&apos;re building from the master
branch because Rust is just like that. I can also list Hyprland in the
things I fetch from its own flake and hope the cache has it working. What? I
like my software fresh. &lt;a href=&quot;https://notashelf.dev/posts/nix-cache-proxy#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;A bit more than that now, but the tool itself is feature-complete and
functional. Not sure if I&apos;ll stop tinkering with the tool, but the scope is
well-defined by itself. Even if it &lt;em&gt;does&lt;/em&gt; end up growing in size, it&apos;ll
remain similar in codebase size and simplicity. This make ncro future-proof
effortlessly. &lt;a href=&quot;https://notashelf.dev/posts/nix-cache-proxy#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>software</category><category>programming</category><category>nix</category></item><item><title>Why I think Go is a Terrible Language</title><link>https://notashelf.dev/posts/go-sucks</link><guid isPermaLink="true">https://notashelf.dev/posts/go-sucks</guid><description>Rants and Ramblings on this little known language known as Go</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ll start with a disclaimer that I know will bother some of you. Maybe bother
you &lt;em&gt;a lot&lt;/em&gt;, or maybe make you laugh: this is not a screed from someone who
doesn&apos;t understand Go. You wish it were, and frankly, I wish it was.
Unfortunately for me, I have written Go in the past and I have also shipped Go
to &quot;prod&quot; in the past. In fact, I continue to write Go when I&apos;m too lazy or
tired to care about correctness---for reasons we&apos;ll talk about shortly. It is
also worth stating out right that while I am still in the process of converting
to more... sane languages, some of my most critical infrastructure components
have been Go for as long as I can remember. They are still Go, and they still
hold.&lt;/p&gt;
&lt;p&gt;This post has started as a Hedgedoc document I uploaded in response to questions
asking me why I think Go is designed by people who understand language design
very well, but refuse to follow established and beloved conventions. I am still
behind this statement and throughout this post you will see exactly why I
believe this. I have read much of the spec and the proposals. I have read, or at
least tried to read, the team&apos;s stated reasoning and surrounding memos. The blog
posts about why the things are the way they are and the such. Their defenses,
arguments, and more.&lt;/p&gt;
&lt;p&gt;What follows is a case. It is &lt;em&gt;my&lt;/em&gt; case about why I think Go is a terrible*
language. This is, put most simply, my attempt at a precise, specific, and
deliberately uncharitable document where the language deserves what is coming.
It is about why Go is a failure of a design philosophy dressed up and paraded
around as pragmatism. The post itself is not to dictate whether you should use
Go or not. I&apos;ll make it very clear that I franky could not care less about what
&lt;em&gt;you&lt;/em&gt; use. The post itself is to organize my thoughts, leave a structured and
developed list of my arguments for those interested in why I don&apos;t like the
language. This is also not for experts who are extremely comfortable with their
language. My goal is not changing your mind, but making my case out in the open.
This is also not a hate mail towards Go, but a strongly worded opinion essay. I
intend to write something truly wholesome for Rust in the future, and one truly
vile for Zig. For now, let&apos;s talk about Go and its design choices.&lt;/p&gt;
&lt;h2&gt;Failure by Design&lt;/h2&gt;
&lt;p&gt;The first thing I want to talk about, and perhaps the lowest hanging fruit that
everyone reaches for, is the error handling patterns of Go. This is simply
because it really is that bad. I think the designers fully understood
exceptions, sum types, and structured error propagation. Quite sure they not
only understand those patterns, but also see the individual beauty of it. They
knew about Haskell&apos;s &lt;code&gt;Either&lt;/code&gt;, about ML&apos;s type-safe exceptions and Java&apos;s
checked exceptions which are &lt;em&gt;flawed indeed&lt;/em&gt; but at least attempt correctness.
Instead, Go enforces explicit error returns everywhere, even when the result is
repetitive boilerplate that actively obscures control flow. Here, let me give
you a better idea. A typical Go function doing three fallible operations would
look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func process(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }

    data, err := io.ReadAll(f)
    if err != nil {
        return err
    }

    result, err := parse(data)
    if err != nil {
        return err
    }

    return store(result)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual logic (open, read, parse, and store) is four lines. FOUR. The error
plumbing, however, is &lt;em&gt;twelve&lt;/em&gt;. The signal-to-noise ratio is, if my math is
correct, 3:1, and this is called explicit. Now compare it to Rust, where &lt;code&gt;?&lt;/code&gt;
propagates errors with identical semantics but doesn&apos;t consume your screen free
estate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn process(path: &amp;#x26;str) -&gt; Result&amp;#x3C;(), Error&gt; {
    let data = fs::read_to_string(path)?;
    let result = parse(&amp;#x26;data)?;
    store(result)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;How many lines is that? &lt;em&gt;1, 2, 3&lt;/em&gt;... Oh yeah, FOUR.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The crucial difference isn&apos;t syntax sugar my dear reader. In Rust,
&lt;code&gt;Result&amp;#x3C;T, E&gt;&lt;/code&gt; is a real type. &lt;code&gt;E&lt;/code&gt; is a real type parameter. You can write
generic combinators over it, &lt;code&gt;map_err&lt;/code&gt;, &lt;code&gt;and_then&lt;/code&gt;, &lt;code&gt;unwrap_or_else&lt;/code&gt;. The error
type is part of the function&apos;s contract in the type system, not a convention you
hold in your head.&lt;/p&gt;
&lt;p&gt;In Go, &lt;code&gt;error&lt;/code&gt; is a single-method interface. Any type with &lt;code&gt;Error() string&lt;/code&gt;
satisfies it, which means there is no structured hierarchy, no enforcement and
absolutely no static knowledge of what errors a function can actually produce.
Somewhere around Go 1.12 or 1.13, the Go team added &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt;
as a post-hoc attempt to recover structure from this mess. This is simply an
admission of guilt. They rejected typed errors, and then recreated a weaker,
ad-hoc version of the same concept without proper exhaustiveness, without
enforced structure and without syntax support.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;io.EOF&lt;/code&gt; is the clearest and perhaps the best illustration of where the model
collapses. It is a &lt;em&gt;sentinel value&lt;/em&gt;. A global error variable callers check with
&lt;code&gt;==&lt;/code&gt;. Entire packages special-case it. In a language where &lt;code&gt;Error&lt;/code&gt; is a proper
enum, EOF would be just another variant the compiler forces you to handle but in
Go it is a convention held together by documentation and (allegedly) discipline.
It is also occasionally violated in ways that take hours to debug. The team
eventually ran a formal design process. The proposal that followed introduced
things like &lt;code&gt;check&lt;/code&gt;/&lt;code&gt;handle&lt;/code&gt;, the &lt;code&gt;try&lt;/code&gt; builtin, a &lt;code&gt;?&lt;/code&gt; operator and a bit more.
As you&apos;d expect, all of those were rejected. Eventually, sometime in 2025, the
Go blog has proudly declared that there will be &lt;em&gt;no further syntax-level
attempts&lt;/em&gt;. Now your boilerplate is intentional, and permanent.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;even lower&lt;/em&gt; hanging fruit---so low that you don&apos;t even have to lift your
arm--- is &lt;code&gt;nil&lt;/code&gt;, which is its own disaster. Go has &lt;code&gt;nil&lt;/code&gt;, and Go has interfaces.
Those two interact in a way that is genuinely (and verifiably) broken. An
interface value is internally a two-word structure. That means it consists of a
type pointer, and a data pointer. A &lt;code&gt;nil&lt;/code&gt; interface means &lt;em&gt;both are nil&lt;/em&gt;. A
German walks into a bar, end of the joke.&lt;/p&gt;
&lt;p&gt;Well not really, but a &lt;code&gt;nil&lt;/code&gt; pointer to a concrete type type assigned to an
interface variable has a &lt;em&gt;non-nil&lt;/em&gt; type pointer and a &lt;code&gt;nil&lt;/code&gt; data pointer, so the
interface is no &lt;code&gt;nil&lt;/code&gt;. Confused? Here, let me illustrate. This produces
something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type MyError struct{}
func (e *MyError) Error() string { return &quot;oops&quot; }

func getError() error {
    var err *MyError = nil
    return err
}

func main() {
    if err := getError(); err != nil {
        fmt.Println(&quot;non-nil error&quot;) // this prints
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function returned no error. The caller receives a &lt;em&gt;non-nil&lt;/em&gt; error. Mind you,
this is not even some obscure corner case. It &lt;em&gt;regularly&lt;/em&gt; shows up in real,
&quot;production&quot; codebases and the idiomatic fix is someone snarkily telling you
&quot;remember not to do this&quot; enough times so that it never leaves your head how
much you want to punch that person. The &lt;em&gt;actual&lt;/em&gt; fix, on the other hand, is
&lt;code&gt;Option&amp;#x3C;T&gt;&lt;/code&gt;, which represents absence in the type system rather than relying on
a zero value that &lt;em&gt;just happens&lt;/em&gt; to carry type metadata. Rust&apos;s &lt;code&gt;Option&amp;#x3C;T&gt;&lt;/code&gt;
makes it impossible to use a value that might be absent without explicitly
handling both cases. There is no typed None that looks like Some. The compiler
catches it before your users do.&lt;/p&gt;
&lt;p&gt;You&apos;d expect &lt;code&gt;nil&lt;/code&gt; to be &lt;em&gt;at least&lt;/em&gt; coherent given how dominant it is, but no.
&lt;code&gt;nil&lt;/code&gt; is not even one coherent thing. You can call methods on &lt;code&gt;nil&lt;/code&gt; pointers if
the method doesn&apos;t reference the receiver. A &lt;code&gt;nil&lt;/code&gt; slice has &lt;code&gt;len 0&lt;/code&gt;, and &lt;strong&gt;is
safe to range over&lt;/strong&gt;. A &lt;code&gt;nil&lt;/code&gt; map panics on write but not on read. A &lt;code&gt;nil&lt;/code&gt;
channel blocks forever on receive or send. A closed channel panics on send.&lt;/p&gt;
&lt;p&gt;That was a lot of &lt;code&gt;nil&lt;/code&gt;s in one sentence. Phew. Anywho, the point is that those
are not rules derived from a single principle, but a collection of special cases
to memorize, and getting any one of them wrong produces a runtime panic with no
compile-time indication that anything was wrong. And I think the type system&apos;s
failures run &lt;em&gt;much&lt;/em&gt; deeper than just &lt;code&gt;nil&lt;/code&gt;. Go has no sum types, no exhaustive
matching, no non-nullable types, no const generics, no immutability &lt;em&gt;by
default&lt;/em&gt;. There are basic tools for making illegal states unrepresentable, and
the designers knew this. They then explicitly rejected them in favor of runtime
discipline. As much as I like &lt;a href=&quot;https://www.youtube.com/watch?v=wHxOiAz4NF8&quot;&gt;Discipline&lt;/a&gt;, that is no substitute for proper
language design.&lt;/p&gt;
&lt;p&gt;The absence of sum types is the most damaging. In Rust you typically write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;enum ParseResult {
    Integer(i64),
    Float(f64),
    Text(String),
    Unknown,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The compiler forces you to handle every variant. Add a new one later, and every
match expression that &lt;em&gt;doesn&apos;t&lt;/em&gt; cover it becomes a compile error. In Go you
write an interface, use a type switch, and every unhandled case (silently) falls
through unless you manually add a &lt;code&gt;default&lt;/code&gt; that panics. This is a convention,
not a guarantee. Nothing in the Go language actually stops you from being unkind
to your future self. Nothing stops you from adding a new implementing type and
never updating the thirty switch statements scattered across your codebase. You,
if ever, find out at runtime. Rust, Haskell, Swift, TypeScript with
discriminated unions---they all surface this as a compile error. Go surfaces it
as a silent no-op, or a wrong result propagating for hours before anyone
notices.&lt;/p&gt;
&lt;p&gt;On the same cursed note, non-nullable types don&apos;t exist. Every pointer,
interface, slice, map, channel or function can &lt;em&gt;and will&lt;/em&gt; be &lt;code&gt;nil&lt;/code&gt;. You cannot
declare a variable that the compiler guarantees is never nil.&lt;/p&gt;
&lt;p&gt;In Kotlin, &lt;code&gt;String&lt;/code&gt; is non-nullable and &lt;code&gt;String?&lt;/code&gt; is nullable. This is enforced
at every callsite. In rust, you use &lt;code&gt;Option&amp;#x3C;T&gt;&lt;/code&gt; and the match handles the absent
case. In go, however, the best you can do is put a comment saying &quot;THIS
PARAMETER MUST NOT BE &lt;code&gt;nil&lt;/code&gt;&quot; and hope that someone does not pass it within the
next 3 months, leading the runtime panic. Similarly, immutability is just as
absent: there is no &lt;code&gt;const&lt;/code&gt; for struct fields, no &lt;code&gt;readonly&lt;/code&gt;, no way to declare
a value passed to a function must not be mutated. Go has &lt;code&gt;const&lt;/code&gt; for &lt;em&gt;primitive
literals&lt;/em&gt; and that&apos;s it. Everything else is just... mutable. The alternative to
immutability guarantees is &quot;just don&apos;t mutate things!&quot; is once again convention,
or advice. There is no guarantee, and for a language catering to beginners this
is too heavy of a footgun to hand &lt;em&gt;loaded&lt;/em&gt; to the users.&lt;/p&gt;
&lt;p&gt;The type system&apos;s gaps extend into territory the standard critique rarely
reaches. &lt;code&gt;encoding/json&lt;/code&gt; unmarshals all numbers into &lt;code&gt;float64&lt;/code&gt; when the target
is &lt;code&gt;interface{}&lt;/code&gt;, silently losing precision on integers exceeding the 53-bit
IEEE 754 mantissa. The integer &lt;code&gt;9007199254740993&lt;/code&gt; becomes
&lt;code&gt;9.007199254740992e+15&lt;/code&gt;. The fix is &lt;code&gt;Decoder.UseNumber()&lt;/code&gt;, but the default path
discards data with no warning. A &lt;code&gt;nil&lt;/code&gt; slice marshals to &lt;code&gt;null&lt;/code&gt; while an empty
slice marshals to &lt;code&gt;[]&lt;/code&gt;; a &lt;code&gt;nil&lt;/code&gt; map marshals to &lt;code&gt;null&lt;/code&gt; while an empty map
marshals to &lt;code&gt;{}&lt;/code&gt;. The distinction between &quot;this field is absent&quot; and &quot;this field
is present but empty&quot; is semantically meaningful in many APIs, yet the language
provides no way to declare which meaning a given value carries. The
differentiation depends on a runtime property indistinguishable from emptiness
in every other context.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;panic(nil)&lt;/code&gt; is indistinguishable from no panic at all. &lt;code&gt;recover()&lt;/code&gt; returns
&lt;code&gt;nil&lt;/code&gt; when the panic value was &lt;code&gt;nil&lt;/code&gt;, so a deferred recovery cannot tell whether
it caught a &lt;code&gt;panic(nil)&lt;/code&gt; or nothing happened. &lt;code&gt;recover()&lt;/code&gt; itself only works when
called directly inside a deferred function---wrapping it in a helper silently
breaks it, with no compiler warning and no lint to catch the error. Comparing
two &lt;code&gt;interface{}&lt;/code&gt; values with &lt;code&gt;==&lt;/code&gt; panics at runtime if the underlying types are
incomparable, such as maps or slices, because the type information is erased at
the comparison site. The compiler cannot protect you because the concrete type
is gone.&lt;/p&gt;
&lt;p&gt;Until Go 1.22 in 2024, loop variables were reused across iterations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;for _, item := range items {
    go func() {
        process(item) // every goroutine sees the last item
    }()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a known problem from the Go 1.0 proposals and fixed only after twelve
years. The language team initially defended the behavior as consistent with the
spec. The spec was wrong. Twelve years of production incidents before a
language-level fix is institutional reluctance to acknowledge a design
decision---reusing a loop variable for efficiency---produced a persistent,
well-understood bug surface.&lt;/p&gt;
&lt;h2&gt;Let&apos;s Talk Generics&lt;/h2&gt;
&lt;p&gt;The generics situation deserves its own section, and its own &lt;em&gt;extended&lt;/em&gt;
treatment because it is &lt;em&gt;such&lt;/em&gt; a perfect illustration of the team&apos;s priorities.
Go 1.0 shipped sometime in 2012 without generics. Which is fine. Then, for
almost a decade, the team &lt;em&gt;explicitly&lt;/em&gt; argued that code duplication is
&lt;strong&gt;PERFECTLY FINE&lt;/strong&gt; and was preferable to abstraction, actually. The result was
&lt;code&gt;interface{}&lt;/code&gt; abuse everywhere, reflection-based helpers, and the unfortunate
&lt;code&gt;go generate&lt;/code&gt; pipelines that turned code generation into a first-class workflow.
The library itself either duplicated implementations for each concrete type, or
punted to &lt;code&gt;interface{}&lt;/code&gt; and runtime assertions. If you were a user, you were
told to copy-paste sort functions.&lt;/p&gt;
&lt;p&gt;This is supposed to be &lt;em&gt;pragmatic&lt;/em&gt;, in case you haven&apos;t noticed.&lt;/p&gt;
&lt;p&gt;I&apos;m definitely not the first person to bash Go over generics, but I still want
to give you its history proper. Generics finally arrived in Go 1.18 sometime in
2022, and they were deliberately constrained. You cannot define a method on a
non-generic type that introduces its own parameters, which rules out a wide
class of useful designs. You cannot write, for example,
&lt;code&gt;func (r *Repository) FindAll[T Entity]() ([]T, error)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Your workaround must be either making the entire struct generic, which may be
structurally wrong, or moving to a free function, which discards the method set.
Neither is satisfactory, and Rust has no such restriction. Type inference is
local and frequently fails on anything involving interfaces or composite types,
in cases where any reasonable inference engine would determine the parameters
unambiguously. You cannot access a field through a type parameter even if every
type in the constraint set shares that field. You &lt;em&gt;must&lt;/em&gt; define a method
instead, because the type system cannot reason about struct layout through
constraints. There is no specialization: you cannot provide a more efficient
implementation for a specific concrete type. Go 1.25 removed the core type
restriction from type sets, a genuine improvement, but the rest of these
limitations remain.&lt;/p&gt;
&lt;p&gt;Structural typing compounds the type system&apos;s weaknesses in its own particular
way. Any type with the right method set automatically satisfies an interface,
with no declaration of intent. When a type accidentally satisfies an interface,
it becomes part of an implicit contract its author never intended to make. When
you change a method signature, you may silently break callers who depended on
that accidental relationship, with no error at the definition site; only at the
use site, possibly in a different package. The &lt;em&gt;canonical&lt;/em&gt; workaround is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var _ io.Writer = (*MyWriter)(nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A compile-time assertion that produces a type error if &lt;code&gt;*MyWriter&lt;/code&gt; doesn&apos;t
implement &lt;code&gt;io.Writer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Think about what that means: the language leaves intent so implicit that
developers invented a hack: &lt;em&gt;assign a nil pointer to a blank identifier with an
explicit type&lt;/em&gt;. This is just to recover the ability to state intent. That this
hack is idiomatic is more damning than any external criticism. Rust uses
explicit &lt;code&gt;impl Trait for Type&lt;/code&gt; declarations. The intent is in the code. You
cannot &lt;em&gt;accidentally&lt;/em&gt; implement a trait. If you change a trait&apos;s definition, the
compiler tells you exactly which &lt;code&gt;impl&lt;/code&gt; blocks need updating. The contract is in
the source, not inferred from the method set.&lt;/p&gt;
&lt;p&gt;Now, a common counterargument is the consumer-side interface pattern: you define
a narrow interface at the call site with only the methods you need, the compiler
verifies the caller provides something that fits, and changing the interface
surfaces every call site that needs updating. On its own terms this is
reasonable. For protocol handlers and I/O pipelines---the domains Go was
designed for---accepting &quot;anything that has a &lt;code&gt;Read&lt;/code&gt; method&quot; is genuinely
cleaner than requiring every type to declare its lineage. The problem is that
this pattern coexists with &lt;em&gt;producer-side interfaces&lt;/em&gt; defined once and consumed
everywhere: &lt;code&gt;json.Marshaler&lt;/code&gt;, &lt;code&gt;fmt.Stringer&lt;/code&gt;, &lt;code&gt;database/sql.Scanner&lt;/code&gt;,
&lt;code&gt;encoding.TextMarshaler&lt;/code&gt;. These carry behavioral contracts that are not encoded
in the type system. If you add a &lt;code&gt;MarshalJSON() ([]byte, error)&lt;/code&gt; method to a
struct for internal logging purposes, &lt;code&gt;json.Marshal&lt;/code&gt; will call it instead of
using reflection. Your JSON output changes silently---not a compile error, not a
test failure unless you happen to have coverage on that exact path. The encoding
packages are built around structural detection: &lt;code&gt;json.Marshal&lt;/code&gt; checks for
&lt;code&gt;Marshaler&lt;/code&gt;, then &lt;code&gt;TextMarshaler&lt;/code&gt;, then falls through to reflection.
&lt;code&gt;fmt.Sprintf&lt;/code&gt; checks for &lt;code&gt;Stringer&lt;/code&gt;. &lt;code&gt;database/sql&lt;/code&gt; checks for &lt;code&gt;Scanner&lt;/code&gt;. Add
the right method name, and the behaviour of your program changes at a distance
without your knowledge or consent.&lt;/p&gt;
&lt;p&gt;This problem compounds over time through interface pollution. Because types
satisfy interfaces implicitly, a type that grows methods through maintenance may
begin satisfying interfaces it never previously matched. A data transfer object
that acquires a &lt;code&gt;Write([]byte) (int, error)&lt;/code&gt; method because someone embedded a
buffer for convenience is now an &lt;code&gt;io.Writer&lt;/code&gt;. Code that accepted an &lt;code&gt;io.Writer&lt;/code&gt;
will accept this DTO, and the mismatch between &quot;this is a writable buffer&quot; and
&quot;this is a data object that happens to have a Write method&quot; becomes a design
problem the type system cannot surface.&lt;/p&gt;
&lt;h2&gt;But The Concurrency&lt;/h2&gt;
&lt;p&gt;Go&apos;s concurrency model is one of its marketed features and perhaps the first to
be mentioned in response when you criticize Go. It is also where the gap between
appearance and reality is widest.&lt;/p&gt;
&lt;p&gt;Goroutines are &lt;em&gt;genuinely&lt;/em&gt; cheap.&lt;/p&gt;
&lt;p&gt;The &quot;share memory by communicating&quot; slogan gestures at CSP and the channels
&lt;em&gt;may&lt;/em&gt; look principled, however, as if to really rub salt in the wound, the
language provides &lt;em&gt;none&lt;/em&gt; of the static guarantees that would make any of it
actually robust. Data races are runtime errors at best: the race detector is
opt-in via &lt;code&gt;go test -race&lt;/code&gt;, disabled in production by default, and only catches
races that occur in the specific test run you happen to execute.&lt;/p&gt;
&lt;p&gt;Not to mention, Go has no ownership model. Any goroutine can read or write any
shared variable at any time. The type system has no concept of thread safety
whatsoever. Rust&apos;s type system makes data races impossible by construction:
&lt;code&gt;Send&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt; are automatically derived marker traits, &lt;code&gt;Mutex&amp;#x3C;T&gt;&lt;/code&gt; requires
you to acquire the lock before you can touch &lt;code&gt;T&lt;/code&gt;, &lt;code&gt;Arc&amp;#x3C;T&gt;&lt;/code&gt; requires &lt;code&gt;T: Send&lt;/code&gt;
before it compiles. Rust doesn&apos;t make data races hard to write, it simply makes
them impossible to compile.&lt;/p&gt;
&lt;p&gt;Channels in Go do not enforce ownership or linearity. After you send a value on
a channel, you can still use it. Nothing in the language prevents this. You
cannot encode &quot;send exactly once, then close&quot; either and you cannot ensure a
channel is closed by exactly one goroutine. Goroutines have no parent and no
lifecycle tied to any scope. A function can return while the goroutines it
spawned are still running, still holding references to state that should have
been released, possibly blocked forever on a channel that will never receive.
Goroutine leaks are &lt;em&gt;trivially&lt;/em&gt; easy to produce. The standard mitigation is
&lt;code&gt;context.Context&lt;/code&gt;, but passing it is optional, checking the cancellation signal
is optional, and nothing enforces either. Kotlin coroutines have lexically
scoped lifetimes built into the runtime. Go has documentation asking you to be
careful.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sync.Mutex&lt;/code&gt; is not parameterized over the data it protects. You lock it and
then you can access anything. The relationship between a mutex and the state it
guards exists in comments. Rust&apos;s &lt;code&gt;Mutex&amp;#x3C;T&gt;&lt;/code&gt; wraps the data it protects: the
only way to access &lt;code&gt;T&lt;/code&gt; is through the guard you get by locking it. The invariant
is in the type. In Go it is in the README.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt; is useful and also function-scoped rather than block-scoped, which makes
it wrong for a significant class of resource management problems. Every deferred
call runs when the enclosing function returns, not when the enclosing block
exits. This is fundamentally different from RAII. In C++ and Rust, a destructor
runs when an object goes out of scope---the end of an &lt;code&gt;if&lt;/code&gt; body, a loop
iteration, an arbitrary block. In Go:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func processItems(items []Item) error {
    for _, item := range items {
        f, err := os.Open(item.Path)
        if err != nil {
            return err
        }
        defer f.Close()
        process(f)
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every file opened in the loop stays open until &lt;code&gt;processItems&lt;/code&gt; returns. The fix
is extracting the loop body into an immediately-invoked anonymous function, a
workaround that exists because the primitive isn&apos;t expressive enough for what
you&apos;re doing. Rust&apos;s &lt;code&gt;Drop&lt;/code&gt; triggers at block exit without any special syntax.
The resource is released when the owning variable goes out of scope and that&apos;s
the end of it.&lt;/p&gt;
&lt;p&gt;Less discussed are the channel semantics themselves, which form a matrix of
runtime behaviors with no unifying principle. A send on a nil channel blocks
forever. A receive from a nil channel blocks forever. Closing a nil channel
panics. Sending on a closed channel panics. Receiving from a closed channel
returns the zero value immediately. Closing a closed channel panics. Six
distinct runtime behaviors for three states of a single construct, every one of
them a runtime event with no compile-time guard. The language that prides itself
on simplicity forces you to memorize this table.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;time.After&lt;/code&gt; is another trapdoor. Used in a &lt;code&gt;select&lt;/code&gt; loop it allocates a new
timer on every iteration, and the timer is not garbage collected until it fires.
Under load, the number of pending timers grows without bound. The fix is
&lt;code&gt;time.NewTimer&lt;/code&gt; and explicit &lt;code&gt;Stop()&lt;/code&gt;, but the library offers the footgun as the
shorter path and hopes the developer reads the caveat in the documentation.
Likewise, &lt;code&gt;sync.WaitGroup&lt;/code&gt; is a raw counter with &lt;code&gt;Add&lt;/code&gt; and &lt;code&gt;Done&lt;/code&gt; and no static
enforcement that the two balance. Call &lt;code&gt;Done&lt;/code&gt; one extra time across a goroutine
boundary and the program panics with &lt;code&gt;sync: negative WaitGroup counter&lt;/code&gt;, a
message that tells you the counter went negative but not which goroutine drove
it there. Rust&apos;s &lt;code&gt;WaitGroup&lt;/code&gt; does not exist because &lt;code&gt;std::sync::Arc&lt;/code&gt; and scoped
threads make the pattern unnecessary. When you need it, &lt;code&gt;Crossbeam&lt;/code&gt; provides one
with static drop guards. Go gives you a volatile integer.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;net/http&lt;/code&gt; ships with &lt;code&gt;ServeHTTP(ResponseWriter, *Request)&lt;/code&gt; returning nothing.
Every error path inside every handler must be handled inline instead of
propagated to middleware that converts errors to responses. The community has
reinvented
&lt;code&gt;type HandlerWithError func(http.ResponseWriter, *http.Request) error&lt;/code&gt; in
roughly every Go web framework ever written. The standard library chose a
signature that every non-trivial user must wrap or replace to achieve basic
error composition.&lt;/p&gt;
&lt;h2&gt;At Least It&apos;s Simple&lt;/h2&gt;
&lt;p&gt;I think the first and most trivial element of confusion will come from how Go
programs will be &lt;em&gt;structured&lt;/em&gt;. I find it to be the exact opposite, however, as
Go has little to no coherent &quot;packaging&quot; conventions. This is to say,
package-level variables and &lt;code&gt;init()&lt;/code&gt; functions add another class of problems.&lt;/p&gt;
&lt;p&gt;You see, initialization order within a single file follows declaration order.
Across multiple files in the same package it is not guaranteed. The compiler
&lt;em&gt;tries&lt;/em&gt; to resolve the dependency graph but the spec&apos;s response to cross-file
ordering subtleties is essentially &quot;don&apos;t rely on it.&quot; &lt;code&gt;init()&lt;/code&gt; functions run
automatically, cannot return errors, cannot accept arguments, cannot be called
or tested directly, and can have arbitrary side effects.&lt;/p&gt;
&lt;p&gt;To illustrate this issue, I&apos;d like to give you the &lt;code&gt;database/sql&lt;/code&gt; driver as an
example. You write&lt;code&gt;import _ &quot;github.com/lib/pq&quot;&lt;/code&gt; and the blank import registers
a database driver as a side effect inside an &lt;code&gt;init()&lt;/code&gt; call. There is no explicit
initialization, no error return at the callsite, no indication from the import
alone that anything has happened. If registration fails you panic. If you forget
the import you get a runtime error on the first database operation. A language
that claims to value explicitness ships this as the idiomatic database driver
pattern.&lt;/p&gt;
&lt;p&gt;This is the first crack in the &quot;simple&quot; story: the language removes visible
machinery, then smuggles the machinery back in as package-level side effects.
The code looks smaller because the initialization path is no longer in the code
you are reading. That is not simplicity. That is hiding the causal chain.&lt;/p&gt;
&lt;p&gt;On a similar note, the module system&apos;s history is also an embarrassment that the
current state only partially redeems. While a beginners might &lt;em&gt;not&lt;/em&gt; be
interested in &quot;hacking&quot; the module system, they might as well be affected by its
side-effects! Go shipped without dependency management. GOPATH had no
versioning;&lt;code&gt;go get&lt;/code&gt; fetched the latest commit with no lockfile and no way to
specify which version you needed. The community produced Godep, Glide, dep, and
govendor in succession, each incompatible and each dying in turn. Go modules,
required in 1.16, fixed the core problems but introduced new ones. The major
version suffix convention encodes version into the import path itself, so
&lt;code&gt;github.com/foo/bar/v2&lt;/code&gt; is a different import path from &lt;code&gt;github.com/foo/bar&lt;/code&gt;.
Updating a direct dependency to a new major version means changing every import
statement in your codebase that references it. No other major language ecosystem
conflates import paths with version identity this way. The &lt;code&gt;replace&lt;/code&gt; directive
works for local development but does not propagate to consumers, so the
development workflow diverges from the published module and requires manual
management across multiple related modules.&lt;/p&gt;
&lt;p&gt;This is not some unrelated ecosystem complaint stapled onto a language rant. It
is the same design instinct again: avoid richer structure in the language and
tooling, then push the resulting complexity into conventions, import paths, side
effects, and developer memory. The beginner may not care about module mechanics
on day one, but the project will care eventually, and then the bill arrives with
interest.&lt;/p&gt;
&lt;h3&gt;Tooling&lt;/h3&gt;
&lt;p&gt;Of course, there&apos;s also tools not made by the community. Or, in other words,
let&apos;s talk about official tooling.&lt;/p&gt;
&lt;p&gt;As anyone who has ever used Go will tell you, and as you might have noticed
throughout the post, Go has a vast first-party tooling. To its credit, I think
the tooling is &lt;em&gt;good&lt;/em&gt; in a way that doesn&apos;t get enough attention. &lt;code&gt;gofmt&lt;/code&gt; is
excellent, &lt;code&gt;go test&lt;/code&gt; being built in is excellent. &lt;code&gt;go vet&lt;/code&gt; caches a subset of
common mistakes, but the typed-nil-as-interface bug described above is not
caught by &lt;code&gt;go vet&lt;/code&gt;. Goroutine leak detection is not built in. Correct mutex
usage enforcement is not built in. Those require &lt;code&gt;staticcheck&lt;/code&gt;, which is
&lt;em&gt;excellent&lt;/em&gt; but is third-party and requires explicit CI integration. The
baseline static analysis for a Go project is significantly weaker than the
language&apos;s correctness story requires. Build constraints until Go 1.17 were
written as magic comments: &lt;code&gt;// +build linux amd64&lt;/code&gt; and not as syntax, not as a
first-class feature, as comments the toolchain parses by convention. I am rather
biased here, but this is not what good and reliable design looks like. I draw
the line at doc comments.&lt;/p&gt;
&lt;p&gt;This matters because tooling is the usual escape hatch offered in Go&apos;s defense.
The language does not encode the invariant, but the tool will catch it. Except
the first-party tools do not catch enough of the failures the language makes
easy. They format the code. They run the tests. They catch some obvious
mistakes. They do not turn Go into a language with non-nullable types,
exhaustive matching, scoped goroutine lifetimes, typed mutex guards, or
structured error variants. The missing guarantees remain missing. The &lt;em&gt;deeper&lt;/em&gt;
tooling problem is that &lt;code&gt;reflect&lt;/code&gt; and &lt;code&gt;unsafe.Pointer&lt;/code&gt; paper over type system
gaps throughout the ecosystem. The standard library&apos;s encoding packages use
reflection to serialize arbitrary types at runtime, which means structural
mismatches are runtime errors. Rust&apos;s &lt;code&gt;serde&lt;/code&gt; (which is not in stdlib) does the
same work through procedural macros with full type safety at compile time. The
comparison is straightforward and unflattering.&lt;/p&gt;
&lt;p&gt;At this point the boundary between &quot;tooling problem&quot; and &quot;language problem&quot;
stops being meaningful. Reflection, escape analysis output, race detection, and
profiling are not external aids; they are compensatory mechanisms for properties
the language does not or cannot express. Once you rely on them, you are no
longer reasoning about your program purely through its types or its syntax. You
are reasoning about compiler behavior, runtime behavior, and tooling diagnostics
as part of the language. Those are not good cornerstones for a language aiming
to be simple and powerful.&lt;/p&gt;
&lt;p&gt;The memory model was tightened in Go 1.19 and is now more precisely specified
than it was. Without an ownership model this doesn&apos;t change the practical
situation much though, violations are catchable by the race detector if they
happen to occur during a test run, and production is where you find out
otherwise. Whether a value escapes to the heap is determined by the compiler&apos;s
escape analysis, which is a heuristic, not a contract. You cannot declare that a
value must stay on the stack or specify allocation strategy for a type. For most
programs this is an acceptable tradeoff. For latency-sensitive systems it is
not, and the debugging workflow is reading escape analysis output and profiling.
There is no language-level handle on it. Go&apos;s GC has improved dramatically and
achieves sub-millisecond pause times in common workloads, but stop-the-world
pauses still occur, and under heavy allocation pressure or with large heaps the
pause behavior becomes less predictable. For trading systems, real-time control,
audio pipelines, or anything with hard latency requirements, a GC is
disqualifying regardless of how good it is, because &quot;very short pause&quot; and &quot;no
pause&quot; are not the same thing. Rust has no GC. Stack allocation is the default.
Heap allocation is explicit through &lt;code&gt;Box&amp;#x3C;T&gt;&lt;/code&gt;, &lt;code&gt;Arc&amp;#x3C;T&gt;&lt;/code&gt;, &lt;code&gt;Vec&amp;#x3C;T&gt;&lt;/code&gt;. You know where
every allocation is because, well, you put it there.&lt;/p&gt;
&lt;p&gt;This is, if anything, where the section should land: Go is simple only if
&quot;simple&quot; means fewer visible concepts on the surface. Once the program has to
explain initialization, dependency identity, static analysis, reflection, unsafe
escape hatches, allocation behavior, and latency, the complexity has not
disappeared. It has merely moved out of the type system and into the runtime,
the toolchain, the build graph, and the operational culture around the codebase.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;None of these problems are isolated. That is the actual point. A production Go
service at scale is functions returning &lt;code&gt;(value, error)&lt;/code&gt; where &lt;code&gt;error&lt;/code&gt; is an
opaque interface with several undocumented concrete implementations, callers
checking &lt;code&gt;errors.Is&lt;/code&gt; against known sentinels and falling through silently on
unanticipated variants, goroutines outliving the requests that spawned them and
holding references to state that should have been released, a mutex somewhere
protecting the wrong data because the association was in a comment that was
accurate when written and hasn&apos;t been updated since the refactor, a nil
interface that isn&apos;t nil propagating up three layers before producing a panic
the stack trace makes difficult to attribute.&lt;/p&gt;
&lt;p&gt;The same pattern repeats in the supposedly simple parts of the language: package
initialization hidden in &lt;code&gt;init()&lt;/code&gt;, blank imports used for side-effect
registration, module identity encoded into import paths, build constraints once
parsed from comments, reflection used where the type system cannot express the
shape of the program, race detection delegated to an optional runtime tool, and
allocation behavior exposed through compiler diagnostics rather than language
guarantees. These are not separate grievances. They are the same grievance
wearing different costumes.&lt;/p&gt;
&lt;p&gt;Every one of these failure modes is preventable in languages that encode the
relevant invariants in their type systems. Every one of them requires discipline
and convention to avoid in Go, and discipline and conventions fail under time
pressure, team growth, and the ordinary entropy of a large codebase.&lt;/p&gt;
&lt;p&gt;The defense of Go is that all of this is manageable. With experienced teams,
with good code review, with staticcheck in CI, and with the race detector in
test runs and context used consistently, Go codebases can be safe and
maintainable. This is true. The question is why the language demands that entire
investment of infrastructure and discipline to achieve properties that Rust,
Haskell, Kotlin, and Swift provide structurally. Go was designed to solve
Google&apos;s specific operational problems: fast compilation, easy onboarding for
programmers of widely varying experience, and readable code review at massive
scale, just to name a few. And sure, these &lt;em&gt;are&lt;/em&gt; real goals, held with clear
intent. The original team understood exactly what they were leaving out. They
made deliberate tradeoffs, consistently applied.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The problem&lt;/em&gt;, however, is that optimizing for easy onboarding means in this
case (and many others) optimizing against correctness. A language you can learn
in a week is a language that does not make wrong programs hard to write. The
ceiling on what you can prove at compile time is low by design, and as codebases
grow and teams turn over and systems become load-bearing infrastructure, that
low ceiling costs you in ways that are slow, cumulative, and difficult to
attribute directly to the language rather than to the specific code that
happened to fail.&lt;/p&gt;
&lt;p&gt;Rust is &lt;em&gt;by no means&lt;/em&gt; a perfect language. Compile times are long enough to be a
real workflow problem on large projects. The borrow checker&apos;s learning curve is
steep and genuinely so---not artificially steep, because the concepts it
enforces are non-trivial and require building a new mental model before the
errors start making sense. Async Rust is complex in ways that affect real
programs: &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/go-sucks#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; the story around &lt;code&gt;async&lt;/code&gt; in trait methods, &lt;code&gt;dyn&lt;/code&gt; and
&lt;code&gt;impl Trait&lt;/code&gt;, and the borrow checker in async contexts is still rough in places.
The ecosystem is younger and has gaps. These are real criticisms and they should
be stated plainly. Rust is harder to learn than Go. That is true and it is not
nothing.&lt;/p&gt;
&lt;p&gt;But Rust&apos;s design decisions are coherent in a way Go&apos;s are not. Sum types are
embraced rather than rejected. Nil is eliminated rather than worked around. Data
races are impossible rather than detectable. Mutex invariants are encoded in
types rather than documented in comments. Async task lifetimes are scoped rather
than floated as untracked background work. The tradeoffs Rust makes push
complexity into the compiler and the learning curve, not into production
incidents. Errors you make in Rust surface at compile time. Errors you make in
Go surface in production. This is not a philosophical preference. The error
handling is the difference between a bug your toolchain catches in two seconds
and a bug that pisses you off for one long afternoon or an entire week.&lt;/p&gt;
&lt;p&gt;Similarly, Go is not a badly implemented language either. Well, not too badly
implemented. I would go so far to say that the runtime is excellent and the
standard library is coherent and well-documented. The toolchain is pretty fast
in ways other languages treat as aspirational. These are real achievements and I
think dismissing them would be dishonest. But a language is not just its
runtime. It is the guarantees it gives you, the invariants it lets you express,
the class of bugs it makes structurally impossible. By that measure---the one
that matters most for building software that has to be correct, not just
running---Go is deliberately, knowingly, and permanently underpowered. There&apos;s
no escaping it. That was a choice, made by people who understood what they were
choosing and why. It was a clear set of priorities, consistently applied, that
produced a language optimized for a narrow set of operational concerns at the
direct expense of correctness. The gap between what Go lets you build and what
you can actually prove about what you built is not a bug in Go. It is Go.&lt;/p&gt;
&lt;p&gt;And that gap exists for a reason. Go was designed for Google&apos;s specific
operational constraints circa 2009--2012: a monorepo with two billion lines of
code, tens of thousands of engineers with widely varying experience, a build
graph where every second of compilation time multiplied across the organization.
Fast compilation, mechanical readability, and low abstraction were not aesthetic
choices. They were operational requirements. The language satisfies them. The
problem is that nearly every Go user is not Google. They do not have Google&apos;s
SRE culture absorbing the cost of runtime failures. They do not have the code
review bandwidth that makes convention-based correctness feasible at scale. For
them the tradeoffs Go made for Google become liabilities. The fast compilation
matters less than the bugs the language fails to prevent. The onboarding speed
matters less than the invariants you cannot encode. Go was designed for an
organization whose scale is unique in the industry and marketed as a
general-purpose language. The mismatch between the design target and the actual
user base is the root cause of most of the frustrations catalogued here.&lt;/p&gt;
&lt;p&gt;None of this means Go is useless, or that writing Go is malpractice. The runtime
is excellent. The toolchain is fast enough to be aspirational. The standard
library is coherent within the constraints of the language. Generics removed the
worst of the &lt;code&gt;interface{}&lt;/code&gt; abuse. The race detector catches real bugs. Go
succeeds at what it was designed for: fast compilation, rapid onboarding, and
readable code at massive scale. But what it was designed for is narrower than
its user base assumes, and a language optimized for one organization&apos;s
operational problems at the direct expense of correctness is---for most other
users---a language that makes wrong programs easy to write. The ceiling was
chosen knowingly. The question is whether you want to live under it, and whether
you think the tradeoff of knowing how to deal with a language&apos;s quirks is worth
not picking a language with considerably less quirks.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Real Rust async has never been tried, actually. &lt;a href=&quot;https://notashelf.dev/posts/go-sucks#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>thoughts</category><category>programming</category><category>software</category></item><item><title>The Nihilist&apos;s Guide to Cross-Compiling Dioxus for Windows</title><link>https://notashelf.dev/posts/cross-compiling-dioxus</link><guid isPermaLink="true">https://notashelf.dev/posts/cross-compiling-dioxus</guid><description>One Stack to Rule Them All</description><pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The Rust GUI ecosystem has matured into a landscape of impressive, specialized
tools. If you need immediate-mode simplicity, you reach for egui; if you want an
Elm-inspired architecture, you go with Iced; and for raw, GPU-accelerated
performance, GPUI is pushing the boundaries of what’s possible.&lt;/p&gt;
&lt;p&gt;Choosing Dioxus in this environment wasn&apos;t really about a &lt;em&gt;lack of options&lt;/em&gt;, so
to speak, but about leveraging a specific, battle-tested paradigm that I have
been interested in for a while now. It is a framework that allows developers to
bring the declarative &quot;hooks and components&quot; model and the entire CSS/Tailwind
ecosystem into a native context, effectively bridging the gap between
high-velocity web development and the efficiency of a Rust backend.&lt;/p&gt;
&lt;p&gt;In the case you didn&apos;t know, I come from a frontend background. As such, I
expected to be &lt;em&gt;right at home&lt;/em&gt; when I started with Dioxus. I was not wrong.&lt;/p&gt;
&lt;h2&gt;Why Dioxus&lt;/h2&gt;
&lt;p&gt;Admittedly, Dioxus occupies a very odd niche at a superficial level. It is
neither a traditional, retained-mode native GUI toolkit nor a thin
immediate-mode layer over a GPU abstraction. It (unapologetically) embraces the
mental model of modern frontend development: declarative components,
unidirectional data flow, and side effects expressed explicitly through hooks.&lt;/p&gt;
&lt;p&gt;The choice, of course, is not about novelty. The hooks and components paradigm
has already been stress-tested at a planetary scale by the web. Its strengths
and weaknesses are very well understood, its ergonomics have been refined
through years of iteration, and its failure modes are similar. By importing that
model wholesale into Rust, Dioxus allows you to reuse not just &lt;em&gt;ideas&lt;/em&gt;, but
instincts. At least if you&apos;ve written for the web before. State lives where you
expect it to live. Effects run when dependencies change. UI becomes a pure
function of state, with impurity carefully fenced off.&lt;/p&gt;
&lt;p&gt;Dioxus&apos; website will brandish itself a little differently, and talk about
various advantages that may or may not convince you. One of those advantages is
the one-stack-for-all you gain with Rust and being able to compile for many
targets but I&apos;d like to state out loud the quiet part, which is velocity without
chaos. Compared to egui, you trade immediate-mode simplicity for structural
clarity once applications grow beyond a single window. Compared to Iced, you
give up strict Elm-style purity in exchange for a model that tolerates
real-world side effects without contortions. And compared to GPU-first
experiments like GPUI, you accept a webview boundary in return for a mature
layout engine, a ubiquitous styling language, and an ecosystem of tooling that
already knows how to scale.&lt;/p&gt;
&lt;p&gt;For someone coming from a frontend background, this is less a paradigm shift
than a homecoming. CSS is not an afterthought. Accessibility semantics exist by
default. Layout is expressive rather than imperative. Tailwind, for better or
worse, works exactly the way you expect it to. The result is a framework that
lets you spend your time thinking about application behavior instead of fighting
your UI toolkit.&lt;/p&gt;
&lt;p&gt;Dioxus is not the lowest-level option, nor the most ideologically pure. It is,
however, a pragmatic synthesis: Rust’s safety and performance paired with a UI
model that has already survived contact with reality.&lt;/p&gt;
&lt;p&gt;Just as importantly, it is one of the few frameworks in the Rust ecosystem that
treats cross-platform delivery as a first-class concern. The same component tree
can target the web, Linux, macOS, and Windows with minimal structural
divergence, which makes it possible to develop primarily on Linux without
relegating other platforms to second-class status.&lt;/p&gt;
&lt;h2&gt;Building with Dioxus&lt;/h2&gt;
&lt;h3&gt;Enter: Webview&lt;/h3&gt;
&lt;p&gt;The decision to use a system webview carries a hidden cost that becomes
painfully apparent the moment you try to ship. Because Dioxus on Windows relies
on WebView2, you are no longer &lt;em&gt;just&lt;/em&gt; compiling Rust; you are orchestrating a
complex interaction with proprietary Microsoft loaders and COM interfaces. For
those of us who prefer the deterministic, reproducible world of a Linux-based
Nix environment, this creates a significant engineering challenge. You cannot
simply cargo build your way out of a cross-compilation task that requires
official Windows SDK headers and proprietary DLLs.&lt;/p&gt;
&lt;p&gt;To bridge this gap professionally, you have to bypass the standard
cross-compilation shortcuts and target the MSVC (Microsoft Visual C++) toolchain
directly from Linux. While MinGW is often the &quot;default&quot; for cross-compiling, it
frequently struggles with the specific nuances of the WebView2 SDK. By using
cargo-xwin, you can fetch the actual Windows CRT and SDK headers into a local
cache, allowing you to treat Windows as a first-class citizen of your build
pipeline.&lt;/p&gt;
&lt;h3&gt;The Nix Way&lt;/h3&gt;
&lt;p&gt;The process begins by teaching Nix how to handle the proprietary Microsoft
artifacts. Since Nix sandboxes prevent build scripts from reaching out to the
internet, you must define a fixed-output derivation that fetches the official
WebView2 NuGet package and extracts the necessary &lt;code&gt;.lib&lt;/code&gt; and &lt;code&gt;.dll&lt;/code&gt; files. This
ensures that your build remains reproducible and that your linker has exactly
what it needs to satisfy the Windows-specific dependencies of the &lt;code&gt;wry&lt;/code&gt; and
&lt;code&gt;tao&lt;/code&gt; crates. My derivation looks something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# webview.nix
{
  stdenv,
  fetchurl,
  unzip,
}:
stdenv.mkDerivation (finalAttrs: {
  pname = &quot;webview2-sdk&quot;;
  version = &quot;1.0.3650.58&quot;;

  src = fetchurl {
    url = &quot;https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/${finalAttrs.version}&quot;;
    sha256 = &quot;sha256-kRpHISjIKsi6oMSGwjNCzJ3W59xQ11TmdnJmQsoGXGA=&quot;;
  };

  nativeBuildInputs = [unzip];

  unpackPhase = &apos;&apos;
    unzip $src
  &apos;&apos;;

  installPhase = &apos;&apos;
    runHook preInstall
    mkdir -p $out

    # Copy the x64 libraries (we can use x86 for 32-bit targets)
    install -Dm755 build/native/x64/WebView2Loader.dll.lib $out/WebView2Loader.lib
    install -Dm755 build/native/x64/WebView2Loader.dll $out/
    runHook postInstall
  &apos;&apos;;

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It could be better, but I have gotten sick of trying to get the correct URL and
loading the WebView runtime in Wine (which we&apos;ll talk about shortly.)&lt;/p&gt;
&lt;p&gt;Once the SDK is available, you have to perform what is essentially environment
surgery within your Nix devShell. The standard Nix clang wrapper is designed for
Unix-style purity and will often reject the Windows-specific flags that
cargo-xwin and MSVC-compatible crates expect. To solve this, you must explicitly
direct Cargo to use &lt;code&gt;lld-link&lt;/code&gt; as the linker and &lt;code&gt;clang-cl&lt;/code&gt; as the C compiler.
This configuration ensures that even complex C dependencies, such as SQLite, are
compiled with the correct Windows headers and linked without flavor-mismatch
errors.&lt;/p&gt;
&lt;p&gt;Furthermore, you must account for the fact that a compiled Windows executable is
functionally useless if it cannot find its dependencies at runtime. Unlike
Linux, where we can often rely on package managers or rpath, Windows expects the
&lt;code&gt;WebView2Loader.dll&lt;/code&gt; to be present in the same directory as the &lt;code&gt;.exe&lt;/code&gt; if it
cannot be loaded from the runtime. A robust pipeline automates this by using a
&lt;code&gt;build.rs&lt;/code&gt; script that detects the Nix environment and copies the DLL from the
Nix store into the target directory every time a build succeeds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// build.rs
fn main() {
    let target = std::env::var(&quot;TARGET&quot;).unwrap_or_default();
    if target.contains(&quot;windows-msvc&quot;) {
        if let Ok(sdk_path) = std::env::var(&quot;WEBVIEW2_BIN_PATH&quot;) {
            let dll = &quot;WebView2Loader.dll&quot;;
            let src = std::path::PathBuf::from(&amp;#x26;sdk_path).join(dll);
            let out_dir = std::path::PathBuf::from(std::env::var(&quot;OUT_DIR&quot;).unwrap());
            let dest = out_dir.ancestors().nth(3).unwrap().join(dll);
            if src.exists() {
                std::fs::copy(&amp;#x26;src, &amp;#x26;dest).expect(&quot;Failed to bundle WebView2Loader.dll&quot;);
            }
        }
        println!(&quot;cargo:rerun-if-env-changed=WEBVIEW2_BIN_PATH&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Final Devshell&lt;/h2&gt;
&lt;p&gt;I have crafted an intricate devshell that pulls in various tools, and sets up a
modern Clang-based compiler and linker pipeline to handle my builds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  pkgs,
  rust-bin,
  # rust-overlay params
  extraComponents ? [],
  extraTargets ? [],
}: let
  webview2-sdk = pkgs.callPackage ./packages/webview.nix {};
in
  pkgs.mkShell {
    name = &quot;mercant-dev&quot;;
    packages = [
      pkgs.taplo # TOML formatter
      pkgs.lldb # debugger
      pkgs.rust-analyzer-unwrapped # LSP
      pkgs.llvm
      pkgs.libiconv

      # Additional Cargo Tooling
      pkgs.cargo-nextest
      pkgs.cargo-deny

      # Build tools
      # We use the rust-overlay to get the stable Rust toolchain for various targets.
      # This is not exactly necessary, but it allows for compiling for various targets
      # with the least amount of friction.
      (rust-bin.nightly.latest.default.override {
        extensions = [&quot;rustfmt&quot; &quot;rust-analyzer&quot; &quot;clippy&quot;] ++ extraComponents;
        targets =
          [
            &quot;arm-unknown-linux-gnueabihf&quot; # Android
            &quot;wasm32-unknown-unknown&quot; # web
            # Windows
            &quot;x86_64-pc-windows-msvc&quot;
            &quot;x86_64-pc-windows-gnu&quot;
          ]
          ++ extraTargets;
      })

      # Cross-compiling to Windows
      pkgs.pkgsCross.mingwW64.stdenv.cc
      pkgs.pkgsCross.mingwW64.buildPackages.gcc
      pkgs.cargo-xwin

      # Link with Clang &amp;#x26; lld
      pkgs.clang
      pkgs.lld

      # Handy CLI for packaging Dioxus apps and such
      pkgs.dioxus-cli

      # Dioxus desktop dependencies (GTK/WebKit)
      pkgs.pkg-config
      pkgs.glib
      pkgs.gtk3
      pkgs.webkitgtk_4_1
      pkgs.libsoup_3
      pkgs.cairo
      pkgs.pango
      pkgs.gdk-pixbuf
      pkgs.atk
      pkgs.xdotool # provides libxdo
      pkgs.openssl
      pkgs.kdePackages.wayland
    ];

    env = let
      mcfgthread = pkgs.pkgsCross.mingwW64.windows.mcfgthreads;
    in {
      # Allow Cargo to use lld and clang properly
      LIBCLANG_PATH = &quot;${pkgs.libclang.lib}/lib&quot;;
      RUSTFLAGS = &quot;-C link-arg=-fuse-ld=lld&quot;;

      # Windows cross-comp
      WEBVIEW2_BIN_PATH = &quot;${webview2-sdk}/lib&quot;;

      CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS = &quot;-L native=${mcfgthread}/lib&quot;;
      CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = &quot;-L native=${webview2-sdk}/lib&quot;;
      CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = &quot;lld-link&quot;;

      CC_x86_64_pc_windows_gnu = &quot;x86_64-w64-mingw32-gcc&quot;;
      CXX_x86_64_pc_windows_gnu = &quot;x86_64-w64-mingw32-g++&quot;;
      CC_x86_64_pc_windows_msvc = &quot;clang-cl&quot;;
      AR_x86_64_pc_windows_msvc = &quot;llvm-lib&quot;;

      # &apos;cargo llvm-cov&apos; reads these environment variables to find these
      # binaries, which are needed to run the tests.
      LLVM_COV = &quot;${pkgs.llvm}/bin/llvm-cov&quot;;
      LLVM_PROFDATA = &quot;${pkgs.llvm}/bin/llvm-profdata&quot;;

      # Runtime library path for GTK/WebKit/xdotool
      LD_LIBRARY_PATH = &quot;${pkgs.lib.makeLibraryPath [
        pkgs.xdotool
        pkgs.gtk3
        pkgs.webkitgtk_4_1
        pkgs.glib
        pkgs.cairo
        pkgs.pango
        pkgs.gdk-pixbuf
        pkgs.atk
        pkgs.libsoup_3
        pkgs.openssl
        pkgs.kdePackages.wayland
      ]}&quot;;
    };
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Metadata and Identity&lt;/h2&gt;
&lt;p&gt;By the time you drop into your shell and execute
&lt;code&gt;cargo xwin build --release --target x86_64-pc-windows-msvc&lt;/code&gt;, you are no longer
fighting the toolchain. You have built a deterministic machine that translates
your Rust source code into a native, high-fidelity Windows binary. This approach
respects both the strict requirements of the Windows OS and the reproducible
philosophy of Nix, resulting in a distribution workflow that is as clean as the
code it compiles. The &lt;code&gt;WEBVIEW2_BIN_PATH&lt;/code&gt; variable comes from our devShell,
which sets various environment variables for the tooling.&lt;/p&gt;
&lt;h2&gt;Wine Tasting&lt;/h2&gt;
&lt;p&gt;Before arriving at this setup, it is tempting to try to paper over the problem
by simply running the Windows pieces under Wine. After all, if Dioxus ultimately
depends on WebView2, and WebView2 depends on the Edge runtime, why not just
install the runtime into a Wine prefix and let everything discover it
implicitly?&lt;/p&gt;
&lt;p&gt;That line of reasoning leads you to commands like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;wine MicrosoftEdgeWebView2RuntimeInstallerX86.exe /install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And yes, this does install the runtime. But it does not actually solve the
underlying problem.&lt;/p&gt;
&lt;p&gt;WebView2 is split across multiple layers: a runtime installed system-wide, a
loader DLL that applications link against, and a set of COM interfaces that
assume a Windows loader, registry, and activation model. Wine can approximate
enough of this environment to allow some binaries to start, but it cannot make
those assumptions disappear. From the perspective of a cross-compiling Linux
toolchain, Wine is still an opaque, mutable black box. &lt;em&gt;More importantly&lt;/em&gt;, Wine
does nothing for the build itself. Your Rust code still needs headers, import
libraries, and a linker that understands MSVC semantics. Installing the runtime
under Wine gives you a way to run a finished executable for ad-hoc testing, but
it does not help Cargo or rustc produce that executable in the first place. At
best, it delays failure until runtime. At worst, it masks missing dependencies
behind Wine-specific behavior that will not exist on a real Windows system.&lt;/p&gt;
&lt;p&gt;This is why the Wine approach ultimately gets abandoned here. It trades a hard
but explicit problem for a soft, implicit one. By pulling the SDK artifacts
directly into Nix and targeting MSVC properly, you surface the real constraints
early, make them reproducible, and stop relying on an emulation layer as an
undocumented part of your build pipeline.&lt;/p&gt;
&lt;p&gt;If you&apos;re lucky, the correct runtime will be installed out-of-the-box in your
&lt;em&gt;real&lt;/em&gt; Windows machine. You may or may not have the same luck for Wine.
Something like &lt;code&gt;winetricks corefonts windowscodecs&lt;/code&gt; might come in handy but I&apos;ve
ultimately stopped trying to get Wine to work, and booted into Windows on my
work machine.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Cross-compiling a Dioxus application for Windows from a Nix-based Linux
environment is not difficult in the sense of missing tools or undocumented
hacks. It is difficult because it forces you to reconcile two fundamentally
different worldviews. Windows assumes mutable global state, opaque SDKs, and
proprietary loaders. Nix assumes hermetic builds, explicit inputs, and
reproducibility as a first principle.&lt;/p&gt;
&lt;p&gt;The approach outlined here does not attempt to smooth over that mismatch.
Instead, it accepts Windows on its own terms by using the MSVC toolchain and
official SDK artifacts, while forcing those artifacts into Nix’s deterministic
model through fixed-output derivations and explicit environment wiring.
cargo-xwin acts as the linchpin, turning what would otherwise be an ad-hoc
Wine-based mess into a repeatable, cacheable build step.&lt;/p&gt;
&lt;p&gt;The result is not just a working executable, but a pipeline you can reason
about. Every DLL is accounted for. Every header comes from a known source. Every
build either succeeds deterministically or fails loudly. That matters,
especially when you are shipping a GUI application whose failure modes tend to
surface at runtime rather than compile time.&lt;/p&gt;
&lt;p&gt;In the end, this is less about Dioxus specifically and more about refusing false
dichotomies. You do not have to choose between modern UI paradigms and native
binaries. You do not have to choose between Windows support and a sane
Linux-based workflow. With enough stubbornness and a willingness to engage with
the toolchains honestly, you can have all of it, and you can make it
reproducible. The nihilism, such as it is, lies in accepting Windows not as a
system to be admired, but as a constraint to be modeled precisely and without
illusion.&lt;/p&gt;
&lt;p&gt;Be well.&lt;/p&gt;</content:encoded><category>programming</category><category>software</category><category>guide</category></item><item><title>2025 Wrapped &amp; 2026 Wishlist</title><link>https://notashelf.dev/posts/2025-wrapped</link><guid isPermaLink="true">https://notashelf.dev/posts/2025-wrapped</guid><description>Projects shipped, things learned, and what I want less of in 2026</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Howdy, it seems creating something with &quot;wrapped&quot; in the name is a ritual this
year around so I decided to name my 2025 recap this way... It is also
fashionably late because &lt;em&gt;Nix&lt;/em&gt; is a wonderful piece of software.&lt;/p&gt;
&lt;p&gt;Another year almost in the books. I thought about skipping the recap thing
because honestly who cares, but then I remembered I care (a little), and also
the blog has been quiet lately so might as well fill the void with some
self-indulgent yapping before my time runs out. If you are surprised by me
posting at the last second (i.e., after it became 2026 for some people reading
this), I am honestly impressed. I thought myself more predictable and you...
less gullible. That said, I am sufficiently intoxicated for the New Year
celebrations and jokes aside my dear reader, 2025 has been a very long year and
a lot has happened. Hope you&apos;ve got a drink or some snacks ready, because this
is a long one.&lt;/p&gt;
&lt;p&gt;Starting strong, NixOS 25.05 happened and I took part as a release editor. This
coincided with a lot of important real-life events so I could not do as much as
I hoped to but it was a good release regardless, and I&apos;m proud of however much I
was able to contribute. You can thank me for most of the grammar or typo fixes
in the changelog. In the end it was the usual mix of module arguments,
inconsistent defaults, and people discovering that &lt;code&gt;lib.mkForce&lt;/code&gt; is both
powerful and a crime scene. We shipped without total catastrophe, nobody died. I
count that as a win.&lt;/p&gt;
&lt;p&gt;Hyprland is also coming along nicely, for those interested. I&apos;m a little over
Hyprland at this point, but it appears that wlroots -&gt; Aquamarine migration is
continuing its slow, painful crawl toward something resembling sanity. While I&apos;m
still the resident babysitter for the community, I took off from the Nix side of
thing as those are usually not deferred to me. We&apos;ve got some Nix-related
changes coming along, but there is some time until those are actually...
tangible. Long story short, the ecosystem is still alive. That&apos;s more than most
Wayland compositors can say after three years.&lt;/p&gt;
&lt;h2&gt;Project News&lt;/h2&gt;
&lt;p&gt;Projects-wise, it was a year of small-to-medium itches scratched in various
languages. I think 2025 is the year I can finally call Rust my &quot;go-to&quot; language,
and the year my honeymoon period with Go has ended. I&apos;ve &lt;a href=&quot;https://notashelf.dev/posts/my-new-stack&quot;&gt;written about this&lt;/a&gt;
and I intend to talk a bit more in depth about the Go language because of how
confused people seem by my stance but I am quite happy with my adoption of Rust
for the time being. Plenty of new Rust projects now plague my &quot;portfolio.&quot;&lt;/p&gt;
&lt;p&gt;Something worth noting is that I have taken over &lt;a href=&quot;https://github.com/nix-community/nh&quot;&gt;nh&lt;/a&gt; from my good friend
ViperML (who considers the project complete and is no longer interested in
maintaining it as per his original vision) to continue its maintenance as time
goes on. I was a contributor to nh before this happened, so I am quite familiar
with the codebase already. This gradual change in maintainers has covered the
4.0, 4.1 and 4.2 releases as I continue to publish more bug fixes and feature
additions. If you are not familiar with nh, I recommend that you check it out as
it is quite a handy CLI for managing NixOS systems. It will continue to improve
over the time as new updates come along :)&lt;/p&gt;
&lt;p&gt;We have also released v0.8 for &lt;a href=&quot;https://github.com/notashelf/nvf&quot;&gt;nvf&lt;/a&gt;! It was a long time coming and was doubted
by many, but after just over a year, the release is out. This update brings the
Nix-for-Neovim approach of nvf to a whole new level with more robust LSP
configurations, many plugin additions, new documentation and more! The reason
that it took this long, of course, is that I was on a &lt;em&gt;tiny&lt;/em&gt; side-quest writing
a documentation generator for my Nix projects! You may or may not be aware that
I have a lot of ongoing projects related to Nix, and documentation is most often
a very high priority. Unfortunately I find the ecosystem &lt;em&gt;very&lt;/em&gt; lacking, and I
just had to create something of my own. The first half of 2025 was mostly spent
working on &lt;a href=&quot;https://github.com/feel-co/ndg&quot;&gt;ndg&lt;/a&gt;, our in-house documentation generator at &lt;a href=&quot;https://github.com/feel-co&quot;&gt;feel-co&lt;/a&gt;. While this
lives in feel-co and has been designed for feel-co (and my own) projects, it is
a project for &lt;em&gt;any&lt;/em&gt; Nix module system looking to document its options in a
stylish manner. Try it, it grew on me despite the hellish CSS and Javascript
misadventures.&lt;/p&gt;
&lt;p&gt;Another noteworthy project, which ndg was actually initially designed &lt;em&gt;for&lt;/em&gt;, is
&lt;a href=&quot;https://github.com/feel-co/hjem&quot;&gt;Hjem&lt;/a&gt;. I&apos;ve been bothered by Home Manager&apos;s poor file management primitives for
a long while, and all this rage ultimately bubbled up into our own in-house
module system for managing one&apos;s &lt;code&gt;$HOME&lt;/code&gt;, elegantly. Powered by SMFH, a manifest
based file linker made by my good bald friend Gerg-L and designed for atomicity
&amp;#x26; correctness, Hjem has received many new features such as brand new
documentation, much better module interfaces, and Darwin support---which many
were excited for. It has come a very long way over the past year, and I am happy
to report that it is nearing &quot;completion.&quot; Of course, it&apos;s doubtful we&apos;ll stop
tinkering on it &lt;em&gt;ever&lt;/em&gt; but the features originally envisioned are almost fully
implemented. If you are interested in fast, low-cost and correct file linking in
your &lt;code&gt;$HOME&lt;/code&gt; perhaps give it a try. It is at a stage where feedback and
bugreports are very valuable to us. We eventually hope to upstream Hjem into
Nixpkgs as a first-party home management system, as it is just the right amount
of abstraction; almost none. Contributions are also welcome, as they are to any
other project of mine!&lt;/p&gt;
&lt;p&gt;Some other projects worth mentioning are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/snugnug/micros&quot;&gt;MicrOS&lt;/a&gt; got its initial push in January --- Nixpkgs modules + runit instead
of Systemd. It&apos;s still extremely niche, still requires manual prayers to boot
on real hardware, and still mine. This is a long-term project that I am hoping
to develop more in 2026. If you are one of those nutjobs that are against
Systemd, your feedback would be appreciated. Contributions even more so.&lt;/li&gt;
&lt;li&gt;The byproduct of a most unfortunate naming conflict, &lt;a href=&quot;https://github.com/notashelf/stash&quot;&gt;Stash&lt;/a&gt; has gotten a bit
of attention around August and has taken its forever place in my Wayland
desktop as a feature-rich clipboard &quot;manager&quot; with persistent history and
multimedia support.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/notashelf/nix-bindings&quot;&gt;nix-bindings&lt;/a&gt; has appeared after my poor attempts to deal with C++, and is
now a somewhat viable way of interacting with Nix&apos;s C API. I don&apos;t have a
worthwhile use for this project yet, but perhaps you do. Either way, your
feedback would be appreciated. It seems that Nix is ramping up the work being
done for the C API, and as such the bindings will only evolve further over
time. Perhaps those bindings will make their way into more projects of mine to
replace instances where I shell out to Nix.&lt;/li&gt;
&lt;li&gt;I have finally &lt;a href=&quot;https://github.com/notashelf/tuigreet&quot;&gt;forked tuigreet&lt;/a&gt; to solve many of the minor issues that were
bothering me. This ended up with me implementing some requested features,
modernizing the codebase over a single week, and adding a new configuration
system. Since tuigreet is my primary greeter and will remain as such for the
foreseeable future, I plan to maintain it as much as I can. If you &lt;em&gt;also&lt;/em&gt; use
tuigreet and have had feature requests or bugs to report, then head over to
the issue tracker and let me know. I promise to keep an open mind. PRs are
also very welcome as you might guess. Ratatouille is an infinitely powerful
TUI framework and I plan to leverage its powers further as time goes by. The
result? Terribly extensible TUI greeter. You&apos;re welcome.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/notashelf/microfetch&quot;&gt;Microfetch&lt;/a&gt; has gotten &lt;em&gt;much&lt;/em&gt; faster and has taught me a lot about Rust. It
has also seen a bit of attention late 2025, so perhaps it is now adopted by
more NixOS users. It will, of course, continue to get faster. Sky is the
limit. In both speed and pointlessness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other than that, I&apos;ve continued housekeeping work on &lt;a href=&quot;https://github.com/notashelf/watt&quot;&gt;watt&lt;/a&gt; (CPU power
management), &lt;a href=&quot;https://github.com/notashelf/tailray&quot;&gt;tailray&lt;/a&gt; (Tailscale systray in Rust), and a dozen smaller things
like &lt;a href=&quot;https://github.com/notashelf/flint&quot;&gt;flint&lt;/a&gt; (flake input linter), and &lt;a href=&quot;https://github.com/notashelf/tempus&quot;&gt;tempus&lt;/a&gt;. Nothing revolutionary, but they
ship, people use them, and my system feels marginally less awful.&lt;/p&gt;
&lt;h2&gt;Blog Updates&lt;/h2&gt;
&lt;p&gt;My blog has remained opinionated and chaotic as it was designed to be. I think
I&apos;ve dropped plenty of posts within 2025, but I&apos;d like to highlight a few that
I&apos;m particularly proud of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;&lt;em&gt;The Curse of Knowing How, or; Fixing Everything&lt;/em&gt;&quot; was my most popular post
this year, and it opened the door to many interesting conversations about
software and the compelling urge to &quot;fix&quot; things. I&apos;m told this is also called
&quot;obsession.&quot; No, not like the perfume.&lt;/li&gt;
&lt;li&gt;&quot;&lt;em&gt;The Federation Fallacy&lt;/em&gt;&quot; in June---my thoughts on federated software as I
have come to adopt them in 2025. Federation is cool until you realize most
people just want their little walled garden with extra steps.&lt;/li&gt;
&lt;li&gt;&quot;&lt;em&gt;I am Not Convinced by Vibe Coding&lt;/em&gt;&quot; in August---I&apos;m quite disappointed in
the state of... well, everything. Software is only one of the things AI has
managed to ruin (I reckon with many more to come) but imagine how beautiful it
would be if we just stopped telling glorified autocomplete to rewrite things?&lt;/li&gt;
&lt;li&gt;&quot;&lt;em&gt;The Tradeoff Trap&lt;/em&gt;&quot; shortly after---simplicity isn&apos;t a virtue if you&apos;re just
sweeping edge cases under the rug and calling it minimalist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Shorter rants and post have made their appearances, but I am not going to ruin
the fun of discovering those posts for yourself, organically. I also plan to
make a few more posts in early 2026 as I get to finishing them, so maybe also
stay tuned? I&apos;ve got an &lt;a href=&quot;https://notashelf.dev/rss.xml&quot;&gt;RSS feed&lt;/a&gt; that you can
subscribe to if what I write interests you :)&lt;/p&gt;
&lt;p&gt;Shorter rants sprinkled in about Golang deliberately dodging good patterns,
mutation testing as the only test metric that matters, why Hydra is ironic hell
for a declarative ecosystem, and the usual Nix-sucks-but-less-than-alternatives
sermon.&lt;/p&gt;
&lt;h2&gt;Other Stuff&lt;/h2&gt;
&lt;p&gt;I think I aged like ten years over the last year. A lot of things happened, but
most prominently, &lt;em&gt;burnout happened&lt;/em&gt;. Still ongoing work on privacy/data
integrity in policy contexts. Briefly relapsed to Arch in November---don&apos;t ask,
it was bad, I&apos;m back on NixOS, lesson learned (again).&lt;/p&gt;
&lt;p&gt;Sailing, as usual, was alright when weather allowed. It seems that I am making
less time for things I actually enjoy. Chess ELO still trapped in the 1100-1150
limbo but I haven&apos;t played chess in almost six months. I guess you can&apos;t climb
in ELO without, you know, actually playing the damn game.&lt;/p&gt;
&lt;p&gt;Overall? Just the slow realization that software culture is still mostly vibes,
politics, and bloat, and we&apos;re all just trying to carve out tiny corners where
it sucks marginally less. We make do, we always do.&lt;/p&gt;
&lt;h2&gt;2026 Wishlist: bigger than projects, smaller than miracles&lt;/h2&gt;
&lt;p&gt;This brings us to my 2026 wishlist, and I&apos;m tired of small asks. Polish is nice,
but polish on a rotting foundation is just expensive lipstick.&lt;/p&gt;
&lt;p&gt;What I &lt;em&gt;actually&lt;/em&gt; want this year is an industry (and our little corners of it)
that &lt;em&gt;starts punishing redundant complexity&lt;/em&gt;. Not rewarding it with stars,
funding or useless engineer titles but instead make adding a dependency cost
something real---social cost, maintenance cost or cognitive cost. Stop
pretending that &quot;but it&apos;s modern!&quot; is an argument. We are in the year of 2025
and for some godforsaken reason our computer resources seem like they will be
dwindling over time. Do you know what this means? OPTIMIZATION god damn you,
actually think about it for once. I am tired of useless Typescript projects for
desktop or Python in production. Write actual languages. Write Rust, write C,
write C++ but stop overcomplicating. On this note, tooling that does not
actually hate maintainers would not be so bad either. NixOS modules cleaner from
the start. A CI that isn&apos;t as ugly as the nightmare that is Hydra. Less YAML
worship. Less vibe &quot;coding&quot; in infra that people rely on daily.&lt;/p&gt;
&lt;p&gt;Minimalism that isn&apos;t performative or cultish. Suckless is an unfunny joke sure,
but it is not and has never been the endgame. We need small &lt;em&gt;and&lt;/em&gt; correct, small
&lt;em&gt;and&lt;/em&gt; usable, small &lt;em&gt;and&lt;/em&gt; secure. Not just &quot;fits in my head&quot; while leaking like
a sieve. We know this is possible, because people used to do it when it was
funny. Now it&apos;s important, and people have long forgotten.&lt;/p&gt;
&lt;p&gt;I also want privacy to stop being a quirky nerd interest. This is not a niche
hobby for people who like encryption diagrams. This is something normal adults
should understand well enough to get angry about. Educate your wine aunt.
Educate your siblings. Explain it in plain language until it clicks that
&quot;exceptional access&quot; is just marketing speak for &quot;someone else gets to read your
shit&quot;. No more ChatControl style proposals quietly sliding through because
someone mumbled &quot;&lt;em&gt;but cyberbullying uwu&lt;/em&gt;&quot; and everyone nodded along to avoid
looking callous. That rhetorical trick needs to stop working. I need widespread
acceptance of a very boring technical truth: every backdoor is just an exploit
wearing a badge. There is no such thing as a friendly vulnerability, no such
thing as access that only the good guys use, no such thing as a hole that stays
politely scoped to its original intent. If a system can be accessed, it will be
accessed, by actors you did not anticipate and in ways you did not authorize.
Governments, companies, and especially mid-tier tech bros need to internalize
this as a fact of reality, not treat it as optional flavor you can trade away
for optics or convenience. You &lt;em&gt;wish&lt;/em&gt; this is paranoia, but it isn&apos;t. It is
&lt;em&gt;pattern recognition&lt;/em&gt; and anyone with more than just a goldfish brain should be
able to notice the patterns as well. There is a rant coming about this, but I
will spare you for today. Or rather, I will spare you &lt;em&gt;the rest&lt;/em&gt; for today.&lt;/p&gt;
&lt;p&gt;Last but not least, I wish for a single ecosystem somewhere, anywhere, that
still chooses craft over clout. Not as branding, not as a values statement, not
as a thin excuse for moral theater, but as an actual operating principle. No
moral grandstanding, no trend chasing, no resume padding rewrites whose main
purpose is to signal participation in whatever the current discourse cycle
happens to be. Just good engineering &lt;em&gt;because it is good engineering&lt;/em&gt;. Systems
that are designed to be understood, maintained, and trusted by the people who
have to live with them. We can have this. We have had this. We know exactly what
it looks like when correctness, performance, and restraint are treated as first
class concerns rather than optional polish.&lt;/p&gt;
&lt;p&gt;What changed is not that nobody cares anymore. It is that we have become so
fragmented that care is no longer visible. A lot of people still care, quietly,
competently, and without hashtags. They just do not want to spend their limited
time arguing with the loud group of critters who insist that performance can be
bolted on later, that quality is premature optimization, and that
maintainability is someone else&apos;s problem. &quot;We shipped and that is all that
matters&quot; has become an excuse to stop thinking, as if shipping were the finish
line rather than the point where responsibility actually begins. That mentality
does not just produce bad software, it produces brittle ecosystems that slowly
train everyone involved to accept decay as normal. If craft does not matter,
nothing downstream does either. That idea needs to die. It needs to die for
&lt;em&gt;good&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Okay, I think that&apos;s it for my rant. Let&apos;s wrap it up for today.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;It has been a long and somewhat productive year. I&apos;ve created a lot, contributed
to a lot, and cynically criticized a lot. I do not plan to change. I will
continue to create, contribute and criticize.&lt;/p&gt;
&lt;p&gt;2025 was survivable.&lt;br&gt;
2026 will probably be too.&lt;br&gt;
Software will keep being disappointing.&lt;br&gt;
We&apos;ll keep trying to disappoint it back a little less.&lt;/p&gt;
&lt;p&gt;If things get better, thank the people who still care.&lt;br&gt;
If they get worse... well, blame webdevs. Always blame webdevs.&lt;/p&gt;
&lt;p&gt;I&apos;m looking back with bittersweetness but I cherish a lot of things, and a lot
of... &lt;em&gt;you&lt;/em&gt;. I&apos;ve met a many amazing people over 2025 that I am happy to call
&quot;friends&quot; over this parasocial existence that you call the internet. Know that
you are appreciated my dear reader, and see you next year, or; see you when I&apos;ve
got something worth saying. Have a nice one.&lt;/p&gt;
&lt;p&gt;--- raf&lt;/p&gt;</content:encoded><category>thoughts</category><category>programming</category></item><item><title>The Tradeoff Trap</title><link>https://notashelf.dev/posts/the-tradeoff-trap</link><guid isPermaLink="true">https://notashelf.dev/posts/the-tradeoff-trap</guid><description>When Simplicity Feels Like Surrender</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Not so long ago, I have spent quite a bit of time writing a new component for my
site. It is not really &lt;em&gt;that&lt;/em&gt; fancy. Though suffice it to say, it took more time
than it was worth; the component is written in Rust, compiled to WASM using
&lt;code&gt;wasm-pack&lt;/code&gt; and it&apos;s shoehorned into my Astro stack through a Vite plugin that I
&lt;em&gt;probably&lt;/em&gt; regret adding to my site. It does work, technically, but it&apos;s not
too clean. It&apos;s not even clever. It just... feels heavy. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/the-tradeoff-trap#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Truth be told, I did enjoy the experiment. I have been long fascinated by the
idea behind WASM, and this was a fair excuse to finally give it a shot myself.
This whole ordeal started because I wanted to fix something small: my search
widget. However, I ended up dragging the rest of the codebase into it like a
black hole pulling in nearby stars... as usual. Several components got written,
new abstractions had to be born, and a majority of the codebase changed to
accommodate this experiment. I opened a second terminal window just to keep
track of my regrets.&lt;/p&gt;
&lt;p&gt;Somewhere in the middle of this mess, I remembered when my site was a single
text file. A &lt;em&gt;literal &lt;code&gt;.txt&lt;/code&gt; file&lt;/em&gt; dumped into &lt;code&gt;/var/www/html&lt;/code&gt; and served via
NGINX. No JavaScript, no build step. No decisions whatsoever. It just &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So, how the hell did I end up here?&lt;/p&gt;
&lt;h2&gt;The Itch That Builds Complexity&lt;/h2&gt;
&lt;p&gt;There is this itch that I have. It has me so that simplicity and stagnation
bother me a little. Hell, I can still remember the night I rewrote a single
utility three times. First version: 30 minutes, dead simple, rock solid. Second
version: three hours of reactive garbage and decorators that made the call stack
look like performance art. Third version: a weird hybrid that used a language
feature I had to re-learn every time I saw it. None of them felt right. I hated
all of them. And not in the funny self-deprecating way. I mean actual disgust.
Because none of them satisfied both sides of me: the builder and the tinkerer.&lt;/p&gt;
&lt;p&gt;The itch comes when I write code that is too simple. You probably do too. It&apos;s
that little voice that says: you could be more clever than this. You could
abstract this. You could invent something novel. It’s not about what the code
needs. It’s about what you need: a little shot of novelty, a way to prove that
you still know how to do something slightly absurd. And sure, I tell myself it&apos;s
for maintainers. Future contributors. Elegance.&lt;/p&gt;
&lt;p&gt;But the truth? I do it for myself. For the thrill of wrapping a problem in
something unnecessarily beautiful. To prove that I can.&lt;/p&gt;
&lt;h2&gt;The Joy Tax&lt;/h2&gt;
&lt;p&gt;Truth be told, joy matters. It drives creativity. It is also why I still spend
time programming. However, it comes with a tax. Joy tells you that
layering three patterns on top of each other is fine because you will &lt;em&gt;totally&lt;/em&gt;
clean it up later. Joy tells you that you are learning. Sometimes that&apos;s true.
But more often, it leaves behind a breadcrumb trail of entropy. Small
complications that add up over time until you&apos;re afraid to touch anything
without a full test suite and a stiff drink.&lt;/p&gt;
&lt;p&gt;Simplicity, on the other hand, has a kind of power I have a hard time
describing. If I were to try and visualise it for you, I&apos;d tell you of a puzzle
piece falling into place on the first try. Just in.&lt;/p&gt;
&lt;p&gt;Consider this. A loop. A few conditionals. No clever tricks. No helper macros
that magically know what context you&apos;re in. Just boring code. And I mean that in
the best way. Boring code runs fast. It fits in your head. It does not surprise
you six months later.It might look uninspired but it has its own power.
Simplicity means you can revisit it months later and feel like you understand it
immediately. It means fewer bugs and fewer performance surprises. A plain
solution usually uses less memory and runs faster without requiring a profiler.&lt;/p&gt;
&lt;p&gt;Yet there is this intriguing feeling that accompanies simplicity. It feels
almost like surrender. It feels like admitting the challenge was too difficult
Maybe we fear judgement for writing something that looks
pedestrian. We call it &lt;em&gt;boring&lt;/em&gt; code instead of reliable code. We convince
ourselves that plain means uninspired when in reality it often means practical
and predictable.&lt;/p&gt;
&lt;h2&gt;A Fork in the Road&lt;/h2&gt;
&lt;p&gt;Projects used to come in two broad flavors. At least there are two that are
relevant to us today, but the new third variant is more sinister. Traditionally
there were those two flavors. Anyway, I digress.&lt;/p&gt;
&lt;p&gt;You either get the reliable, boring version that just works and feels like a
spreadsheet with a keyboard. Or you build something expressive and brilliant
that collapses under its own weight the moment your attention shifts. Neither
path is inherently better. What matters is context. A tiny service that
processes thousands of requests per second deserves minimal overhead. Pull in
fewer dependencies and write code that can be understood with a single glance.
In that space, every millisecond counts and every external library is a
liability. You need the plain solution.&lt;/p&gt;
&lt;p&gt;By contrast, a research prototype meant to explore an idea might not care about
raw performance. It &lt;em&gt;might&lt;/em&gt; benefit from dynamic typing or metaprogramming even
if that code will never see production. In that realm the joy of creativity and
the speed of iteration trump long term maintenance. The cost of rewriting from
scratch is acceptable because you will probably scrap it once you learn what
works. The cost of learning something new is far less than the cost of missing new
knowledge.&lt;/p&gt;
&lt;p&gt;The real trap is believing you can have everything. High performance plus
effortless maintainability plus endless enjoyment. Right. There is a narrow
sweet spot in the design space but for most projects it is elusive. You end up
either cutting complexity until nothing interesting remains or stacking layers
until you cannot find the core logic anymore. The only skill worth mastering is
knowing which sacrifice to make at what moment.&lt;/p&gt;
&lt;h2&gt;Chisel and the Sledgehammer&lt;/h2&gt;
&lt;p&gt;Sometimes you need a chisel. You want to carve something that fits perfectly,
piece by piece. Other times, you need a sledgehammer. Bash it together until it
stands and deal with the mess later. There is nothing noble about always
choosing one tool.&lt;/p&gt;
&lt;p&gt;Sometimes the best path forward is plain. Your code will not sparkle, but it
might just hum along quietly without any unpleasant surprises. Alternatively,
the best path forward might just be &lt;em&gt;intricate&lt;/em&gt;. Your code may creak under
pressure but it will teach you things you could never learn with a toy example.
Neither choice diminishes you as a coder. Both are valid expressions of craft.&lt;/p&gt;
&lt;p&gt;So the next time you stand at that crossroads, pause before you reach for the
clever trick. Ask yourself what you truly need from this project. Do you need
speed or delight in playing with language features? Do you need rock solid
uptime or rapid feedback on a research question? Answer that and choose
deliberately. Honor the tradeoff. There is no perfect solution, only the right
compromise for now. When you are racing against a deadline, choose simplicity and
accept the dullness. When you are prototyping a radical idea, choose
sophistication and accept the technical debt. When you are building something
meant to last, choose the combination that yields reliability, even if it does not
inspire a standing ovation from your future self.&lt;/p&gt;
&lt;p&gt;And remember: simplicity or sophistication, they are not sole measures of your
talent. They are tools you wield to shape the experience of building and
maintaining code. Master the tradeoff and you will write software that serves
its purpose without betraying your own values. Know that there is no perfect
answer. There is just the answer that works for this moment, and the honesty to
admit what you&apos;re optimizing for.&lt;/p&gt;
&lt;p&gt;And if the answer is &apos;I just want to mess around and write something cool,&apos; then
you need to own it. Just don&apos;t lie to yourself and call it a proper
architecture. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/the-tradeoff-trap#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;And the performance benefits aren&apos;t even that great! What happened to my
blazingly fast, memory-safe Rust? &lt;a href=&quot;https://notashelf.dev/posts/the-tradeoff-trap#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Believe me when I say that this is self-reflection more than anything. I
have called my own creations &quot;solutions&quot; when they were just experiments.
Sure they &lt;em&gt;did&lt;/em&gt; substitute a solution for the time being, but I should have
known it was not the way to go. &lt;a href=&quot;https://notashelf.dev/posts/the-tradeoff-trap#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>thoughts</category><category>software</category><category>programming</category></item><item><title>I am Not Convinced by Vibe Coding</title><link>https://notashelf.dev/posts/vibe-coding</link><guid isPermaLink="true">https://notashelf.dev/posts/vibe-coding</guid><description>A Critique of Vibe Coding and its False Promise of Effortless Software Development.</description><pubDate>Sun, 03 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There has been some growing current of enthusiasm recently, around so called
vibe &quot;coding.&quot; To those of you unfamiliar with the term, if that is even
possible, vibe coding refers to a software development mode where instead of
writing code directly, developers interact with large language models (LLMs)
using prompts, voice, or vague intent. The tools generate code, and the human
nudges it back and forth by feeling---by vibe, they say---until it works.&lt;/p&gt;
&lt;p&gt;The promise is seductive: anyone can build an app, and you don&apos;t even need to
know programming! Prototypes come together in a weekend. Codebases emerge not
from typing but from suggestion, improvisation, and iteration. The developer
becomes a kind of conductor, gesturing at the machine to summon form from
possibility.&lt;/p&gt;
&lt;p&gt;I am, however, not entirely convinced. Not because I&apos;m a Luddite, or because I&apos;m
resistant to change. I &lt;em&gt;have&lt;/em&gt; used these tools (at least I tried to) and I &lt;em&gt;do&lt;/em&gt;
understand their appeal. What I reject is the framing that this is a sustainable
or revolutionary new paradigm for software development. It&apos;s not. It&apos;s a
generator of noise with occasional signals, and it relies entirely on your
ability to tell the difference. What happens when you &lt;em&gt;can&apos;t&lt;/em&gt; tell the
difference? Have you really accounted for that?&lt;/p&gt;
&lt;p&gt;The assumption that the technology alone solves the entire problem overlooks the
complexity embedded in software development beyond mere code generation.&lt;/p&gt;
&lt;h2&gt;The Machine That Designed Cars&lt;/h2&gt;
&lt;p&gt;There&apos;s a plot point in the book The Dice Man &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; that I keep returning to. At
one point, a team invents a machine that generates car designs entirely at
random. Millions of iterations, nonsensical and broken and unbuildable---but
every once in a while, a design emerges that is striking. Beautiful, even. So
they discard the garbage and hand-pick the good ones.&lt;/p&gt;
&lt;p&gt;That&apos;s what I find vibe coding to be. You prompt an AI a hundred times, throw
away the malformed outputs, and keep the one that works, at least on the
surface. It looks right. It runs. The UI renders. The button clicks. Success,
right?&lt;/p&gt;
&lt;p&gt;I think this misconception, so to speak, comes from the fact that software
programming has fewer win conditions. While in most domains a &quot;win condition&quot; is
some universally recognized state of victory---e.g., in chess, it&apos;s checkmate;
in biology, it&apos;s a Nobel-worthy discovery; in sports, it’s a gold
medal---programming has &lt;em&gt;no universal definition of &quot;done right&quot;&lt;/em&gt;, &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;em&gt;no
standard of elegance that everyone agrees on&lt;/em&gt; and &lt;em&gt;no fixed endpoint&lt;/em&gt;. Hell,
even shipping a product doesn&apos;t mean you &quot;won.&quot; There are always bugs, tech
debt, UX complaints, performance ceilings, etc. Even &quot;perfect code&quot; is usually
replaced or rewritten within a decade.&lt;/p&gt;
&lt;p&gt;As such the internal structure matters. Code is not just visual. It is layered
with behavior, security, data access, side effects, and edge cases. You are not
choosing car sketches for a brochure; you are selecting an actual vehicle that
someone will drive. And if you don&apos;t understand what&apos;s under the hood, you are
gambling with your users&apos; safety. The belief that AI-generated code can be
blindly trusted because it &quot;builds and runs&quot; dismisses these hidden complexities
and the inherent unpredictability of probabilistic models. Treating generated
code as a finished product rather than a draft invites risk.&lt;/p&gt;
&lt;h2&gt;What Vibe Coding Forgets&lt;/h2&gt;
&lt;p&gt;Proponents of vibe coding frame it as a democratizing force. Claiming that you
do &lt;em&gt;not&lt;/em&gt; need to know how it works, and that you can just explain what you want
to the machine. Let the machine handle it. This &lt;em&gt;does&lt;/em&gt; sound empowering to a
degree, but it also breaks down under scrutiny. Truth is, someone will always
have to understand the code. The difference is whether it&apos;s you &lt;em&gt;now&lt;/em&gt;, or an
unfortunate engineer, a curious security researcher or a malicious party down
the road. Biggest question here is, what if someone malicious gets their hands
on your vibe coded SaaS?&lt;/p&gt;
&lt;p&gt;Blindly trusting generated code without review or comprehension is not
engineering, it is nowhere near that. In fact, I think it is something to be
ashamed of. The pride tied to the fact that you do not understand something
is... tragic. I can only describe (most cases of) vibe coding as &quot;aesthetic
selection&quot;. The entire method hinges on your ability to recognize what is good
or most of the time good &lt;em&gt;looking&lt;/em&gt;, but good on what axis? Speed? Readability?
Robustness? Security? These are not things you can judge from a glance. They
take context. They take understanding.&lt;/p&gt;
&lt;p&gt;Relying on AI as a catch-all acceleration tool ignores the essential nature of
software as a system. Speed without comprehension leads to fragility and
technical debt. The supposed gains are only superficial if the foundational
integrity of the codebase is compromised.&lt;/p&gt;
&lt;h2&gt;The Shortcut That Becomes a Detour&lt;/h2&gt;
&lt;p&gt;I&apos;ll be honest; yes, vibe coding &lt;em&gt;does&lt;/em&gt; accelerate prototyping. Yes, it also
lowers the floor for experimentation. But we have to stop pretending that this
comes without trade-offs. You are accumulating technical debt at warp speed. You
are outsourcing logic to a model trained on GitHub, filled with every security
mistake and anti-pattern known to man. What, you thought it was trained on
&lt;em&gt;good&lt;/em&gt; code? Hah.&lt;/p&gt;
&lt;p&gt;Worse, you may not even notice. The very act of vibe coding discourages
investigation. You didn&apos;t write this code. You don&apos;t own it. You don&apos;t feel
responsible for it. If it breaks, you will just prompt again. But that&apos;s not
development. That&apos;s gambling at best. Desperation at worst. Even if you are
someone who understands the principles of programming, which is safer than most
use cases I have witnessed, it still gives you tunnel vision to a degree unless
you had a perfectly outlined plan from the start. Unless you know what &lt;em&gt;exactly&lt;/em&gt;
you were going for, then you are letting a subpar sentence generator decide what
will be happening in your project. Even if you reject its proposals, it will
plant the seeds of an idea in your head.&lt;/p&gt;
&lt;p&gt;This mindset assumes that the code is only a surface-level artifact and ignores
that true software development involves maintaining systems, anticipating edge
cases, and managing complexity. The idea that one can simply &quot;iterate until it
works&quot; neglects the institutional knowledge and discipline required to sustain
software over time.&lt;/p&gt;
&lt;h2&gt;When Vibe Coding Makes Sense&lt;/h2&gt;
&lt;p&gt;I think there is a place for vibe coding. Or rather, AI-assisted programming. I
reject the idea of prompting a sentence generator to have what I can do better
while also enjoying the process. It can be a great tool for demos, quick and
hacky scripts, or even internal tooling if the risk is acceptable. If you are
hacking something together for one-time use or just a presentation, the speed is
hard to beat. Though you must also realize that the domain is very narrow. The
moment your software has users, state, uptime requirements, or a security
surface, you are in a different game. At that point, the vibe must end. You need
understanding. You need rigor. You need systems that are legible, testable, and
maintainable.&lt;/p&gt;
&lt;p&gt;The excitement around these tools often ignores that they require significant
infrastructure: embedding the codebase, hooking into CI pipelines, and layering
tests and linting to catch AI hallucinations. These are not trivial tasks, and
most teams are not equipped to treat AI as a dependable collaborator rather than
a toy.&lt;/p&gt;
&lt;h3&gt;You Cannot Abdicate Responsibility&lt;/h3&gt;
&lt;p&gt;No matter how arcane it looks, software is not magic. It is logic encoded for
machines and maintained by humans. Tools like Copilot, Cursor or models like
Claude and Gemini may be useful but it must be made perfectly clear that their
use case is acting as assistants, not replacements. Using them well requires
clarity, and not vibes.&lt;/p&gt;
&lt;p&gt;What I take issue with in the usage of such tools is that I &lt;strong&gt;do not trust&lt;/strong&gt;
most developers to draw the line clearly. The most common and dare I say natural
response to the task often is something along the lines of &quot;&lt;em&gt;Well I used it for
prototyping and it worked. It can probably handle the production code as well&lt;/em&gt;&quot;
and to me that violates a contract of trust.&lt;/p&gt;
&lt;p&gt;When I use software made by another person, I trust in their moral judgement and
the potential consequences on a personal level. If a software programmer makes a
mistake that causes my home directory to be deleted, they will feel remorse.
They will also aim to never make that mistake again. What about LLMs? It&apos;ll give
you an empty apology, fix its mistake for one time and move on. Will you always
check the code in case it accidentally deletes your user&apos;s home directory again?&lt;/p&gt;
&lt;p&gt;Thus I want to say that we do not need to throw out our understanding to embrace
the future. We need to carry it forward. Celebrate the collective intelligence,
experience and effort. Otherwise, we&apos;re not building; we are merely browsing.
The cost of mistaking one for the other is always paid in production.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;It is not my intention to gatekeep anyone from doing anything. I avoid LLMs by
principle, but that is a personal choice that I do not and cannot enforce on
anyone. It also stems from the fact that I have only had disappointing
experiences with tools like Claude. I tell it to do something, and it makes
outrageous assumptions about my codebase or straight up tries to throw something
away because it collides with its own shallow implementation. Maybe I&apos;m unlucky
though, who knows?&lt;/p&gt;
&lt;p&gt;The point I really wanted to make in this post is that no matter how the app was
coded (but especially if it was &lt;em&gt;vibe&lt;/em&gt; coded) then you have a moral
responsibility to understand it before you ship it. When I see something that
boasts being written by LLMs, I tend to turn away and pretend that the repo does
not exist. Not because I feel a moral superiority by doing so, it was never
about that, but because I am not willing to take the chances of getting caught
by a destructive oversight no human developer would make. After years of
programming, I would like to &lt;em&gt;think&lt;/em&gt; that human programmers inherently target or
at least document production-critical bugs, whereas LLMs choose to put
environment variables in production code. Sure we all know better than that, but
what if you make a mistake you can&apos;t even recognize? Can AI learn from its
mistakes? I don&apos;t think so.&lt;/p&gt;
&lt;p&gt;Lastly there is the part of effort. When I see an art piece that I like, it is
not just about the art itself but also the time spent on the piece. I &lt;em&gt;respect&lt;/em&gt;
the artist as much as I &lt;em&gt;enjoy&lt;/em&gt; their art. If the piece is good, but I &lt;em&gt;know&lt;/em&gt;
the artist has spent no time on the piece, then my respect for the artwork and
thus the level of my enjoyment only goes down. Sure you might have expressed an
idea, but what about the journey? What have you gained out of this except for a
potentially malformed artwork that is the expression of words instead of
feelings? What am I even looking at? There is no beauty here, just lines. Vibe
coding is very similar in principle. Yes most enterprise code is for the sake of
completing a task, and doing so while making or saving the company money but
what about FOSS? What happens to the individuality, to the collaborative
creative wisdom of the people? Deferring inventing to AI is nothing but
reductive. It can &lt;em&gt;never&lt;/em&gt; be anything but reductive. This is not pragmatism. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This has been my miniature rant on vibe coding. Truth be told, I was exposed to
&quot;AI slop&quot; more often than I&apos;d like while in academia, and all it has done to
serve me was to get on my nerves. Waste my time. Sure, don&apos;t think I guess. That
said, I do not reject the idea outright. I believe there is merit in AI-assisted
code reviews or project planning. Not yet though, at least not until the hype
bubble pops. Hope you enjoyed this post. It was more emotional than the rest of
my writing, and I&apos;d like to hear your thoughts. If you think I&apos;ve missed
anything, I&apos;m also open to further discussion.&lt;/p&gt;
&lt;p&gt;I want to leave you with two interesting studies. &lt;a href=&quot;https://advancesinsimulation.biomedcentral.com/articles/10.1186/s41077-025-00350-6&quot;&gt;One of them&lt;/a&gt; is on the
AI-assisted academic writing and recommendations for ethical use. While ethics
are whole new discussion point that I want to avoid, the practicality of AI is
an interesting question. It is a relatively short read, so I recommend that you
give it at least a skim. Especially if you are a student. &lt;a href=&quot;https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/&quot;&gt;The second study&lt;/a&gt; is
on the impact of AI on developers in 2025. It challenges some of the baseless
assumptions usually perpetuated by those that stand to gain from perpetuating
the unseen (but somehow very real) productivity boost of AI in enterprise. If
you would like a real statistical study (that you can pull out next time someone
tells you that &quot;AI makes developers faster, trust me&quot;) then I recommend you take
a look at this one as well. If you have some other interesting articles, please
feel free to contact me to let me know.&lt;/p&gt;
&lt;p&gt;Cheers!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Or maybe its sequel, The Son of Dice Man? It&apos;s been years since I read the
books... &lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;No matter what the grifters on tech Twitter will tell you, there simply
isn&apos;t one. &lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;If you mention driving a car or using printing presses to replace walking
and printing, I will simply defenestrate you. You &lt;em&gt;know&lt;/em&gt; those things are
not equal, and you KNOW that they solve problems of capacity. You cannot
walk a thousand miles, even if you could the time spent would not be worth
it. One man cannot copy a million books, even if he did it would take a
lifetime. One developer, however, solve write one hundred projects. A
thousand. AI only makes it less incentivized to think on those projects. &lt;a href=&quot;https://notashelf.dev/posts/vibe-coding#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>thoughts</category><category>software</category><category>programming</category></item><item><title>The Federation Fallacy</title><link>https://notashelf.dev/posts/federation-fallacy</link><guid isPermaLink="true">https://notashelf.dev/posts/federation-fallacy</guid><description>Thoughts on federated networks and their implications</description><pubDate>Fri, 27 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post is greatly inspired by the awesome article
&lt;a href=&quot;https://rosenzweig.io/blog/the-federation-fallacy.html&quot;&gt;The Federation Fallacy&lt;/a&gt;
by Alyssa Rosenzweig. Please make sure to check her out. Some of the ideas
discussed here today are greatly inspired by the original post, bearing the
same name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Imagine this: the boundary between government control and corporate influence
has all but disappeared. One bleeds into the other so seamlessly that you can&apos;t
quite tell who&apos;s pulling the strings. And here&apos;s the kicker---you&apos;re already in
the middle of it. Every time you unlock your phone, launch an app, or just exist
online, you&apos;re stepping onto a battleground you didn&apos;t really sign up for.&lt;/p&gt;
&lt;p&gt;I missed a great portion of the technological advancements that computers and
the internet went through. I was blissfully offline throughout most of it.
Although now that I have been diving more into OSdev and other programming
concepts, my understanding of it is that technology was supposed to be the great
equalizer. A tool for freedom, connection, progress. Instead, it appears to have
become something else entirely---something much more insidious. Governments no
longer need to strong-arm their citizens into compliance when corporations can
do the dirty work for them. And the best part? Most people, perhaps even you
reading this, don&apos;t even notice. The social contract didn&apos;t just get rewritten;
it got outsourced.&lt;/p&gt;
&lt;p&gt;But here is where things take a turn. The same technology used to monitor,
categorize, and manipulate us also holds the potential to set us free. The
internet, encryption, decentralized platforms—these aren&apos;t just tools, they are
weapons in a fight most people don&apos;t even realize they&apos;re a part of. And that&apos;s
the problem. Too many of us engage with technology passively, as consumers and
not as participants. We think of it as a service, not a structure of power. That
is a monumental mistake. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Take something as basic as data ownership. Who owns your data? The reflexive
answer might be, &quot;I do, obviously.&quot; But think about it for a second. The moment
you tap &quot;Agree&quot; on a terms-of-service agreement you didn&apos;t read, you&apos;re giving
away more than you think. Your habits, your movements, maybe even your
thoughts---logged, analyzed, and monetized by entities that have no obligation
to you in ways that would not occur to you in your wildest dreams. And here is
the brutal truth: once it&apos;s gone, it&apos;s gone. You don&apos;t get to take it back.&lt;/p&gt;
&lt;p&gt;Now, let&apos;s talk about &lt;strong&gt;digital sovereignty&lt;/strong&gt;. Sounds like something ripped from
a cyberpunk novel, but it&apos;s real, and it matters. You might have not heard of it
before, and frankly I have not thought much of it until my research brought me
across several sources that used the term. It is the idea that individuals---and
entire nations---should control their digital presence the way they control
their physical borders. It is about deciding who gets access to your identity,
on what terms, and with what consequences. Yet, somehow, this is is &lt;em&gt;not&lt;/em&gt; a
mainstream political discussion. We act like it is a niche concern, something
for tech nerds to stress about, instead of what it really is---a fundamental
issue of power in the 21st century.&lt;/p&gt;
&lt;h2&gt;Interlude: On Data Ownership&lt;/h2&gt;
&lt;p&gt;While working on this post, I was reminded several times that &lt;em&gt;in some
countries&lt;/em&gt; data ownership is, in fact, a mainstream political discussion. I am
aware of this, and I really admire the level of optimism required to believe
this is somehow the case for an acceptable majority. It isn&apos;t. According to
Freedom House Freedom on the Net 2020 report, over 80% (the actually cited
number is 86%, and I fear it has gotten worse by now) of the world&apos;s population
live in in regimes where the online participation is partially or fully
censored. What is the possibility that most of us own the data when most of us
cannot go online without our every move being recorded?&lt;/p&gt;
&lt;p&gt;Some European countries have been making extended efforts to ensure privacy as a
right, other continents have been regressing steadily. The United States has
granted unlimited access to every citizen&apos;s data to a private citizen while in
Turkiye, the government has decided to form a government department to monitor
and act on online activity with little to no regulation. This kind of immense
censorship is also the case in China, Russia, Saudi Arabia and many more that I
can count. Whatever privacy incentive you might be aware of is &lt;em&gt;not&lt;/em&gt; the norm,
nor is it setting an example for the rest of us. This is quite unfortunate, yet
not at all surprising. One of the many dimensions of a state is to express
&lt;em&gt;power&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Federation Fallacy: Continued&lt;/h2&gt;
&lt;p&gt;Make no mistake, this &lt;em&gt;is&lt;/em&gt; about power. Data is the raw material of modern
power, the foundation upon which influence, control, and wealth are built. Those
who have it shape the world. The problem? Governments and corporations have been
playing this game for decades, and while you are barely of the rules, they have
mastered it. The most average player of the game can &lt;em&gt;barely&lt;/em&gt; remember their own
passwords, let alone having any awareness of the game.&lt;/p&gt;
&lt;p&gt;So what now? What do we do with this knowledge? First, we need to accept that
technological literacy isn&apos;t optional anymore---it is &lt;em&gt;survival&lt;/em&gt;. Second,
resistance doesn&apos;t always mean taking to the streets, or drawing graffiti on
walls. Sometimes, it is as simple as using open-source software, encrypting your
messages, or just stopping for a moment to ask, &quot;&lt;em&gt;Why does it have to be this
way?&lt;/em&gt;&quot; The final step? That&apos;s on you. But let me be very clear: doing nothing is
a choice, and it has consequences. Costly consequences.&lt;/p&gt;
&lt;p&gt;We happen to be at a turning point. The choices we make now--about technology,
governance, and personal autonomy--will shape the next century. The only
question is: will we be the architects of that future? Or just subjects to it?&lt;/p&gt;
&lt;h3&gt;Federation: The False Promise of Decentralization&lt;/h3&gt;
&lt;p&gt;You might have heard of the promise of decentralization, an ideal where the
power and control of the digital world are distributed evenly, away from the
corporate giants and government entities that currently dominate. Many of us in
the open-source and privacy communities dream of this decentralized world. We
envision a future where individuals can host their own servers, own their data,
and share information freely, without intermediaries like Google, Amazon, or
Facebook controlling the flow.&lt;/p&gt;
&lt;p&gt;But the reality of decentralized networks, such as federated platforms like
Mastodon or the once-promising web, exposes a serious flaw in this vision. Sure,
decentralization &lt;em&gt;sounds&lt;/em&gt; like the solution to the problem of monopolistic
control. But let me break it down. Decentralization isn&apos;t as simple as just
opening a server and letting users do whatever they want. If you think about it,
decentralization often becomes a highly technical and impractical solution for
the majority. Most people aren&apos;t system administrators. They don&apos;t want to host
their own email servers, &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; handle constant updates, or manage security
vulnerabilities. And let&apos;s be honest: it is not something most will do. So what
happens? The dream of decentralization becomes a playground for the
technologically &quot;elite,&quot; and the rest of us are left either uninvolved or stuck
relying on others to handle the complexity for us.&lt;/p&gt;
&lt;p&gt;Federation attempts to solve this issue by allowing individuals or groups to
host their own servers while still communicating with others across the network.
Unlike a fully decentralized system where each user is entirely independent, a
federated system connects multiple independent servers that can exchange data
while maintaining local control. The idea behind federated platforms like
Mastodon is that you can join or create independent instances and still interact
with users across the wider network, much like how different email providers
(Gmail, Outlook, etc.) can send messages to each other. This approach avoids the
need for a single central authority while still enabling cross-platform
communication.&lt;/p&gt;
&lt;p&gt;Federation seems like a happy medium between full decentralization and reliance
on centralized corporate platforms. But it comes with its own problems. Despite
the ideal of decentralization, federated systems have shown a tendency to
concentrate power into the hands of a few large instances. While there may be
thousands of Mastodon instances, the majority of users end up clustering around
a few, leaving power and control in the hands of administrators who run these
larger servers. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Take Mastodon, for instance. It promises a decentralized, federated alternative
to X/Twitter, where users are free to join different servers and interact across
a network. But in practice, most people gravitate toward a handful of large
instances, making the system feel centralized. The same problem plagues email,
XMPP, and even the original decentralized web: while the systems themselves are
designed to be federated, the reality is that scale and complexity inevitably
lead to centralization. These federated systems are still subject to the whims
of a few large players, who end up determining the experience for most users.&lt;/p&gt;
&lt;p&gt;So, what does this mean for the dream of decentralization? It means that
federation, despite its merits, often falls short of its ideal. It is an
imperfect middle ground that tries to balance decentralization with
practicality. But as the user base grows, so does the tendency for power to
centralize; just in different ways.&lt;/p&gt;
&lt;p&gt;Perhaps the solution lies not in decentralization for its own sake but in
creating more democratic information spaces. Rather than solely focusing on
breaking free from the power of corporations or governments, we must ask: how
can we create systems where the power is distributed fairly, transparently, and
with participation from all users, not just the elites who can afford to manage
complex systems? Ultimately, decentralization and federation offer only partial
solutions. The real goal should be creating systems that empower individuals
while maintaining the democratic oversight necessary to prevent concentration of
power. The dream of a decentralized future may be far from perfect, but it&apos;s not
impossible. The question is, &lt;em&gt;how do we design it in a way that is inclusive,
scalable, and fair to all?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Even as decentralized networks and cryptographic tools emerge as potential
solutions, they strike me as far from perfect. The very technologies that
promise to free us from corporate and governmental oversight are also fraught
with their own limitations and vulnerabilities. Decentralization, for instance,
sounds like &lt;em&gt;the&lt;/em&gt; answer to control over one&apos;s data, yet it introduces new
risks--fragmentation, inconsistent security protocols, and the lack of cohesive
infrastructure. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; As much as we crave autonomy, it&apos;s difficult to ignore that
true digital sovereignty may require us torely on infrastructure that is,
ironically, still controlled by large tech entities. Peer-to-peer networks and
decentralized platforms may offer us a glimmer of freedom, but without mass
adoption, scalability, and seamless user experience, they remain niche
alternatives.&lt;/p&gt;
&lt;p&gt;This creates a paradox: the tools that could allow individuals to reclaim their
autonomy are often too difficult or inaccessible for the average user. The
notion of &quot;sovereignty&quot; in the digital world, much like its political
counterpart, is one of constant negotiation. Governments are learning how to
integrate blockchain technologies for more efficient control, while corporations
leverage surveillance capitalism to expand their grasp on user behavior.
Ironically, the very same technology that could empower individuals is becoming
a tool for governance and control, albeit one that is harder to trace and
combat. The question we need to grapple with is whether true decentralization is
even achievable in a society so deeply embedded in corporate capitalism and
surveillance mechanisms. At the same time, can we ever truly achieve the
autonomy we crave, or will the future of digital space always be a game of
tug-of-war, where the strings are pulled by invisible powers far beyond our
control?&lt;/p&gt;
&lt;p&gt;In a way, we are all participating in this grand experiment---sometimes as
innovators, sometimes as victims---and our level of awareness determines how
much influence we have over our own digital future. The question isn&apos;t just
about how we can protect ourselves, but about whether we can transform the
system from within. We may be able to opt-out of certain platforms, but as
digital life becomes more entrenched, can we afford to truly escape the
architecture that shapes us? The choices we make today in how we interact with
technology will set the precedent for how future generations experience their
own agency and sovereignty in an increasingly connected, monitored, and
data-driven world.&lt;/p&gt;
&lt;p&gt;This is all I have for today. I think this post is already very long (and &lt;em&gt;much&lt;/em&gt;
longer than I have initially intended for it to be), but I&apos;d love to dive deeper
into something else that&apos;s been on my mind recently: the global reliance on CDNs
like Cloudflare. It&apos;s wild when you think about how much of the internet today
is essentially built on the backs of these centralized systems. Cloudflare and
similar services are everywhere, speeding up websites, making them &quot;more
secure,&quot; and keeping them online. The catch? As we depend more on them, we&apos;re
putting a lot of trust in a handful of companies that control a massive portion
of the web&apos;s infrastructure. This brings up some huge questions about power and
control. What happens when a company like Cloudflare decides to pull the plug on
a website---or worse, if it&apos;s compromised in a way that takes down whole
sections of the internet? It&apos;s one thing to have content spread across servers
worldwide, but it&apos;s another when those servers are owned by a handful of
corporations. It creates single points of failure and makes the internet feel
more like a corporate-controlled ecosystem rather than a decentralized space for
free expression. There&apos;s also the reality that these services often make the
calls about what&apos;s acceptable or not, sometimes with little transparency or
accountability. We&apos;ve seen this with Cloudflare&apos;s past deplatforming decisions, &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;
which leave us asking, &lt;em&gt;&quot;Who gets to decide who stays online and who doesn&apos;t?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Sorry, I&apos;m rambling again. I hope I was able to pique your interest today and
perhaps offer a different perspective. There are many interesting subtopics I
could cover, but I would like to avoid overwhelming you with a wave of
unfiltered thoughts. Special thanks to &lt;a href=&quot;https://frzn.dev/~amr/&quot;&gt;@mraureliusr&lt;/a&gt;
for the initial proofreading, and of course, thank you for reading this. Cheers!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Perhaps an inconsequential one. How many of us care about the consequences
of our data being shared freely? &lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Bad example, I admit. Self-hosting e-mail servers is notoriously annoying,
but this artificial difficulty is also a testament to the problem at hand.
Why must hosting something as simple as e-mail be this difficult? Why will
spam-lists immediately put you on their blocklist? Can we not have better
solutions? &lt;em&gt;Why does most of our technology suck?&lt;/em&gt; &lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Years ago, a friend of mine shared a theory with me about humanity&apos;s
tendency to cluster together. He argued that shared interests, appearances,
or ideas naturally bring us together. According to him, this drive leads
people to form groups that reinforce these commonalities. Over time, these
groups become more insular, focused on mutual validation and a narrower set
of values. Looking back, his theory seems particularly evident in tech
communities. These spaces form around shared goals or ideologies, often
growing into large instances where members develop a sense of identity and
belonging. Yet, as these groups grow, they can transform into echo
chambers--spaces where the same ideas are constantly amplified. Eventually,
these communities may &quot;defederate,&quot; or be &quot;defederate_d_&quot;, through either
external pressure or internal fracture, reinforcing their separation from
broader networks in an effort to preserve their core beliefs. &lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;I will avoid picking the low hanging fruit that is Matrix and its clients.
Make whatever you will from this footnote. &lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Cloudflare has made notable deplatforming decisions that have sparked
debates about free speech, censorship, and the power of private companies
over the internet. One significant incident occurred in 2017 when Cloudflare
terminated its services to The Daily Stormer, a neo-Nazi website. This
decision followed the site&apos;s claims that Cloudflare secretly supported their
ideology, prompting Cloudflare&apos;s CEO, Matthew Prince, to take action. In
2019, Cloudflare ceased services to 8chan, an imageboard linked to multiple
mass shootings, including the tragic event in El Paso, Texas. Prince
described 8chan as a &quot;cesspool of hate&quot; and cited its role in inspiring
tragic events as the reason for the decision. &lt;a href=&quot;https://notashelf.dev/posts/federation-fallacy#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>federation</category><category>software</category><category>thoughts</category></item><item><title>On Mutation Testing</title><link>https://notashelf.dev/posts/on-mutation-testing</link><guid isPermaLink="true">https://notashelf.dev/posts/on-mutation-testing</guid><description>Do You Have A Moment To Talk About Your Codebase&apos;s Extended Warranty?
</description><pubDate>Fri, 06 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you have ever worked on any serious codebase, then you probably know how
truly deceptive confidence can be; you push a change, tests pass, CI is all
green and the voice in your head yells &quot;&lt;strong&gt;MERGE ON GREEN!&lt;/strong&gt;&quot; Then regressions
show up two days later in production. At some point, every experienced developer
comes to the same realization: coverage is not correctness.&lt;/p&gt;
&lt;p&gt;That is where mutation testing becomes a quiet revolution. It doesn&apos;t just
measure code that&apos;s been run, it asks, does your test suite care if this logic
is wrong?&lt;/p&gt;
&lt;h2&gt;A Practical Example&lt;/h2&gt;
&lt;p&gt;Imagine, for a moment, writing a unit test for a function that calculates
discounts. It &lt;em&gt;passes&lt;/em&gt;, because the discount is 10% and the test expects 10%.
But what if the function always returns 10%, regardless of input? What if you
accidentally hardcoded it and never noticed? &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; Traditional coverage wouldn&apos;t
say a word. It sees the function ran, so it&apos;s satisfied. Mutation testing
wouldn&apos;t let that slide, no sir. It would change the discount from 10% to 20%,
rerun the test, and when it passes anyway, it would call your bluff.&lt;/p&gt;
&lt;p&gt;This is not a hypothetical. Mutation testing catches exactly these kinds of
failures: logic that appears tested but is not meaningfully verified. Silent
bugs. Assumptions that you didn&apos;t realize your tests were making. All the things
that lead to 2AM incident reports and embarrassing root cause writeups, or maybe
an apology to your co-maintainers...&lt;/p&gt;
&lt;p&gt;And once you start using it, your relationship with testing changes. You stop
writing tests to satisfy metrics. You stop writing tests for the CI badge. You
start writing tests with intent because you know they&apos;ll be interrogated. This
does something subtle, but powerful: it aligns testing with reality. Code is
messy, deadlines exist, teams rotate, and not every dev has full context.
Mutation testing cuts through that by asking the only question that matters: if
this breaks, will anyone notice? The answer is &quot;no&quot; more often than it is &quot;yes,&quot;
because the noticing part usually comes when something &lt;em&gt;does&lt;/em&gt; break.&lt;/p&gt;
&lt;p&gt;When you start to see mutants surviving---mutants that change core logic, edge
cases, or conditional flows---it stings, it becomes a scratch that you &lt;em&gt;must&lt;/em&gt;
scratch. But it also shows you exactly where your testing assumptions were too
generous. That makes mutation testing a better code reviewer than most humans.
It doesn&apos;t praise you for structure. It doesn&apos;t care if your mocks are elegant.
It just says: this logic was wrong, and nobody noticed. It might even add a
subtle &quot;fuck you&quot; sometimes.&lt;/p&gt;
&lt;p&gt;Of course, you don&apos;t run this on every commit. Mutation testing is heavy. Too
heavy, especially for languages like Rust. You run it during quiet phases. You
target it at critical modules. You use it to audit tests after big refactors.
And over time, you build up a sense for the kind of brittle logic that needs
better test discipline.&lt;/p&gt;
&lt;p&gt;In some ways, mutation testing is the opposite of test-driven development. TDD
is aspirational. You write tests for the code you intend to write. Mutation
testing is cynical. It assumes your code is garbage, &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; and dares your tests
to prove otherwise.&lt;/p&gt;
&lt;p&gt;That tension is useful. Developers often lean too far in one direction: either
overly optimistic about what their tests catch, or so cynical they abandon
testing altogether. Mutation testing grounds both extremes. It doesn&apos;t require
faith. It just produces data. Data that tells you, without ceremony, what
would&apos;ve slipped through. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;When you are building something that matters, however... Perhaps an API others
depend on, an authentication system, anything stateful or money-related. This is
the kind of tool you&apos;ll wish you&apos;d introduced earlier. You simply cannot afford
the fluff, you MUST test.&lt;/p&gt;
&lt;h2&gt;Postscript&lt;/h2&gt;
&lt;p&gt;If you have never used mutation testing, don&apos;t start with the whole codebase.
Instead, pick one module you think is &quot;well tested&quot; and run a mutation tool on
it. Watch how many mutants survive. That first run will teach you more about
your tests than hours of code review ever could.&lt;/p&gt;
&lt;p&gt;And if nothing survives? Congratulations. Your tests actually work. That means
something. Give yourself a pat on the back.&lt;/p&gt;
&lt;p&gt;Not because a metric says so, but because you tried to break your code &lt;em&gt;and it
held its ground&lt;/em&gt;. Congratulations.&lt;/p&gt;
&lt;h3&gt;Mutation Testing in Rust: cargo-mutants&lt;/h3&gt;
&lt;p&gt;Rust is uniquely positioned to benefit from mutation testing, and
&lt;a href=&quot;https://mutants.rs&quot;&gt;cargo-mutants&lt;/a&gt; is the tool that makes it possible. It&apos;s small, sharp, and
well-informed just like the language itself. It works with Rust&apos;s compilation
model, not against it. It generates mutants at the source level, one mutation
per run, compiles them, and executes the tests---all while preserving the
surrounding structure. It doesn&apos;t really try to be clever about parsing Rust. It
uses the compiler.&lt;/p&gt;
&lt;p&gt;This is important because Rust&apos;s strict type system and ownership rules make
traditional mutation tools (often written for dynamic or loosely typed
languages) struggle to produce meaningful changes. Most random mutations won&apos;t
even compile. cargo-mutants appears to be understanding this, so it carefully
selects mutations that produce valid, buildable code. That level of respect for
the language is rare in tooling. It also integrates tightly with the way Rust
projects are structured. It knows about your &lt;code&gt;Cargo.toml&lt;/code&gt;, about test targets,
about workspace layouts. It even detects and reports untested public items,
things that your code exposes but no test ever touches. That alone is worth the
install.&lt;/p&gt;
&lt;p&gt;The result is a tool that doesn&apos;t just churn through changes for the sake of
metrics. It highlights real testing blind spots in real-world Rust code. And it
does so without you having to leave the comfort of cargo. What&apos;s &lt;em&gt;especially
admirable&lt;/em&gt; about cargo-mutants is its humility. It doesn&apos;t overpromise. It
doesn&apos;t pretend mutation testing is a silver bullet. It just methodically shows
you where your test suite is asleep at the wheel, and in doing so, quietly
encourages better code, not just better coverage.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;I discovered mutation testing today almost by accident. I was reading
documentation for something else entirely. And like most good tools, it
immediately reframed how I think about code quality. It&apos;s rare that a testing
technique feels like a missing piece, but this one does. Not because it&apos;s
complex, but because it asks the question we should&apos;ve been asking all along:
what if this code breaks, will my tests even notice? Mutation testing does not
offer comfort. It offers clarity. And in a field full of false confidence and
cargo cult coverage, that&apos;s exactly what I didn&apos;t know I needed.&lt;/p&gt;
&lt;p&gt;As a final note, I would like to remind you that mutation testing &lt;em&gt;is&lt;/em&gt; heavy.
With cargo-mutants, it has taken me around an hour to test around 2000 lines of
code. Though my hardware is not the best, I can&apos;t imagine having to spend around
an hour every time I have to evaluate my codebase for the possibility of
regressions. It is critical that we learn to evaluate the codebase mentally as
if mutation tests will be ran on it &lt;em&gt;anyway&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Cheers!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;This is a hypothetical example, but it is &lt;em&gt;exactly&lt;/em&gt; what I have done. I
got too preoccupied with investigating a different part of the codebase and
with writing tests, and forgot about un-hardcoding my dummy test values. The
result was meaningless tests. Yikes. &lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;How often is it not, lol. &lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;And maybe that&apos;s what makes mutation testing hard to sell. It doesn&apos;t
flatter you. It shows you the difference between looking tested and being
tested. Between surface coverage and real confidence. &lt;a href=&quot;https://notashelf.dev/posts/on-mutation-testing#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>thoughts</category><category>programming</category><category>software</category></item><item><title>The Curse of Knowing How, or; Fixing Everything</title><link>https://notashelf.dev/posts/curse-of-knowing</link><guid isPermaLink="true">https://notashelf.dev/posts/curse-of-knowing</guid><description>A reflection on control, burnout, and the strange weight of technical fluency.</description><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Banner from &quot;@components/Banner.astro&quot;;&lt;/p&gt;
&lt;p&gt;It starts innocently.&lt;/p&gt;
&lt;p&gt;You rename a batch of files with a ten-line Python script, or you alias a common
&lt;code&gt;git&lt;/code&gt; command to shave off two keystrokes. Maybe you build a small shell
function to format JSON from the clipboard.&lt;/p&gt;
&lt;p&gt;You&apos;re not even trying to be clever. You&apos;re just solving tiny problems. Making
the machine do what it should have done in the first place. And then something
happens. You cross a &lt;em&gt;threshold&lt;/em&gt;. You look at your tools, your environment, your
operating system---even your editor---and suddenly &lt;strong&gt;everything&lt;/strong&gt; is fair game.&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; rebuild that (if you wanted to).&lt;br&gt;
You could &lt;em&gt;improve&lt;/em&gt; that (if you wanted to).&lt;/p&gt;
&lt;p&gt;Then someone challenges you. As banter maybe, perhaps jokingly but also with a
dash of hope. Then the air in the room changes.&lt;/p&gt;
&lt;p&gt;It suddenly becomes something else. It becomes:&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;should&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;And from that moment forward, the world is broken in new and specific ways that
only &lt;em&gt;you&lt;/em&gt; can see.&lt;/p&gt;
&lt;h2&gt;Technical Capability as a Moral Weight&lt;/h2&gt;
&lt;p&gt;Before I could program, broken software was frustrating but ignorable. For years
I&apos;ve simply &quot;used&quot; a computer, as a consumer. I was what companies were
concerned with tricking into buying their products, or subscribing to their
services. Not the technical geek that they prefer to avoid with their software
releases, or banning from their games based on an OS.&lt;/p&gt;
&lt;p&gt;Now it has become &lt;em&gt;provocative&lt;/em&gt;. I can see the patterns that I wish I couldn&apos;t,
find oversights that I can attribute to a certain understanding (or the lack
thereof) of a certain concept and I can &lt;em&gt;hear&lt;/em&gt; what has been echoing in the head
of the computer illiterate person who conjured the program I have to debug.&lt;/p&gt;
&lt;p&gt;I notice flaws like a good surgeon notices a limp.&lt;br&gt;
Why the &lt;em&gt;hell&lt;/em&gt; does this site send ten megabytes of JavaScript for a static
page?&lt;br&gt;
Why is the CLI output not parseable by &lt;code&gt;awk&lt;/code&gt;?&lt;br&gt;
Why is this config hardcoded when it could be declarative?&lt;/p&gt;
&lt;p&gt;Those things are &lt;em&gt;not&lt;/em&gt; just questions, they are &lt;em&gt;accusations&lt;/em&gt;. And,
unfortunately, they do not stop.&lt;/p&gt;
&lt;p&gt;Now that I&apos;ve learned to notice, my perception of software has changed in its
entirety.&lt;/p&gt;
&lt;p&gt;Every piece of software becomes a TODO list.&lt;br&gt;
Every system becomes a scaffolding for a better one.&lt;br&gt;
Every inconvenience becomes an indictment of inaction.&lt;/p&gt;
&lt;h2&gt;One Must Imagine Sisyphus Happy&lt;/h2&gt;
&lt;p&gt;Like Camus&apos; Sisyphus, we are condemned to push the boulder of our own systems
uphill---one fix, one refactor, one script at a time. But unlike the story of
Sisyphus, the curse is not placed onto you by some god. We built the boulder
ourselves. And we keep polishing it on the way up.&lt;/p&gt;
&lt;p&gt;I&apos;ve lost count of how many projects I have started that began with some
variation of &quot;Yeah, I could build this &lt;em&gt;but better&lt;/em&gt;.&quot;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A static site generator because the existing ones had too many opinions.&lt;/li&gt;
&lt;li&gt;A note-taking tool because I didn&apos;t like the way others structured metadata.&lt;/li&gt;
&lt;li&gt;A CLI task runner because Make is cryptic and Taskfile is YAML hell.&lt;/li&gt;
&lt;li&gt;A personal wiki engine in Rust, then in Go, then in Nim, then back to
Markdown.&lt;/li&gt;
&lt;li&gt;A homelab dashboard because I don&apos;t like webslop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The list continues, and trust me it &lt;em&gt;does&lt;/em&gt; continue. My dev directory, as it
stands, is nearing 30 gigabytes.&lt;/p&gt;
&lt;p&gt;If you ask me, I was solving real, innocent problems. But in hindsight, I was
also feeding something else: a compulsion to assert control. Every new tool I
built was a sandbox I &lt;em&gt;owned&lt;/em&gt;: No weird bugs. No legacy constraints. No
decisions I didn&apos;t entirely agree with. Until, of course, I became the legacy.&lt;/p&gt;
&lt;p&gt;Kafka once wrote that &quot;&lt;strong&gt;a cage went in search of a bird&lt;/strong&gt;.&quot; &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; That is what
these projects can become. Empty systems we keep building, waiting for purpose,
for clarity, for... salvation? I&apos;m not sure how else would you call this
pursuit.&lt;/p&gt;
&lt;h2&gt;Entropy Is Undefeated&lt;/h2&gt;
&lt;p&gt;Now let&apos;s go back. Back to when we didn&apos;t know better.&lt;/p&gt;
&lt;p&gt;Software doesn&apos;t stay solved. Every solution you write starts to rot the moment
it exists. Not now, not later, but eventually. Libraries deprecate. APIs change.
Performance regressions creep in. Your once-perfect tool breaks silently because
&lt;code&gt;libfoo.so&lt;/code&gt; is now &lt;code&gt;libfoo.so.2&lt;/code&gt;. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;have&lt;/em&gt; had scripts silently fail because a website changed its HTML layout.&lt;br&gt;
I &lt;em&gt;have&lt;/em&gt; had configuration formats break because of upstream version bumps.&lt;br&gt;
I &lt;em&gt;have&lt;/em&gt; had Docker containers die because Alpine Linux rotated a mirror URL.&lt;/p&gt;
&lt;p&gt;In each case, the immediate emotional response was not just inconvenience but
something that moreso resembles &lt;em&gt;guilt&lt;/em&gt;. I built this, and I do know better. How
could I not have foreseen this? Time to fix it.&lt;/p&gt;
&lt;p&gt;If you replace every part of the system over time, is it still the same tool?
Does it still serve the same purpose? Do &lt;em&gt;you&lt;/em&gt;?&lt;/p&gt;
&lt;h2&gt;The Illusion of Finality&lt;/h2&gt;
&lt;p&gt;I think we lie to ourselves.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;If I just get this setup right, I&apos;ll never have to touch it again.&quot;&lt;br&gt;
&quot;If I just write this one tool, my workflow will be seamless.&quot;&lt;br&gt;
&quot;If I automate this, I&apos;ll save time forever.&quot; &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;br&gt;
&quot;Write once, run everywhere.&quot; My ass.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is, I admit, a seductive lie. It frames programming as a conquest of sorts. A
series of battles you win, or challenges you complete. But the imaginary war
never ends. You don&apos;t build a castle. You dig trenches. And they flood every
time it rains. The trials are &lt;em&gt;never&lt;/em&gt; complete.&lt;/p&gt;
&lt;h2&gt;Technical Work as Emotional Regulation&lt;/h2&gt;
&lt;p&gt;On the theme of filling this post with literary references, let me quote the
Stoic Marcus Aurelius.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You have power over your mind--not outside events. Realize this, and you will
find strength.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But programming lures us into believing we &lt;em&gt;can&lt;/em&gt; control the outside events.
That is where the suffering begins. There is something deeper happening here.
This is &lt;em&gt;not&lt;/em&gt; just about software.&lt;/p&gt;
&lt;p&gt;I believe sometimes building things is how we self-soothe. We write a new tool
or a script because we are in a desperate need for a small victory. We write a
new tool because we are overwhelmed. Refactor it, not because the code is messy,
but your life is. We chase the perfect system because it gives us something to
hold onto when everything else is spinning. This is the lesson I&apos;ve taken from
using &lt;a href=&quot;https://nixos.org&quot;&gt;NixOS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have written entire applications just to avoid thinking about why I was
unhappy. Programming gives you instant feedback. You run the thing, and it
works. Or it &lt;em&gt;doesn&apos;t&lt;/em&gt;, and you fix it. Either way, you&apos;re &lt;em&gt;doing something&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;That kind of agency is addictive. Especially when the rest of life doesn&apos;t offer
it. We program because we &lt;em&gt;can&lt;/em&gt;, even when we shouldn&apos;t. Because at least it
gives us something to rebel against.&lt;/p&gt;
&lt;h2&gt;The Burnout You Don&apos;t See Coming&lt;/h2&gt;
&lt;p&gt;Burnout does not just come from overwork. It comes from &lt;em&gt;overresponsibility&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;And programming, once internalized deeply enough, makes everything feel like
your responsibility. The bloated website. The inefficient script. The clunky
onboarding process at your job. You &lt;em&gt;could&lt;/em&gt; fix it. So why aren&apos;t you?&lt;/p&gt;
&lt;p&gt;The truth you are very well aware of is that you can&apos;t fix it all. You &lt;em&gt;know&lt;/em&gt;
this, you always knew it regardless of your level of skill. But try telling that
to the part of your brain that sees every inefficiency as a moral failing.&lt;/p&gt;
&lt;p&gt;Nietzsche warned of gazing too long into the abyss. But he did &lt;em&gt;not&lt;/em&gt; warn what
happens when the abyss is a &lt;code&gt;Makefile&lt;/code&gt; or a 30k line of code Typescript project.&lt;/p&gt;
&lt;h2&gt;Learning to Let Go&lt;/h2&gt;
&lt;p&gt;So where is the exit? Is this akin to Sartre&apos;s depiction of hell, where hell
&lt;em&gt;is&lt;/em&gt; other people and how they interact with your software? Or is it some weird
backwards hell where people create software that you have to interact with?&lt;/p&gt;
&lt;p&gt;The first step is recognizing that &lt;em&gt;not everything broken is yours to fix&lt;/em&gt;.&lt;br&gt;
Not every tool needs replacing.&lt;br&gt;
Not every bad experience is a call to action.&lt;/p&gt;
&lt;p&gt;Sometimes, it&apos;s OK to just &lt;em&gt;use&lt;/em&gt; the thing. Sometimes it&apos;s enough to know &lt;em&gt;why&lt;/em&gt;
it&apos;s broken---even if you don&apos;t fix it. Sometimes the most disciplined thing you
can do is &lt;em&gt;walk away&lt;/em&gt; from the problem you know how to solve. There&apos;s a kind of
strength in that.&lt;/p&gt;
&lt;p&gt;Not apathy, no. Nor laziness. Just... some restraint.&lt;/p&gt;
&lt;h2&gt;A New Kind of Skill&lt;/h2&gt;
&lt;p&gt;What if the real skill isn&apos;t technical mastery? Or better yet what if it&apos;s
emotional clarity?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Knowing which problems are worth your energy.&lt;/li&gt;
&lt;li&gt;Knowing which projects are worth maintaining.&lt;/li&gt;
&lt;li&gt;Knowing when you&apos;re building to help—and when you&apos;re building to cope.&lt;/li&gt;
&lt;li&gt;Knowing when to stop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what I&apos;m trying to learn now. After the excitement. After the obsession.
After the burnout. I&apos;m trying to let things stay a little broken. Because I&apos;ve
realized I don&apos;t want to &lt;em&gt;fix everything&lt;/em&gt;. I just want to feel OK in a world
that often isn&apos;t. I can fix something, but not everything.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;You learn how to program. You learn how to fix things. But the hardest thing
you&apos;ll ever learn is when to &lt;em&gt;leave them broken&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;And maybe that&apos;s the most human skill of all.&lt;/p&gt;
&lt;h2&gt;Post-Mortem&lt;/h2&gt;
&lt;p&gt;As of 6th of May, this post &lt;a href=&quot;https://news.ycombinator.com/item?id=43902212&quot;&gt;has blown up on Hackernews&lt;/a&gt;.
First of all, thank you all for your support and the kind words that I have received on various platforms. This post
was not written with any other intention than getting some things off my chest and off my mind, but it was humbling
to see that it resonated with &lt;em&gt;many&lt;/em&gt; different people of various origins. What surprised me the most was that it seems
to resonate people outside the field of tech as well, so it seems the problem is not as exclusive to something as
&quot;arcane&quot; as programming. Thank you also to those who were kind enough to point out small technical flaws in the site
that I now feel compelled to fix, and those who pointed out small typos I&apos;ve forgotten to push after storyboarding.&lt;/p&gt;
&lt;p&gt;It was also quite valuable in the sense that I was able to see my webserver configuration is &lt;em&gt;in fact&lt;/em&gt; able to handle
lots of traffic. There was no downtime aside from a small hiccup after I messed up a configuration option while trying
to relax my ratelimits to give the influx of readers a nicer experience.&lt;/p&gt;
&lt;p&gt;Lastly, thank you all that left their insight about the post itself (my writing, etc.) as comments on Hackernews or
lobste.rs. I have no doubt missed some, but know that it matters a lot to me. Of course, feel free to contact me directly
via e-mail (or any other method you prefer as detailed in &lt;a href=&quot;/about&quot;&gt;the about page&lt;/a&gt;) to let me know what could be better.&lt;/p&gt;
&lt;p&gt;Thank you, and all the best!&lt;/p&gt;
&lt;p&gt;--raf&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;From, I believe, Kafka&apos;s &lt;em&gt;The Zurau Aphorisms&lt;/em&gt;. &lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Nix solves this. Or does it? Nix was a can of worms of its own. &lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Remember when you spent 2 hours automating a 30 minute task? Yeah, it&apos;s
that again. &lt;a href=&quot;https://notashelf.dev/posts/curse-of-knowing#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>thoughts</category><category>programming</category><category>software</category></item><item><title>Considerations</title><link>https://notashelf.dev/posts/considerations</link><guid isPermaLink="true">https://notashelf.dev/posts/considerations</guid><description>Yet another heart-to-heart for no reason other than to empty my head</description><pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;I am a sick man... I am a spiteful man. I am an unattractive man. I believe my
liver is diseased. However, I know nothing at all about my disease, and do not
know for certain what ails me. I don’t consult a doctor for it, and never
have, though I have a respect for medicine and doctors. Besides, I am
extremely superstitious, sufficiently so to respect medicine, anyway (I am
well-educated enough not to be superstitious, but I am superstitious). No, I
refuse to consult a doctor out of spite. That you probably will not
understand. Well, I understand it, though. Of course, I can’t explain who it
is precisely that I am mortifying in this case by my spite: I am perfectly
well aware that by all this I am only injuring myself and no one else. But
still, if I don’t consult a doctor, it is out of spite. My liver is bad;
well—let it get worse!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first page of Dostoyevsky&apos;s &lt;em&gt;Notes From Underground&lt;/em&gt; has always been
fascinating to me. Not because I fully relate to this sick, spiteful,
unattractive man, but because I share a sentiment with him: spite. I have always
carried it with me, as long as I have been self-aware. I do things when I&apos;m
spiteful; I do things &lt;em&gt;because&lt;/em&gt; I am spiteful, and it usually becomes a strange
mixture of emotions, actions, and sentiments that gets me results.&lt;/p&gt;
&lt;p&gt;I am in pain. I have been for as long as I can remember. My leg hurts, my hand
hurts, and now my neck hurts. Age? Sports injury? Your guess is as good as mine.
Yet despite all that, in spite of the pain, I prevail. I walk, I run, I lift, I
write, I draw, and many more. It hurts? Sure, let it get worse.&lt;/p&gt;
&lt;p&gt;One of the first things I have learned was trust---or rather, mistrust---shortly
after spite. It was fairly obvious. Simply figuring out ways I can do things
myself is more cost effective. Simply put, I don&apos;t trust. Not anyone but myself.
Sure, I will share responsibilities &lt;em&gt;some of the time&lt;/em&gt; if I deem the cost
insignificant, but when there are stakes that matter, then I am alone. Which is
usually always. Surrounded by people whom I love, but alone, nevertheless.
Spiteful, in pain, and alone.&lt;/p&gt;
&lt;p&gt;Despite all of that, I prevail. I am successful, I have accomplished things that
people my age have not even dreamed of, and I should probably be proud of this.
I&apos;m not. I feel disappointed, but not unhappy. Never unhappy. How I feel cannot
be described as unhappy, or depressed, or anything of that sort. Neither happy
nor unhappy. Disappointed, at best. Today was a normal day, nothing out of the
ordinary. Yet I feel overwhelmed by disappointment more than ever. Acceptance,
perhaps, of the fact that it is meaningless. Disappointed that knowing how
things will end up in 5 years, 10, pushing onwards as if there is something on
the horizon. Does it matter? I don&apos;t know, but I&apos;m not particularly sure if I
care either. I want to give up, with my whole being. I have never been more sure
of something. There is not a doubt in my mind that I want to leave everything
behind. And go where? I don&apos;t know. I don&apos;t care. Yet I know that even if I did
that, even if I left everything and everyone behind, the ideas and memories
would continue to haunt me as if nothing happened. Such a cost for nothing in
return.&lt;/p&gt;
&lt;p&gt;There are many things I have invested in. Not in a way that calls for sunk
costs; I invested willingly, and I don&apos;t regret any of it. Not one bit. It is
not like my investments were for nothing either. I have invested my time, my
efforts, and have gained trust in return. Yet, despite it being my choice in the
first place, I feel powerless to betray the trust people have put in me, a trust
I have never shown them. A little funny, perhaps narcissistic. I should be
happy, right? That I was able to gain the trust of people who value me as a
friend, a mentor, or whatever I may be to them. But I don&apos;t feel happy. I feel
trapped. The idea of betraying this trust becomes my worst nightmare. If I were
to describe this, I would ask you to imagine gasping for air in between choking
on water while you are drowning. You know what&apos;s going to happen. You know
what&apos;s next. Yet every single cell in your body &lt;em&gt;screams&lt;/em&gt; and &lt;em&gt;pushes&lt;/em&gt; for one
more moment of survival. Every nanosecond becomes minutes until it ends.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My life was even then gloomy, ill-regulated, and as solitary as that of a
savage. I made friends with no one and positively avoided talking, and buried
myself more and more in my hole.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I enjoy solitude, possibly more than anything else. More than the company of
people I love. It is liberating---not subconsciously overanalyzing every
expression or making small predictions based on clues within a conversation—not
something I do willingly. In fact, for a while now, my dream has been to get my
things and live in the middle of nowhere with not a single soul around. Not one.
I do not care for a warm greeting, I do not care for any appreciation. If we are
going to coexist, let us do so quietly and peacefully. There is not much I can
ask for. Yet it is not what is going to make me not feel whatever this feeling
is called. I find myself yearning for something. It is not solitude, nor
company. It is not some kind of big win that will lift my spirits, and it is not
salvation. I want an end. It is beginning to feel awfully like a boring movie
that I have seen, and I am &lt;em&gt;dying&lt;/em&gt; for it to end. In fact, I spite every second
of it. In fact, I sometimes think I thrive under discomfort. My spite for it
makes me take &lt;em&gt;one more step&lt;/em&gt;, just to spite some imaginary enemy. Laughable,
isn&apos;t it?&lt;/p&gt;
&lt;p&gt;I would be remiss, however, if I did not point out the irony of it all. This is
all of my own making. I chose to want the things I wanted, do the things I did.
I built a prison, got inside, and locked myself in. I could get out whenever I
wanted to, but I don’t want to be wanting to escape. I want to know that I can
simply curl up somewhere and rot away with everything moving on around me. I
want it so that I can simply &lt;em&gt;give up&lt;/em&gt; without the consequences nagging me. Let
me stop grasping for air, and I will be at peace. Though chances are, nothing is
going to change. I am full of spite, regardless of how I feel at any given
moment, and it&apos;s not long before I find something to direct it toward. There
will be something to pique my interest and distract me from the fact that I have
not signed up for any of this. That will be the way things are for a while, and
then back to drowning. Each time with more water in my lungs than the last, each
time more aware of the inevitable. This is how I have been doing things for a
while now, and it is probably going to remain that way until the end of time.
Why do I even bother? Let it get worse.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This has been a piece much outside of my usual pace. Truth be told, I am writing
to clear the thoughts in my mind. I don&apos;t want them; maybe you do. If you don&apos;t,
well, then you should have stopped reading earlier. How about that?&lt;/p&gt;</content:encoded><category>thoughts</category></item><item><title>My New Tech Stack</title><link>https://notashelf.dev/posts/my-new-stack</link><guid isPermaLink="true">https://notashelf.dev/posts/my-new-stack</guid><description>From Javascript, to Golang and Rust: A Technical Journey</description><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have began programming with Javascript back in 2018. It was a spontaneous
decision, made mostly in the hope of solving a basic problem that a game server
that I moderated was facing. Long story short, I learned Javascript (or at
least, some Javascript) over night and that was it for a while.&lt;/p&gt;
&lt;p&gt;That somehow propelled me head-first into web development. I was young, I wanted
a personal website and maybe I could even make a few bucks on the side from
developing websites for people. Since I knew Javascript somewhat decently, I
began looking at HTML and CSS, read some web blogs and a few programming books
that describe the basics of web development and went on with my journey.&lt;/p&gt;
&lt;p&gt;Alongside other experiences, this lead me to use Linux as my main operating
system and one of the most important reasons why I chose Linux was because it
was a liberating, customizable experience. As I moved to use tiling window
managers (i3 at the time), I also began experimenting with writing my own
desktop utilities. Learn Python, Go, C, little bit of C++ and eventually Rust;
which is now most of my stack.&lt;/p&gt;
&lt;p&gt;While my programming journey has taken a winding road through multiple
languages, each offering a unique set of strengths and trade-offs. Recently, I
made the decision to transition from Golang to Rust while still retaining C in
my toolbox. This post details the technical reasoning behind this move, the
advantages of each language, and why, despite its venerable status, C&apos;s build
tooling still leaves much to be desired.&lt;/p&gt;
&lt;h2&gt;The Golang Chapter&lt;/h2&gt;
&lt;p&gt;Go was... a compelling choice to say the least. Be it for its simplicity,
powerful concurrency model and rapid compilation times (which I miss to this day
in Rust.) Its robust standard library and clean syntax allowed me to build
distributed systems with ease. However, as projects grew in complexity, several
limitations became apparent.&lt;/p&gt;
&lt;p&gt;One very large issue was memory management. I want my software to be &lt;em&gt;lean and
mean&lt;/em&gt; but Go&apos;s garbage collection was imposing unpredictable latencies---an
issue in performance-sensitive applications that are the backbone of my home
network. For example, in a real-time data processing application, garbage
collection pauses caused noticeable delays. Benchmarks like &lt;code&gt;goperf&lt;/code&gt; have shown
that garbage collection pauses can sometimes exceed acceptable limits, making Go
much less suitable for low-latency requirements. The type system is also,
despite its simplicity, a hit-or-miss and it can can feel limiting when dealing
with more intricate, low-level abstractions or when requiring fine-grained
control over data. Lastly, the overhead introduced by some of Go&apos;s abstractions
are a bottleneck that I&apos;ve grown a distaste for during my time using Nix.&lt;/p&gt;
&lt;p&gt;These aspects led me to explore alternatives that offer more control without
sacrificing safety or performance.&lt;/p&gt;
&lt;h2&gt;Why Rust?&lt;/h2&gt;
&lt;p&gt;I didn&apos;t switch to Rust because of hype. If anything, the constant evangelism
around &quot;memory safety&quot; as if it&apos;s the only thing that matters was a turnoff. But
Rust kept solving real problems I hit when working in Go and C, to the point
where using anything else felt like unnecessary friction. It emerged as a
&lt;em&gt;natural next step&lt;/em&gt;. It is widely used, and even more widely shilled. While I
was using Go, I had experimented with Rust several times but extremely long
build times---alongside the complex type system---drove me off each time.
Eventually I accepted those as a cost for correctness, and began migrating to
Rust. Although, it was a slow process. The Rust book was extremely unhelpful,
especially in the pacing department, but eventually and through the help of many
web searches I accepted Rust. It brings several improvements that address the
limitations I experienced with Golang, which I would like to go over.&lt;/p&gt;
&lt;h3&gt;1. Memory Safety without Performance Penalties&lt;/h3&gt;
&lt;p&gt;First advantage I&apos;m quite fond of is Rust&apos;s &lt;strong&gt;memory safety without the need for
a garbage collector&lt;/strong&gt;. Rust&apos;s ownership model ensures memory safety at compile
time without relying on a garbage collector. This allows for predictable
performance, a very crucial factor that affected my decision. The borrow
checker, while initially imposing a learning curve, provides a safety net that
prevents data races and dangling pointers. Unlike Go, where garbage collection
kicks in unpredictably, Rust&apos;s compile-time borrow checker also ensures that
memory is allocated and freed at the right time. Unlike C, which is another
language I enjoy writing, but not so much *using, it doesn&apos;t require explicit
&lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt; calls or reference counting in most cases, meaning it balances
control and automation far better than either language.&lt;/p&gt;
&lt;p&gt;Instead of garbage collection, Rust relies on &lt;strong&gt;RAII&lt;/strong&gt; (Resource Acquisition Is
Initialization), where memory is freed as soon as it goes out of scope.
Consequently the borrow checker ensures safe aliasing of memory, use-after-free
bugs, and iterator invalidation. Nice. I want to control my memory, without
having to micromanange it.&lt;/p&gt;
&lt;h3&gt;2. &lt;em&gt;True&lt;/em&gt; Zero-Cost Abstractions&lt;/h3&gt;
&lt;p&gt;Golang&apos;s philosophy is &quot;simple is better.&quot; The problem is that simplicity in Go
often means &quot;dumbed down.&quot; No proper generics (until recently), no inlining
across package boundaries, and everything is done through interfaces that
introduce runtime overhead. As such, a common issue I had with Go is that
writing high-level, ergonomic code often meant sacrificing performance.
Interfaces, reflection, and even some seemingly simple abstractions introduce
overhead that isn&apos;t always obvious (or sometimes not even available because... I
don&apos;t know.) Rust does &lt;em&gt;not&lt;/em&gt; have this problem because of its zero-cost
abstraction principle: abstractions should compile down to the most efficient
possible machine code.&lt;/p&gt;
&lt;p&gt;For example, iterators in Rust provide the same expressiveness as Go&apos;s slices
but compile into raw loops without runtime overhead. Generics in Rust are fully
monomorphized at compile time, meaning there&apos;s no boxing or interface dispatch
cost. This means Rust allows writing expressive, high-level code while
maintaining (mostly) C-level performance. I have learned recently that Rust
compiles down to efficient machine code thanks to &lt;strong&gt;monomorphization&lt;/strong&gt; (try
saying that out loud 3 times in a row)---a process where generics are fully
expanded at compile time, allowing optimizations that eliminate runtime dispatch
overhead. Combined with LLVM&apos;s optimizations, Rust often generates assembly that
is as fast as, or faster than, handwritten C.&lt;/p&gt;
&lt;h3&gt;3. Concurrency Without Race Conditions&lt;/h3&gt;
&lt;p&gt;Goroutines and channels are nice, but they don&apos;t prevent data races. In Go, you
still need to rely on locks, atomics, etc, and most importantly careful design
to avoid issues like race conditions or deadlocks. Rust enforces safety at the
type level. This means that Rust code, once compiled, has &lt;em&gt;mathematically
guaranteed&lt;/em&gt; thread safety. No surprises, no debugging obscure race conditions in
production. The &lt;code&gt;Send&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt; traits act as static checks that prevent
unsafe sharing of resources, something that Go&apos;s runtime cannot enforce. Rust&apos;s
async ecosystem, using &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; and tools like Tokio, allows for
excellent asynchronous programming without the pitfalls of traditional
thread-based concurrency models.&lt;/p&gt;
&lt;h3&gt;4. Tooling That Actually Works&lt;/h3&gt;
&lt;p&gt;C&apos;s build tooling is archaic, Go&apos;s module system is still a mess (&lt;code&gt;GOPATH&lt;/code&gt; was
awful, and modules are only marginally better), but Rust? Cargo just works.
Dependencies, versioning, cross-compilation---it&apos;s all built-in, and I don&apos;t
have to waste time wrestling with third-party package managers or Makefiles.
Plus, tools like clippy, rustfmt, and cargo bench integrate seamlessly. Cargo
workspaces make it easy to manage multi-crate projects, allowing for better
organization and dependency management. Honestly, it&apos;s impeccable overall.&lt;/p&gt;
&lt;h2&gt;Considering Zig&lt;/h2&gt;
&lt;p&gt;One thing worth nothing about C is that I &lt;em&gt;enjoy&lt;/em&gt; writing it. The design of the
language, unlike C++, is simple and it&apos;s &lt;em&gt;fun&lt;/em&gt; to write. I find Rust fun too,
but sometimes type errors get on my nerves because I appear to get caught by the
most basic ones that I &lt;em&gt;probably&lt;/em&gt; wouldn&apos;t encounter on C. And while Rust has
largely taken over my system-level programming needs, I have been keeping an eye
on Zig. Zig&apos;s simplicity, lack of hidden control flow, and compile-time
execution capabilities make it an interesting alternative to both C and Rust in
certain contexts. Though I find it still a bit immature to really &quot;switch&quot; to as
my primary language. For now Rust will do, but I still feel compelled to mention
what Zig does well.&lt;/p&gt;
&lt;p&gt;Some of Zig&apos;s strengths include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Manual memory management without footguns&lt;/strong&gt; – Unlike C, Zig provides safer
manual memory management patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better C interoperability than Rust&lt;/strong&gt; – While Rust has &lt;code&gt;bindgen&lt;/code&gt; and
&lt;code&gt;cbindgen&lt;/code&gt;, Zig is designed from the ground up to interface smoothly with C
code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No hidden control flow&lt;/strong&gt; – No automatic panics, exceptions, or surprises in
the compiled output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;m not replacing Rust with Zig anytime soon, but for cases where Rust&apos;s safety
mechanisms feel like overkill, or where I need tighter control over binary size
and startup time, Zig looks promising. It&apos;s too early to say if it&apos;ll take a
permanent place in my workflow, but I am keeping an eye on it.&lt;/p&gt;
&lt;h2&gt;Why C Still Has a Place&lt;/h2&gt;
&lt;p&gt;Despite moving to Rust, C continues to hold a vital place in my workflow. Its
low-level access and direct mapping to hardware make it indispensable for
performance-critical tasks, interfacing with legacy systems, or working on
projects where every cycle counts. However, there are areas where C leaves a lot
to be desired.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build Systems Are a Mess – Make, CMake, Autotools—none of them are great, and
package management is nonexistent.&lt;/li&gt;
&lt;li&gt;Undefined Behavior Everywhere – The sheer number of ways you can shoot
yourself in the foot in C is staggering. Buffer overflows, use-after-free,
integer overflows—Rust eliminates these entirely.&lt;/li&gt;
&lt;li&gt;Lack of Modern Language Features – No generics, no proper modules, and macros
are still the hacky preprocessor-based mess they have always been.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, C isn&apos;t going anywhere. There&apos;s too much legacy code, too many
embedded systems, and too many low-level performance demands for it to
disappear. If you want to write kernels, firmware, or low-level graphics code,
you still want C, though you don&apos;t necessarily &lt;em&gt;need&lt;/em&gt; it. I find Rust&apos;s
bootstrappability to be a drawback, but that&apos;s for another post.&lt;/p&gt;
&lt;h2&gt;Go Has Replaced Python for Web and Scripting&lt;/h2&gt;
&lt;p&gt;For quick scripting, Go has taken over Python&apos;s role in my workflow, especially
for anything web-related. Python&apos;s performance is atrocious, and while Flask is
easy to use, it crumbles under load. Go, on the other hand, compiles to a single
binary, has a solid HTTP stack, and doesn&apos;t require setting up a virtual
environment just to run a script. The &lt;code&gt;net/http&lt;/code&gt; package provides a robust and
easy-to-use HTTP server and client, while frameworks like &lt;code&gt;gorilla/mux&lt;/code&gt; offer
more advanced routing capabilities.&lt;/p&gt;
&lt;p&gt;That&apos;s not to say Go is perfect---its standard library is missing some utilities
that Python has had forever---but for quick-and-dirty web tools, it&apos;s become my
go-to and I&apos;ll take Go over Python any day. Though the bar is low, and Go is
barely doing anything to remain above it.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Transitioning to Rust did not mean discarding Golang or C entirely. Each
language serves its purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rust&lt;/strong&gt; now leads my efforts in building safe, efficient, and modern systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt; continues to be my go-to for low-level programming tasks, where control
over hardware is paramount, despite its antiquated tooling. I usually try to
work in single files and avoid libraries when I&apos;m working with C, which is a
good learning experience on how to build stuff but still annoying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Golang&lt;/strong&gt; still finds its place in projects where rapid development and
simplicity are necessary, though its limitations have driven my current focus
away.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zig&lt;/strong&gt; is an emerging contender that I may use for certain low-level
applications where Rust or C might otherwise be considered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Long thing short, Rust isn&apos;t for everything, and neither is Go or C. Each
language has its niche, and the trick is knowing when to use what. Rust replaced
Go for me in performance-sensitive areas, but I still keep Go around for web
scripts and C for low-level work. Zig? Maybe it&apos;ll earn a place too. Time will
tell.&lt;/p&gt;</content:encoded><category>software</category><category>programming</category></item><item><title>NixOS Testing Framework I: On VM Tests</title><link>https://notashelf.dev/posts/nixos-testing-i</link><guid isPermaLink="true">https://notashelf.dev/posts/nixos-testing-i</guid><description>Introduction to the NixOS testing framework with flakes</description><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the things that convinced me to &lt;em&gt;stay on NixOS&lt;/em&gt; was how easy it is to
write integration tests for my existing infrastructure. Unlike traditional
testing setups, which often require complex tooling, manual configuration, or
fragile dependencies, NixOS makes testing nearly effortless. In fact, Nixpkgs
itself contains tests for even the most trivial cases---proving just how
seamless the process can be. If you&apos;ve ever struggled with testing your services
in other environments, where dependencies shift and system configurations break
unpredictably, then this post might convince you to give NixOS a second look.&lt;/p&gt;
&lt;p&gt;Today&apos;s article walks you through setting up your own integration tests outside
the context of Nixpkgs. I&apos;ll also briefly touch on how tests are executed within
Nixpkgs itself. Not long ago, the testing framework underwent some internal
changes, and now that &lt;code&gt;testers.runNixOSTest&lt;/code&gt; is stable, I believe it&apos;s the
perfect time to explore how you can leverage it for your own projects.&lt;/p&gt;
&lt;h2&gt;Obtaining Nixpkgs&lt;/h2&gt;
&lt;p&gt;We must first obtain the testers from somewhere. That somewhere is---
naturally---Nixpkgs.&lt;/p&gt;
&lt;p&gt;Testers are &lt;code&gt;system&lt;/code&gt; dependent, and they are dependant on &lt;code&gt;pkgs&lt;/code&gt;. I don&apos;t think
this is a surprise to anyone since we will be interacting with, well, packages
but the caller for &lt;code&gt;runNixOSTest&lt;/code&gt;
&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/230479874c95697578c56179905e4acf97d23e4d/pkgs/build-support/testers/default.nix#L168&quot;&gt;especially sets hostPkgs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That said, we will need to instantiate &lt;code&gt;pkgs&lt;/code&gt; somewhere. Usually in Nixpkgs you
are already in a scope that provides testers, but since we are working &lt;em&gt;outside&lt;/em&gt;
Nixpkgs for the purposes of this post, let&apos;s get &lt;code&gt;pkgs&lt;/code&gt; rolling. While I&apos;m at
it, I&apos;ll also provide a self-contained &lt;code&gt;flake.nix&lt;/code&gt; that fetches and locks a
Nixpkgs instance for us.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# flake.nix
{
  inputs.nixpkgs.url = &quot;github:nixos/nixpkgs?ref=nixpkgs-unstable&quot;;
  outputs = inputs: {
    # No import required unless you want to propagate `inputs` to the module
    nixosModules.default = ./your-service.nix;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s assume for the purposes of this post that you have a small NixOS module
that you want to execute tests for. This can be a short and simple Python script
for a webserver, a VPN mesh controller, or anything that you might have
expressed in a simple NixOS module. Let&apos;s also assume that whatever this service
might be, it serves a HTML page with... healthcheck information over at port
&lt;code&gt;3000&lt;/code&gt;. Got anything in your mind yet?&lt;/p&gt;
&lt;h2&gt;Writing the Test&lt;/h2&gt;
&lt;p&gt;One of the many handy features of flakes is that you can define &lt;em&gt;checks&lt;/em&gt; that
will be built on &lt;code&gt;nix flake check&lt;/code&gt; unless &lt;code&gt;--no-build&lt;/code&gt; is passed. While I&apos;m
testing outside resource constrained environments, I prefer to execute my VM
tests directly on check because that is two birds with one stone.&lt;/p&gt;
&lt;p&gt;Let&apos;s extend the &lt;code&gt;flake.nix&lt;/code&gt; from above with a check that will be built on
&lt;code&gt;nix flake check&lt;/code&gt; or manually with &lt;code&gt;nix build .#checks.&amp;#x3C;system&gt;.your-check&lt;/code&gt;.
Since this is meant to serve as an example, I have omitted the system
abstractions and we will be testing for a single system only. You may change
this however as you see fit. The key point here is to define a &apos;package&apos; using
&lt;code&gt;runNixOSTest&lt;/code&gt; that will be executed in a way that you see fit.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  inputs.nixpkgs.url = &quot;github:nixos/nixpkgs?ref=nixpkgs-unstable&quot;;
  outputs = inputs: {
    nixosModules.default = ./your-service.nix;

    checks = let
      system = &quot;x86_64-linux&quot;;
      pkgs = inputs.nixpkgs.legacyPackages.${system};
    in {
      &quot;${system}&quot;.default = pkgs.testers.runNixOSTest { /*...*/ };
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is how our flake will look like, for now. I want to explain the potential
arguments to &lt;code&gt;runNixOSTest&lt;/code&gt; to give you an idea before I write the rest of the
test to give you an idea before I write the rest of the test.&lt;/p&gt;
&lt;h2&gt;Anatomy of &lt;code&gt;runNixOSTest&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;runNixOSTest&lt;/code&gt; is a wrapper function around &lt;code&gt;runTest&lt;/code&gt; from Nix_OS_ (not
nix_pkgs_) library, defined in
&lt;a href=&quot;https://github.com/NixOS/nixpkgs/tree/7d5cd42fece2ae9b065b00c08696b439a864401f/nixos/lib&quot;&gt;nixos/lib&lt;/a&gt;.
It imports &lt;code&gt;nixos/lib/default.nix&lt;/code&gt; and wraps the &lt;code&gt;runTest&lt;/code&gt; function that is
defined in NixOS library, which is &lt;em&gt;inherited from the testing library inside
the NixOS library inside nixpkgs...&lt;/em&gt; Ugh.&lt;/p&gt;
&lt;p&gt;Regardless, and in the spirit of &lt;code&gt;runTest&lt;/code&gt;, there are a few parameters that you
can pass to &lt;code&gt;runNixOSTest&lt;/code&gt;. I will not be covering &lt;em&gt;all&lt;/em&gt; of them, but here are
those that you &lt;em&gt;want&lt;/em&gt; to pass for a functional test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;runNixOSTest {
  # First, the &apos;name&apos; parameter. This will determine the name of the package
  # that you will be building, but from what I can tell, not much else. For
  # example a test with the name &quot;my-test&quot; would result in &quot;vm-test-run-my-test&quot;
  # being built.
  name = &quot;my-test&quot;;

  # Next up is nodes. This is where it gets interesting, because you can define
  # as many nodes as you deem necessary in this set. More importantly, those
  # nodes are allowed to interact with each other inside the texting context.
  nodes = { /* ... */ };

  # Last but not least, the test script. The test script is what I would call
  # a *flavored* Python script, where you get access to a few special machine
  # objects as a part of Nixpkgs, and the documentation describes it as a
  # &quot;...sequence of Python statements that perform various actions, such as
  # starting VMs, executing commands in the VMs and so on.&quot;
  testScript = &quot;&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few other parameters that you can use, such as &lt;code&gt;extraPythonPackages&lt;/code&gt;
that you can use to add additional Python libraries in the script. Though I&apos;m
getting ahead of myself, we&apos;re here to talk about writing tests.&lt;/p&gt;
&lt;h2&gt;Writing the Test: Continued&lt;/h2&gt;
&lt;p&gt;As I was saying. Now that we know how a test looks like, lets write it for real
this time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  inputs.nixpkgs.url = &quot;github:nixos/nixpkgs?ref=nixpkgs-unstable&quot;;
  outputs = inputs: {
    nixosModules.default = ./your-service.nix;

    checks = let
      system = &quot;x86_64-linux&quot;;
      pkgs = inputs.nixpkgs.legacyPackages.${system};
    in {
      &quot;${system}&quot;.default = pkgs.testers.runNixOSTest {
        # This can be anything. I like giving my test special
        # codenames. Yes, I&apos;m quirky like that.
        name = &quot;narwals-are-awesome&quot;;

        # Let&apos;s go with a single-node test, for now. I&apos;m going to add an example
        # at the end of this post to give you an idea of multi-node tests.
        nodes = {
          # Names of the nodes are up to you as well. They expose the same
          # objects regardless of names, though &apos;machine&apos; is mostly standard, so
          # I will go with that for now. As a bonus, you can read this with the
          # voice of Gianni Matragrano, who is said to voice anyone for a
          # chicken nugget.
          machine = {pkgs, ...}: {
            imports = [ inputs.self.nixosModules.default ];
            environment.systemPackages = [ pkgs.curl ];

            # Enable the service. This does not exist in Nixpkgs and
            # is added by our nixosModule. You can enable anything defined in
            # nixpkgs inside a machine&apos;s configuration *and* extend them with
            # your own NixOS modules.
            services.syshc.enable = true;
          };
        };


        # Now the test script. You can add a comment like /* python */ before
        # the body of this string to provide syntax highlighting via Treesitter
        # if your editor is Neovim. Neat!
        # The script will start all available nodes, wait for the service to
        # start, and then the port. Lastly it will query the / endpoint to look
        # for a specific value that indicates success. Your test cases may be
        # more complex than this, in which case you can write a more detailed
        # Python script.
        testScript = /* python */ &apos;&apos;
          # Function to start *all* nodes at once. This can be used as an
          # alternative to &amp;#x3C;nodename&gt;.start() (e.g. `machine.start()`) when
          # you have multiple nodes.
          start_all()

          # wait_for_unit is a special object that will wait for a Systemd
          # unit to get into &quot;active&quot; state. Throws exceptions on &quot;failed&quot;
          # and &quot;inactive&quot; states as well as after timing out.
          machine.wait_for_unit(&quot;python-server&quot;)

          # Also wait for an open port on the node. In my script the service
          # binds to port 3000, so we must wait for it to open.
          machine.wait_for_open_port(3000)

          # Finally lets log the output from the service. In my example the
          # server logs health information directly in /, but you may get the
          # information from anywhere.
          status = machine.succeed(&quot;curl --fail localhost:3000&quot;)

          # Check if our server returns the expected result.
          assert &quot;Healthy&quot; in status, f&quot;&apos;{status}&apos; is not healthy! Check failed.&quot;
        &apos;&apos;;
      };
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running Your Test&lt;/h2&gt;
&lt;p&gt;Now lets build the check with some verbosity. &lt;code&gt;-L&lt;/code&gt; will tell the tester to print
all logs from the test, and &lt;code&gt;v&lt;/code&gt; adds some verbosity to the Nix builder.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nix flake check -Lv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this will print a lot of logs. Let&apos;s isolate the output that we really care
about.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;vm-test-run-narwals-are-awesome&gt; (finished: waiting for TCP port 3000 on localhost, in 18
vm-test-run-narwals-are-awesome&gt; machine: must succeed: curl --fail http://localhost:3000
vm-test-run-narwals-are-awesome&gt; machine #   % Total    % Received % Xferd  Average Speed
vm-test-run-narwals-are-awesome&gt; machine #                                  Dload  Upload
vm-test-run-narwals-are-awesome&gt; machine #   0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0[   34.854829] syshc[637]: 127.0.0.1 - - [04/Apr/2025 09:49:17] &quot;GET
 / HTTP/1.1&quot; 200 -
vm-test-run-narwals-are-awesome&gt; machine # 100   115  100   115    0     0    108      0  0:00:01  0:00:01 --:--:--   108100   115  100   115    0     0    107      0  0:00:01  0:00:01 --:-
-:--   107
vm-test-run-narwals-are-awesome&gt; (finished: must succeed: curl --fail http://localhost:3000, in 1.43 seconds)
vm-test-run-narwals-are-awesome&gt; (finished: run the VM test script, in 36.99 seconds)
vm-test-run-narwals-are-awesome&gt; test script finished in 37.09s
vm-test-run-narwals-are-awesome&gt; cleanup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And there it is. This waits for port 3000, curls it, gets the result and parses
it. Since the server returned &quot;Healthy&quot;, the check has completed successfully.
Nice.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Hope this article has served to give you an idea of how you may utilize NixOS
testing framework for your projects. At least a very basic idea, enough to help
get you started. Though there is much more to this framework, and more
conditions that you might want to be aware of.&lt;/p&gt;
&lt;p&gt;As such, I invite you to reach out to me and let me know of any additional cases
that you want me to cover. I am happy to help clarify testing with NixOS, as I
think it should be done more often outside of Nixpkgs. Moreover, this is not the
end of this topic. I would like to cover the aforementioned special objects, and
more complex cases surrounding multi-machine test scenarios. I also want to go
over &lt;em&gt;interactive&lt;/em&gt; tests, but that will have to be for another post. Cheers&lt;/p&gt;</content:encoded><category>nix</category><category>nixos</category><category>tutorial</category></item><item><title>What is nixConfig, Should You Trust It?</title><link>https://notashelf.dev/posts/reject-flake-content</link><guid isPermaLink="true">https://notashelf.dev/posts/reject-flake-content</guid><description>A quick look into Nix&apos;s &apos;controversial&apos; nixConfig settings</description><pubDate>Mon, 31 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Those of you who have been a part of the Nix ecosystem, more specifically the
community that often deals with flakes,&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/reject-flake-content#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; have no doubt noticed or perhaps
actively use the &lt;code&gt;nixConfig&lt;/code&gt; attribute in a &lt;code&gt;flake.nix&lt;/code&gt;. For those unfamiliar,
the NixOS wiki in the &lt;a href=&quot;https://wiki.nixos.org/wiki/Flakes#Flake_schema&quot;&gt;article on flakes&lt;/a&gt; defines it as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;nixConfig&lt;/code&gt; is an attribute set of values which reflect the values given to
nix.conf. This can extend the normal behavior of a user&apos;s nix experience by
adding flake-specific configuration, such as a binary cache.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds good, right? Wrong. It is a ticking time bomb waiting to explode. Being
able to modify your &lt;code&gt;nix.conf&lt;/code&gt; on your system is equivalent to having full
control of the Nix daemon. When you check out a repository that uses flakes and
run, say, &lt;code&gt;nix run&lt;/code&gt; on a package, you will get a prompt asking whether you trust
the configuration settings set in the flake.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;$ nix run .#foobar
do you want to allow configuration setting &apos;extra-substituters&apos; to be set to &apos;https://nix-community.cachix.org&apos; (y/N)? y
do you want to permanently mark this value as trusted (y/N)? y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Accept it, and the door is wide open. More technical users may object, saying
they will know the changes being made to their system. They are not wrong, but
if that is their argument, then they are incredibly dense.&lt;/p&gt;
&lt;h2&gt;Security Implications&lt;/h2&gt;
&lt;p&gt;You should be fully aware that changes made through &lt;code&gt;nixConfig&lt;/code&gt; will affect all
Nix operations within the flake, possibly increasing the attack surface through
the introduction of unsafe, unsigned, and malicious binary caches. It can also
introduce &lt;code&gt;allowUnfree&lt;/code&gt;, which might cause ideological or legal (&lt;em&gt;see:
licensing&lt;/em&gt;) issues depending on the context.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nixConfig&lt;/code&gt; is a powerful option that can modify settings you do not want
changed haphazardly—from package sources to substitution and trusted keys your
build process will use. Oftentimes, this option is more of a hassle than a
convenience, though some might find it incredibly useful.&lt;/p&gt;
&lt;p&gt;There exists an &lt;code&gt;accept-flake-config&lt;/code&gt; option that you can set as
&lt;code&gt;nix.settings.accept-flake-config&lt;/code&gt;. Keep this set to &lt;em&gt;false&lt;/em&gt;, as automatically
accepting those options---without the prompt above—is more insecure than you
think. There are &lt;em&gt;many&lt;/em&gt; vulnerabilities that can come from blindly trusting a
flake&apos;s &lt;code&gt;nixConfig&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to go a step further, I have been running the following patch by the
awesome &lt;a href=&quot;https://github.com/eclairevoyant&quot;&gt;@eclairevoyant&lt;/a&gt; to add a
&lt;em&gt;reject-flake-config&lt;/em&gt; option to &lt;a href=&quot;https://lix.systems&quot;&gt;Lix, the Nix fork&lt;/a&gt;, to
automatically reject flakes&apos; &lt;code&gt;nixConfig&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;From 25f1b8e714b13d2aa6fcdc67bedf1544bd17e45a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=A9clairevoyant?= &amp;#x3C;848000+eclairevoyant@users.noreply.github.com&gt;
Date: Fri, 19 Jul 2024 09:23:27 -0400
Subject: [PATCH 3/4] feat: option reject-flake-config

---
src/libexpr/flake/config.cc       | 5 +++++
src/libfetchers/fetch-settings.hh | 4 ++++
2 files changed, 9 insertions(+)

diff --git a/src/libexpr/flake/config.cc b/src/libexpr/flake/config.cc
index 558b3e9b9..bf558e5e2 100644
--- a/src/libexpr/flake/config.cc
+++ b/src/libexpr/flake/config.cc
@@ -51,6 +51,11 @@ void ConfigFile::apply()
         else
             assert(false);
+        if (nix::fetchSettings.rejectFlakeConfig) {
+            warn(&quot;ignoring untrusted flake configuration setting &apos;%s&apos; due to the &apos;%s&apos; setting.&quot;, name, &quot;reject-flake-config&quot;);
+            continue;
+        }
+
         bool trusted = whitelist.count(baseName);
         if (!trusted) {
             switch (nix::fetchSettings.acceptFlakeConfig.get()) {

diff --git a/src/libfetchers/fetch-settings.hh b/src/libfetchers/fetch-settings.hh
index 93123463c..67f2e4d14 100644
--- a/src/libfetchers/fetch-settings.hh
+++ b/src/libfetchers/fetch-settings.hh
@@ -108,6 +108,10 @@ struct FetchSettings : public Config
        )&quot;,
        {}, true, Xp::Flakes};

+    Setting&amp;#x3C;bool&gt; rejectFlakeConfig{this, false, &quot;reject-flake-config&quot;,
+        &quot;Whether to reject nix configuration (including whitelisted settings) from a flake without prompting.&quot;,
+        {}, true, Xp::Flakes};
+
    Setting&amp;#x3C;std::string&gt; commitLockFileSummary{
        this, &quot;&quot;, &quot;commit-lockfile-summary&quot;,
        R&quot;(
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Give it a try if you are running Lix and don&apos;t mind the rebuilds. This will
reject the flake config, skipping the prompt to accept the values set in
&lt;code&gt;nixConfig&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Looking into nixConfig&lt;/h2&gt;
&lt;p&gt;Now that the ultimatum is out of the way, let&apos;s take a look at what &lt;code&gt;nixConfig&lt;/code&gt;
is and what it really does. As mentioned above, it is &lt;em&gt;an attribute set of
values that reflect the values given to nix.conf.&lt;/em&gt; That in itself is a vague
explanation, but it gets the point across: it is an attribute set that takes the
same values you would pass to &lt;code&gt;nix.settings&lt;/code&gt;, which are used to construct the
&lt;code&gt;nix.conf&lt;/code&gt; in a typical NixOS configuration. Or, if you are using Nix
standalone, it is an attribute set of values that would be converted to their
&lt;code&gt;nix.conf&lt;/code&gt; equivalents.&lt;/p&gt;
&lt;p&gt;It is defined and used in &lt;a href=&quot;https://github.com/NixOS/nix/blob/92c4789ec72a5bf485679f9a5e5a244e553fb03d/src/libflake/flake/config.cc&quot;&gt;&lt;code&gt;libflake/flake/config.cc&lt;/code&gt;&lt;/a&gt;, and according to the
&lt;code&gt;getDataDir()&lt;/code&gt; function, this retrieves the full path of Nix&apos;s data directory
(one of &lt;code&gt;NIX_DATA_DIRECTORY&lt;/code&gt;, &lt;code&gt;XDG_DATA_HOME&lt;/code&gt;, or &lt;code&gt;$HOME/.local/share/nix/&lt;/code&gt; in
that order) and creates &lt;code&gt;trusted-settings.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nice. Now we know where to look if we &lt;em&gt;accidentally&lt;/em&gt; accept the prompt or ever
want to retract our blind trust in a flake&apos;s author.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Other than this path, there is not much to &lt;code&gt;nixConfig&lt;/code&gt;. Your system does not
interact with it until you enter the directory of a flake and run a command that
triggers evaluation, in which case &lt;a href=&quot;https://github.com/NixOS/nix/blob/92c4789ec72a5bf485679f9a5e5a244e553fb03d/src/libflake/flake/config.cc#L32C1-L79C2&quot;&gt;&lt;code&gt;ConfigFile::apply&lt;/code&gt;&lt;/a&gt; is invoked.&lt;/p&gt;
&lt;p&gt;In short, &lt;code&gt;nixConfig&lt;/code&gt; is a powerful attribute that &lt;em&gt;may or may not come in
handy&lt;/em&gt; at the cost of exposing a very large attack vector. Even a lazy attacker
can exploit it, and the user is not properly warned about the consequences of
accepting the prompt (which defaults to true for some reason).&lt;/p&gt;
&lt;p&gt;This has been your ever-so-informative technical rant on Nix and its
undocumented mess. Hope you learned something today. Cheers.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;If you don&apos;t use Nix flakes or have no idea what they are, then this post
does not apply to you. In fact, I&apos;m glad you&apos;re saving yourself the
headache! &lt;a href=&quot;https://notashelf.dev/posts/reject-flake-content#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>nix</category><category>flakes</category><category>security</category></item><item><title>Building Deterministic Typst Packages with Nix</title><link>https://notashelf.dev/posts/deterministic-typst</link><guid isPermaLink="true">https://notashelf.dev/posts/deterministic-typst</guid><description>Every time I think to myself &apos;no way Nix can do this,&apos; Nix does it anyway.</description><pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I encountered a &lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/369283&quot;&gt;Typst Builder PR&lt;/a&gt; in Nixpkgs that implements a Typst
builder, similar to other language-specific builders you might be familiar with.&lt;/p&gt;
&lt;p&gt;While I still use Pandoc and Markdown for many tasks (including this blog), I&apos;ve
been using &lt;a href=&quot;https://typst.app&quot;&gt;Typst&lt;/a&gt; extensively recently, and that got me
excited to explore the new builder(s).&lt;/p&gt;
&lt;p&gt;To test them, and possibly demonstrate their functionality, I&apos;ve prepared the
following &lt;code&gt;flake.nix&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  inputs.nixpkgs.url = &quot;github:cherrypiejam/nixpkgs?ref=typst&quot;;
  outputs = {nixpkgs, ...}: let
    systems = [&quot;x86_64-linux&quot;];
    forEachSystem = nixpkgs.lib.genAttrs systems;

    pkgsForEach = nixpkgs.legacyPackages;
  in {
    packages = forEachSystem (system: {
      inherit (pkgsForEach.${system}) buildTypstPackage typstPackages;
    });

    devShells = forEachSystem (system: let
      pkgs = pkgsForEach.${system};
    in {
      default = pkgs.mkShellNoCC {
        # packages provided in &apos;nix develop&apos;
        packages = [
          (pkgs.typst.withPackages (ps:
            with ps; [
              polylux
              cetz_0_3_0
            ]))
        ];
      };
    });
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This flake re-exports &lt;code&gt;buildTypstPackage&lt;/code&gt; and &lt;code&gt;typstPackages&lt;/code&gt; from nixpkgs and
uses the new &lt;code&gt;typst.withPackages&lt;/code&gt; scope to create a deterministic Typst
environment. Its usage, though subject to change, appears straightforward.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;buildTypstPackage&lt;/code&gt; produces a &quot;manifest&quot; of available Typst packages from Typst
Universe, which you can use within &lt;code&gt;typst.withPackages&lt;/code&gt;&apos;s scope. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;typst.withPackages (ps:
 with ps; [
    polylux
    cetz
 ])
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows you to use, for instance, &lt;code&gt;#import &quot;@preview/cetz:0.3.0: \*&quot;&lt;/code&gt; as
cetz is added to Typst&apos;s search path via the &lt;code&gt;TYPST_PACKAGE_CACHE_PATH&lt;/code&gt; variable
set to, e.g.,
&lt;code&gt;/nix/store/02pgr69nc5lczi9rrh2xd40s1mzqrvkl-typst-0.13.1-env/lib/typst/packages&lt;/code&gt;,
populated with the packages in your scope.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;I encourage you to review the &lt;a href=&quot;https://github.com/NixOS/nixpkgs/pull/369283&quot;&gt;Typst Builder PR&lt;/a&gt;. It seems nearly ready and will
likely be included in nixpkgs soon. If you want to try it out now, consider
examining the example flake I&apos;ve provided above.&lt;/p&gt;</content:encoded><category>tutorial</category><category>nix</category><category>software</category></item><item><title>On Editors and Things</title><link>https://notashelf.dev/posts/on-editors-and-things</link><guid isPermaLink="true">https://notashelf.dev/posts/on-editors-and-things</guid><description>Heartfelt ramblings about available editors</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hi all, good news first. I am a &lt;strong&gt;Release Editor for NixOS 25.05&lt;/strong&gt;. This is a
bit difficult for me to talk about, since I do not know what exactly what it
entails &lt;em&gt;yet&lt;/em&gt; but know that I am excited about it, and I wanted to share it with
you, dear reader. It has been one &lt;em&gt;hell&lt;/em&gt; of a journey so far, I am excited to
see what is to come. NixOS, with its flaws, has a special place in my heart and
it is great to be able to finally play a part in the grand scheme of things.&lt;/p&gt;
&lt;p&gt;There are a few articles that I am working on, one of them being the
continuation of the NixOS hardening series. Unfortunately there is not much time
on my hands, and my backlog is filling up again. Worry not though, they are
coming along nicely. I also have some technical rants on the horizon, since I
have not written one of those in a while. Until I get the time to finish those,
enjoy this heartfelt rambling on editors and the heart-to-heart in an unusual
pace.&lt;/p&gt;
&lt;h2&gt;On Editors&lt;/h2&gt;
&lt;p&gt;I do a lot of, well, editing on my computer. This is sometimes for academic
essays that I have to write myself, or sometimes academic essays that I have to
proofread but it usually involves me sitting in front of a computer screen for
hours on end and simply type away. Thanks to the relative leniency of my
department, I have been able to use LaTeX in the past and more recently I have
been enjoying &lt;a href=&quot;https://typst.app&quot;&gt;Typst&lt;/a&gt;. Much of this editing, regardless of
the framework, involves an editor. While it would be all too simple to use
something like LibreOffice, I rarely find myself opening &lt;code&gt;.docx&lt;/code&gt; files to edit
an essay, and it&apos;s even less rare of an occurrence for me to be writing in one.
What I use for most of my editing is, of course, my trusty Neovim setup.&lt;/p&gt;
&lt;p&gt;Neovim holds an interesting place in my tool belt. I have switched off
VSCode--the editor I have started with-- due to its annoyingly high RAM usage
and power draw on my laptop, which made it less than ideal for when I could not
easily find a place to plug my charger. Around the same time I was learning
about Linux (and customizing the hell out of my Arch Linux setup) so it
eventually lead to me checking out Neovim. It was something reminiscent of love
at first sight. It was all snappy and intuitive and simple, I could not have
asked for a better editor. Neovim &lt;em&gt;was&lt;/em&gt; what I was looking for.&lt;/p&gt;
&lt;p&gt;Over time I began adding plugins, usually to add features that I was missing
from VSCode. LSPs and debugger plugins quickly found their way into my
configuration, and over time Neovim was not quite what it first was. It had
become sluggish, and complex. Worse, it had become inconsistent with the number
of plugins that all insist that their way of doings things is the correct one.
Around this time, I switched to NixOS and began thinking about setting up Neovim
declaratively on my system. &lt;code&gt;programs.neovim&lt;/code&gt; was not quite intuitive, and
linking a &lt;code&gt;nvim&lt;/code&gt; directory to &lt;code&gt;~/.config/nvim&lt;/code&gt; felt less than ideal. This lead
to &lt;em&gt;neovim-flake&lt;/em&gt;. I cannot quite call it my creation, as I have soft-forked it
from a project with the same name by Jordan Isaacs, but I had many different,
conflicting ideas in mind. So much so, that it warranted the soft-fork to take
it in a different direction. neovim-flake, the original one, was a learning
experience. I learned a lot about how to write Nix, and I also learned how &lt;em&gt;not&lt;/em&gt;
to write Nix.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A not-so-recent discovery that I have made is that I learn by doing.
&lt;em&gt;Explicitly&lt;/em&gt; by doing. Little bit (a lot) of research later, I found the state
of academia to be be... disappointing, to say the least. There is a need for
greater adoption of evidence-based cognitive learning strategies in
educational settings and despite their proven effectiveness, teacher training
programs often do not appear to be covering any cognitive learning processes.
This, unfortunately, leads to many students (such as myself) distancing
themselves from the education system entirely. Although I do not feel equipped
to do more than just to observe. If you &lt;em&gt;are&lt;/em&gt;, however, an expert in the field
perhaps reach out to me. I would love to hear about your thoughts as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As I was saying, neovim-flake--now nvf--was a learning experience. More
importantly, it was a testing ground for my &quot;ideal&quot; Neovim configuration. I have
learned about Neovim as much as I have learned about Nix by maintaining nvf. The
most important discovery while working on nvf and Neovim, was that &lt;strong&gt;Neovim is a
subpar editor&lt;/strong&gt;. I mean this in an endearing way of course; I would want my
editor to become better and I think it is moving in the right direction,
however, with contenders such as Helix now challenging Neovim&apos;s place it &lt;em&gt;might&lt;/em&gt;
be the time to reconsider priorities and re-allocate resources. Although I am
uninterested in Helix &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-editors-and-things#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, it has pushed--moreso, nudged Neovim in the correct
direction by looking at Neovim&apos;s long-time mistakes and working on fixing them.
For one, Helix boasts a more consistent interface for features that are only
available as plugins on Neovim. There is no conflicting design choices or UI
decisions. Not yet, anyway. I think consistent UI is a good thing to have in
your editor, however good your UX may be.&lt;/p&gt;
&lt;p&gt;As I have mentioned above, Neovim has started moving in the correct direction
with some of the core-features of Helix stealing the hearts of many Neovim
users. After a very underwhelming 0.10 release, I believe 0.11 is taking Neovim
in a better direction by analyzing the core demands of the community and trying
to implement them as a part of the editor, in response to the needs and demands.
The new LSP interface appears very interesting, though I would be equally happy
to see a consistent interface for linters and formatters. null-ls (now none-ls)
has been a frustrating experience, with how slow and clunky it is. none-ls
doubled down on complexity by moving feature-complete builtins to a different
repo, which you have to fetch and load yourself. This is &lt;em&gt;not&lt;/em&gt; how an editor
should function. Taking from Helix, I would be delighted to see some of the
plugin exclusive features as parts of the core editor. Although, this might be
wishful thinking. So far, I am excited for the prospect of natively configuring
LSPs and a few UI additions that I have been looking forward to. Perhaps I&apos;ll
write about Neovim again when 0.11 drops. There is also the less commonly talked
about &lt;a href=&quot;https://github.com/neovim/neovim/pull/27339&quot;&gt;snippet engine&lt;/a&gt; that will be
built into Neovim with 0.11. Those are all very welcome additions, and it is a
great time to be a Neovim user.&lt;/p&gt;
&lt;p&gt;For the time being, I have been aggressively dropping plugins that I don&apos;t
&lt;em&gt;really&lt;/em&gt; need or rewriting them myself as more efficient autocommands as parts
of my configuration. This seems to have helped with the startup times, as some
Neovim plugins are &lt;em&gt;very&lt;/em&gt; inefficient. I have also dropped nvim-cmp because how
how slow it was, but blink.nvim is restoring my &quot;faith&quot; in completion plugins
again. The &lt;a href=&quot;https://github.com/Saghen/frizbee&quot;&gt;frizbee&lt;/a&gt; matcher is quite fast,
and hopefully it will be utilized better in the future. On that note, I
encourage you to do the same. Learning Lua is trivial, and if you have
experience with programming you might be able to find far smarter
implementations to problems &quot;solved&quot; by small convenience plugins.&lt;/p&gt;
&lt;h2&gt;On Things&lt;/h2&gt;
&lt;p&gt;On a more emotional side, I have been very burnt out with things. If you use any
of my projects, you might have noticed that they have not been maintained on
their usual pace for a while now. This is not permanent, I have &lt;em&gt;many&lt;/em&gt; plans but
unfortunately not enough time to follow through &lt;em&gt;for now&lt;/em&gt;. Expect the language
modules to be improved for &lt;a href=&quot;https://github.com/notashelf/nvf&quot;&gt;nvf&lt;/a&gt; in the near future, and an internal rewrite of
&lt;a href=&quot;https://github.com/schizofox/schizofox&quot;&gt;Schizofox&lt;/a&gt;. &lt;a href=&quot;https://github.com/feel-co/hjem&quot;&gt;Hjem&lt;/a&gt; is, fortunately, a team project and is able to proceed with
and without me. I consider other public software (such as &lt;a href=&quot;https://github.com/notashelf/microfetch&quot;&gt;Microfetch&lt;/a&gt;) I&apos;ve
created to be stable, but perhaps they will be picked back up as well. I have
been meaning to make Microfetch even faster (can&apos;t stop, won&apos;t stop) for a while
now...&lt;/p&gt;
&lt;p&gt;Regardless, there are many nice things planned for the future but I first need
some free time. Working on a few projects, and it is almost the busy season at
$WORK. But I digress. I wanted to give you this as a progress report, and to
explain the reasoning behind my absence. Some of those projects have been
getting a lot of attention, even with my absence, so I wanted to personally
thank everyone who has been submitting pull requests. You may not think much of
it, but it means much to me that you have taken the time to contribute to the
project(s).&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;If this reads off as a little pessimistic, worry not. Nothing is changing
anytime soon, or at least nothing is changing for the &lt;em&gt;worse&lt;/em&gt; anytime soon. I
will continue writing on this blog, and I will continue working on nvf as Neovim
appears to be my forever editor. This was a temporary break from my usual pace
of detailed, technical writing that takes more time and an opportunity to get
some things off my chest, or out of my mind. As I&apos;ve said before, there are
multiple articles in the writing, but I would like to hear what you think I
should write about too. Nix ecosystem is weirdly obscure, and sometimes
independent blog articles do better than official documentation for learning.&lt;/p&gt;
&lt;p&gt;If you have read this far, thank you. I would also like to hear your thoughts on
an of the things I&apos;ve talked about above. Feel free to reach out to me anytime.
That said, this is all I have time for today. Thank you for reading.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;This is due to my particular distaste of Lisp and Lisp-likes. Helix&apos;s so
called &quot;plugin system&quot; is, at its core, a glorified Scheme interpreter and I
do not feel positively about it. I believe there is also talks about using
Scheme for &lt;em&gt;configuration&lt;/em&gt;, and not just plugins. Compared to something like
TOML, Scheme is a horrible choice and frankly that alone makes me want to
distance myself from Helix. This is not to say Helix is a bad editor, I
quite like the concepts it introduced to the editor scene but it does not
strike me as something I can bring myself to daily drive. &lt;a href=&quot;https://notashelf.dev/posts/on-editors-and-things#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>software</category><category>neovim</category><category>news</category></item><item><title>NixOS Security I: Systemd</title><link>https://notashelf.dev/posts/insecurities-remedies-i</link><guid isPermaLink="true">https://notashelf.dev/posts/insecurities-remedies-i</guid><description>First installment to a series on securing your NixOS systems</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Linux system is a complex ecosystem of components, each with its own set of
vulnerabilities. NixOS is no exception. While its declarative nature provides
some advantages, it is not--nor can it ever be--a silver bullet against the
inherent insecurities that exist in any system. This &lt;em&gt;status quo&lt;/em&gt;--a state of
perpetual vulnerability--calls you to action.&lt;/p&gt;
&lt;p&gt;For the last 6 months or so, I have been focusing on hardening each and every
single component of my NixOS installation. This post, as an attempt to document
my experiences for the sake of establishing a point of reference, marks the
beginning of a series dedicated to hardening the different components of a NixOS
system. Given that NixOS is a systemd-based distribution, we&apos;ll start by
focusing on systemd. Over the course of the series, I&apos;ll also delve into kernel
and network security, although these topics require further research and are
beyond the scope of this post.&lt;/p&gt;
&lt;p&gt;In this installment, we&apos;ll explore how you can harden various systemd services
on your system to reduce potential attack surfaces.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Systemd, as a central part of many modern Linux distributions, provides various
utilities designed to streamline system management. However, its centralization
can also mean a single point of failure, and its wide array of features offers a
large attack surface if misconfigured. One of the utilities it offers, which
will come in very handy today, is &lt;strong&gt;systemd-analyze security&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Go ahead and run &lt;code&gt;sudo systemd-analyze security&lt;/code&gt; on your system. You will notice
&lt;em&gt;very quickly&lt;/em&gt; that it contains a lot of &quot;UNSAFE&quot; and &quot;EXPOSED&quot; services. First
of all, do not worry. Your system is &lt;em&gt;probably safe&lt;/em&gt;. The assessments done by
Systemd, as I will emphasize time and time again, are &lt;strong&gt;entirely arbitrary&lt;/strong&gt;.
They are rule-based score calculations based on your configuration, do not
correspond to or reflect upon the actual vulnerability of your system; there is
more nuance to such vulnerabilities than a rule-based analysis tool can
determine. Indeed, the score assigned to each service is &lt;em&gt;not&lt;/em&gt; an indicator of
how secure or insecure it is. It is, however, a start. In the case of Systemd,
hardening services is a second line of defense when the executable the service
is for becomes the vulnerability.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemd-analyze security &amp;#x3C;unit&gt;&lt;/code&gt; generates a score for a given unit, showing
all the used directives. Based on limited information we have, it is possible to
try and harden individual services.&lt;/p&gt;
&lt;p&gt;By default, Systemd leaves running services in the open. This is understandable,
as trying to preemptively harden a service is likely to cause conflicts with
individual services and their requirements.&lt;/p&gt;
&lt;p&gt;Although NixOS makes &lt;em&gt;some&lt;/em&gt; effort to harden services, installed services--be it
through NixOS&apos; services or your inferior distribution&apos;s package manager--will be
running with little to no hardening - which you can confirm by viewing the
security report. This post, as per my experience, aims to detail potential
hardening options. Throughout this post, please keep those following core
principles in mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There is &lt;strong&gt;no&lt;/strong&gt; &quot;one size fits all&quot; kind of hardening, all services will
require your undivided attention to make sure everything continues to work as
intended.&lt;/li&gt;
&lt;li&gt;No kind of hardening can catch all kinds of exploits. The key to a secure
system is to remain ever-vigilant.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Some hardening options will disable access to certain paths, or make them
read-only for the service. This is quite helpful, in theory, but not every
program is written intelligently and as such, not all programs will fail
gracefully when they are missing access to a path. Sometimes the service will
fail, and you will not be able to tell why. This is exactly why you must treat
each service with special care - and harden them one service at a time instead
of abstracting a generalized way of modifying multiple services at once.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Hardening Services&lt;/h3&gt;
&lt;p&gt;Systemd services in NixOS are defined through the Systemd module exposed in the
Nixpkgs module system. The schema is very basic, and I suggest that you consult
&lt;a href=&quot;https://search.nixos.org/options?channel=unstable&amp;#x26;size=50&amp;#x26;sort=relevance&amp;#x26;type=packages&amp;#x26;query=systemd.services.&quot;&gt;NixOS option search&lt;/a&gt; if you wish to learn more. For our purposes, just
&lt;code&gt;serviceConfig&lt;/code&gt; is enough as we will be working primarily with the &lt;code&gt;[Service]&lt;/code&gt;
field of systemd services. Documentation is very scattered, but available at
&lt;code&gt;systemd.unit(5)&lt;/code&gt;, &lt;code&gt;systemd.service(5)&lt;/code&gt; and &lt;code&gt;systemd.exec(5)&lt;/code&gt; manpages. They
contain many important tidbids, and I encourage you to go through them alongside
this post during your hardening journey.&lt;/p&gt;
&lt;p&gt;Most services enabled in NixOS, such as Miniflux under &lt;code&gt;services.*&lt;/code&gt;, are
essentially abstractions over &lt;code&gt;systemd.services&lt;/code&gt; When you enable a service,
you&apos;re essentially creating a Systemd service unit via
&lt;code&gt;systemd.services.&amp;#x3C;name&gt;&lt;/code&gt;. Unfortunately, there are limited options (usually
limited only to application-specific settings) to harden services through
&lt;code&gt;services.*&lt;/code&gt; options. Thus, we often need to strip away the abstraction and
modify the settings for the Systemd services directly, using &lt;code&gt;serviceConfig.&lt;/code&gt;
You should also be aware that some services create more than one service unit,
in which case &lt;code&gt;systemctl list-units --type=service&lt;/code&gt; can help you find more
services associated with one module.&lt;/p&gt;
&lt;p&gt;Here is a very basic example to demonstrate how individual options are applied
to a service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  systemd.services.&quot;&amp;#x3C;serviceName&gt;&quot;.serviceConfig = {
    ProtectClock = true;
    ProtectKernelTunables = true;
    ProtectKernelModules = true;
    ProtectKernelLogs = true;
    SystemCallFilter = &quot;~@clock @cpu-emulation @debug @obsolete @module @mount @raw-io @reboot @swap&quot;;
    ProtectControlGroups = true;
    RestrictNamespaces = true;
    LockPersonality = true;
    MemoryDenyWriteExecute = true;
    RestrictRealtime = true;
    RestrictSUIDSGID = true;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Options set in this example usually enough to bring a service down to MEDIUM
exposure level. It removes some of the &quot;unnecessary&quot; permissions--although they
may very well be necessary-- all services are granted by default. In short this
example &lt;em&gt;does&lt;/em&gt; meet &lt;em&gt;the basic requirements&lt;/em&gt; for hardening, but it is also not
very comprehensive. For services that reach more into the system, hardening must
be done more diligently: you must assess the needs of your service and tweak its
capabilities accordingly. Furthermore, you will be able to bring the score down
from MEDIUM by focusing on the needs of service and applying other configuration
fields.&lt;/p&gt;
&lt;p&gt;To check how effective the change was, run &lt;code&gt;systemd-analyze security &amp;#x3C;unit&gt;&lt;/code&gt;
before and after applying service changes, and compare the results. The output
of &lt;code&gt;systemd-analyze security&lt;/code&gt; will also provide in-depth explanation of each
option that is (or is not) set, so it is worth running frequently on services
that you aim to harden.&lt;/p&gt;
&lt;h4&gt;Definitions&lt;/h4&gt;
&lt;p&gt;While we are at it, let&apos;s talk about definitions. There too many options for me
to cover, and as such this section &lt;strong&gt;will not&lt;/strong&gt; cover each and every once of
them. Instead, I would like to focus on the example I have given above to
establish some baseline for how you may look at hardening.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;Option&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectClock&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Prevents procee from accessing the system clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectKernelTunables&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Restricts modification of kernel parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectKernelModules&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Prevents loading of kernel modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectKernelLogs&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Limits access to kernel log messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectHome&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Mounts /home, /root and /run/user as read-only tmpfs fs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectSystem&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Makes &lt;code&gt;/boot&lt;/code&gt;, &lt;code&gt;/etc&lt;/code&gt;, and &lt;code&gt;/usr&lt;/code&gt; directories &lt;strong&gt;read-only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;SystemCallFilter&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Filters out specific system calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;ProtectControlGroups&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Restricts use of control groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;RestrictNamespaces&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Limits process namespaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;LockPersonality&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Prevents changing process personality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;MemoryDenyWriteExecute&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Disallows write-execute memory mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;RestrictRealtime&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Limits real-time scheduling capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;RestrictSUIDSGID&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Restricts SUID/SGID binaries&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These are the definitions for some common directives that you may apply with
&lt;em&gt;minimum headache&lt;/em&gt;. You may stick those into the &lt;code&gt;serviceConfig&lt;/code&gt; of a service in
your configuration, and if the service is a basic daemon that does not need
intricate FS or memory access, it should perform as expected.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The draft &lt;a href=&quot;https://wiki.archlinux.org/title/User:NetSysFire/systemd_sandboxing#Common_directives&quot;&gt;Systemd Sandboxing Article&lt;/a&gt; on Archwiki provides some insight on
other options and their level of impact.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are many directives that are used in Systemd services, with various
exposure scores. While the scores differ, some of them might come in very handy.
Here are a few that are worth mentioning while you work on hardening your
services.&lt;/p&gt;
&lt;h5&gt;InaccessiblePaths&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;InaccessiblePaths&lt;/code&gt; is a very useful directive, which I did not about until
recently, that might come in handy if the service requires access to a lot of
directives. In which case, you may manually add &lt;em&gt;sensitive&lt;/em&gt; directories that you
want hidden at all costs.&lt;/p&gt;
&lt;h5&gt;DynamicUsers&lt;/h5&gt;
&lt;p&gt;Systemd &lt;em&gt;system&lt;/em&gt; services (as opposed to user services) run as root unless they
are explicitly given a user to run as. &lt;code&gt;DynamicUsers&lt;/code&gt; is a special directive
that can help you with &lt;em&gt;dynamically&lt;/em&gt; (look I said the thing!) creating separate
user accounts for each instance of the service. Each of these unique users runs
their own instance of the service, providing a high level of isolation between
different processes. This not only enhances security by limiting the potential
impact of a compromised process but also improves resource management by
isolating each instance&apos;s file system access. Although the impact of
&lt;code&gt;DynamicUsers&lt;/code&gt; on a service&apos;s exposure score is low, it is a very handy
directive that you might consider if root privileges are not necessary for your
service.&lt;/p&gt;
&lt;h5&gt;SystemCallFilter&lt;/h5&gt;
&lt;p&gt;One noteworthy directive is &lt;code&gt;SystemCallFilter&lt;/code&gt;. As its name indicates, this
Directive allows restricting syscalls that a service can call. This is very
tricky to work with, and you can get out of hand as the process gains new
features over time. Systemd, to make your life a &lt;em&gt;bit&lt;/em&gt; easier, includes
so-called &quot;groupings&quot;-- several groups of system calls, all prepended with &lt;code&gt;@&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@swap&lt;/code&gt;, &lt;code&gt;@reboot&lt;/code&gt;, &lt;code&gt;@memlock&lt;/code&gt; and &lt;code&gt;@raw-io&lt;/code&gt; are some examples of those groups.
You may find a more detailed explanation
&lt;a href=&quot;https://linux-audit.com/systemd/settings/units/systemcallfilter/&quot;&gt;on linux-audit.com&lt;/a&gt;&lt;/p&gt;
&lt;h5&gt;IP Accounting&lt;/h5&gt;
&lt;p&gt;With Systemd 235, Systemd was granted ability to track network traffic
statistics for individual services or units. It allows administrators to monitor
bandwidth usage, packet counts, and other network-related metrics associated
with specific systemd services. This feature is implemented through the
&lt;code&gt;IPAddressAllow&lt;/code&gt; and &lt;code&gt;IPAddressDeny&lt;/code&gt; directives in the &lt;code&gt;[Service]&lt;/code&gt;section of a
service unit file.&lt;/p&gt;
&lt;p&gt;By enabling IP accounting, systemd can provide detailed insights into network
activity, facilitating better network management, troubleshooting, and
performance optimization for services running under its control. Network
security is a little out of my scope today, but I encourage you to read the
&lt;a href=&quot;https://0pointer.net/blog/ip-accounting-and-access-lists-with-systemd.html&quot;&gt;awesome article on IP accounting&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Application&lt;/h4&gt;
&lt;p&gt;From the previous section, you should have a basic understanding of directives
we will be using to harden services. Now let&apos;s take a look at the application of
those directives.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Systemd&apos;s error messages for when a service is misconfigured can be vague or
misleading, especially if the executable fails to properly inform the user of
the error. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; Setting the log level temporarily to debug via
&lt;code&gt;systemctl log-level debug&lt;/code&gt; may help getting actually relevant information.
Though do not rely too much on debug information, as it is usually equally
useless.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As mentioned above, you might isolate services that need hardening with
&lt;code&gt;systemd-analyze security &amp;#x3C;unit&gt;&lt;/code&gt; and focus on hardening each and every one of
them. For example, &lt;code&gt;systemd-analyze security acpid&lt;/code&gt; returns for me something
like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;✗ RootDirectory=/RootImage=                                   Service runs within the host&apos;s root directory                                0.1
  SupplementaryGroups=                                        Service runs as root, option does not matter
  RemoveIPC=                                                  Service runs as root, option does not apply
✗ User=/DynamicUser=                                          Service runs as root user                                                    0.4
✗ CapabilityBoundingSet=~CAP_SYS_TIME                         Service processes may change the system clock                                0.2
✗ NoNewPrivileges=                                            Service processes may acquire new privileges                                 0.2
✓ AmbientCapabilities=                                        Service process does not receive ambient capabilities
✗ PrivateDevices=                                             Service potentially has access to hardware devices                           0.2
✗ ProtectClock=                                               Service may write to the hardware clock or system clock                      0.2
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have omitted the rest of the output, but you should get a gist of the output
from this example. First field is the directive, second is the impact of the
currently set value and the third field is the &quot;vulnerability score&quot; assigned to
the service. Higher the score, more vulnerable the service. Acpid, for example,
as a score of &lt;strong&gt;9.6&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Acpid is, of course, not the only example. On my system, there are little over
50 systemd services running and a majority of them were ranked EXPOSED, UNSAFE
or MEDIUM until I went out of my way to harden each and every single one of
them. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; My advice to you is to do the same: there are some Nix-based projects
that try and handle hardening for you, but remember that there is no solution
that suits all systems. Not to mention that relying on a 3rd party project for
your system hardening is a security vulnerability of its own.&lt;/p&gt;
&lt;p&gt;I find that &lt;a href=&quot;https://git.sr.ht/~krathalan/systemd-sandboxing&quot;&gt;krathalan&apos;s systemd sandboxing template&lt;/a&gt; serves as a good base that
you may apply with minimal tweaks. In addition to providing templates for common
services, it explains how certain options might interact with each other or
system settings while set. Based on this template, manpages and the directives
I&apos;ve described (or linked) above; you should be able to go over each and every
service that you find to be vulnerable. Keep in mind that it is crucial that you
keep at least &lt;em&gt;one&lt;/em&gt; stable generation on your system, as hardening can mess with
even your user accounts, or TTYs as they are &lt;em&gt;also&lt;/em&gt; managed by Systemd.&lt;/p&gt;
&lt;h4&gt;Examples&lt;/h4&gt;
&lt;p&gt;There are many services that score high in Systemd&apos;s exposure analysis, but it
is impossible for me to provide configuration options for each service. Instead,
I&apos;ll provide some examples for you to base your work off of. Using the
information and sources I have referenced, it should not be very difficult to
harden each individual service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  systemd.services.acpid.serviceConfig = {
    ProtectSystem = &quot;full&quot;;
    ProtectHome = true;
    RestrictAddressFamilies = [ &quot;AF_INET&quot; &quot;AF_INET6&quot; ];
    SystemCallFilter = &quot;~@clock @cpu-emulation @debug @module @mount @raw-io @reboot @swap&quot;;
    ProtectKernelTunables = true;
    ProtectKernelModules = true;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  systemd.services.power-profiles-daemon.serviceConfig = {
    ProtectHome = true;
    ProtectClock = true;
    ProtectKernelTunables = true;
    ProtectKernelModules = true;
    ProtectKernelLogs = true;
    SystemCallFilter = &quot;~@clock @cpu-emulation @debug @obsolete @module @mount @swap&quot;;
    ProtectControlGroups = true;
    RestrictNamespaces = true;
    LockPersonality = true;
    MemoryDenyWriteExecute = true;
    RestrictRealtime = true;
    RestrictSUIDSGID = true;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is based on the example I have shown above, and should serve as a good base
for &lt;em&gt;most&lt;/em&gt; services.&lt;/p&gt;
&lt;h2&gt;Journal Hardening&lt;/h2&gt;
&lt;p&gt;It is no secret that Systemd services run with very lax permissions. One thing
that is less often talked about is the Systemd journal, Journald.&lt;/p&gt;
&lt;p&gt;As the primary logging mechanism for systemd-based distributions, Journald
captures vast amounts of system and application data. However, this collection
of information also presents significant security risks if left unprotected.
This is why we must also take a look at hardening the system journal, which I
find is essential for maintaining system integrity, protecting sensitive data,
and ensuring compliance with security standards.&lt;/p&gt;
&lt;p&gt;There are various methods through which Journald can leak sensitive information.
The threat model for the Journal may differ based on your distribution (i.e.,
different distributions ship different configurations for the system journal),
but as a general rule of thumb you should consider storing the system journal on
encrypted storage, with proper permissions (e.g., &lt;code&gt;640&lt;/code&gt;) to protect it from
unauthorized inquiries. Additionally, if you have configured Journald to send
logs over the network, then then proper encryption is mandatory, or the data may
be intercepted during transmission. Do treat logfiles like toxic waste, and
handle them with care.&lt;/p&gt;
&lt;p&gt;As a precaution, you might consider using volatile storage for the system
journal as such:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  services.journald = {
    storage = &quot;volatile&quot;; # Store logs in memory
    upload.enable = false; # Disable remote log upload (the default)
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;man 5 journald.conf&lt;/code&gt; provides additional insight on options you may consider
setting through &lt;code&gt;services.journald.extraConfig&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Last but not least, a dedicated enough attacker may attempt to run your system
out of resources by filling your journal (e.g., if service output is forwarded
to the journal) with bogus logs. In which case &lt;code&gt;SystemMaxUse&lt;/code&gt; is a very useful
option to set.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Remember that no amount of service hardening constitutes a bullet-proof security
layer. Also remember that those scores assigned by Systemd are arbitrary. A
service can be &quot;safe&quot; in the oblivious eyes of Systemd, but may risk security or
privacy in other ways. While hardening services, consider the needs and attack
vectors of each service that you are looking at.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;p&gt;I am leaving here the resources I have consulted in the past. This post aims to
serve as a digestible summary of those resources as well as a guide to hardening
your system, but do feel free to consult them at any time. Most of them extend
beyond the scope of Systemd hardening, and will come up again in future posts.
Visit them at your own discretion, you are sure to learn something new.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Sandboxing&quot;&gt;&lt;code&gt;man 5 systemd.exec&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://security.stackexchange.com/questions/209529/what-does-enabling-kernel-unprivileged-userns-clone-do&quot;&gt;Stackexchange on unprivileged userns clone&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/User:NetSysFire/systemd_sandboxing&quot;&gt;Archwiki: Systemd Sandboxing by NetSysFire&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://madaidans-insecurities.github.io/&quot;&gt;Madaidans Insecurities&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://madaidans-insecurities.github.io/security-privacy-advice.html&quot;&gt;Madaidan&apos;s General Security Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://privsec.dev/posts/linux/desktop-linux-hardening/&quot;&gt;Privsec on Desktop Linux hardening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Kicksecure/security-misc&quot;&gt;Kicksecure Security&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kicksecure.com/wiki/Hardened-kernel&quot;&gt;Kicksecure Hardened Kernel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/secureblue/secureblue&quot;&gt;Secureblue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GrapheneOS/infrastructure&quot;&gt;GrapheneOS Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.nixos.org/wiki/Systemd/Hardening&quot;&gt;NixOS Wiki on Systemd Hardening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alegrey91/systemd-service-hardening&quot;&gt;General tips on Systemd Hardening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/k4yt3x/sysctl/blob/master/sysctl.conf&quot;&gt;K4YT3X&apos;s Hardened &amp;#x26; Optimized Linux Kernel Parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sshaudit.com/hardening_guides.html&quot;&gt;Notes on SSH Hardening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix&quot;&gt;Hardened Profile module in Nixpkgs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Security is a meaningless term without a threat model.&lt;/strong&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;SUID (Set User ID) and GUID (Set Group ID) binaries are special
executables that run with elevated privileges. When executed, they
temporarily assume the privileges of the owner or group specified in their
file attributes. This allows certain programs to perform operations that
would otherwise require root-level access, such as changing system settings
or accessing restricted files. However, this capability can be misused if
not properly implemented, therefore it is often restricted through systemd
unit options like &lt;code&gt;RestrictSUIDSGID&lt;/code&gt;. &lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.notashelf.dev/posts/2025-01-02-well-done-verbosity.html&quot;&gt;Hint hint wink wink&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Okay maybe not &lt;em&gt;all&lt;/em&gt; of them. It is not feasible to try and harden each
service, as the minimum required conditions to run some services will always
remain unsafe by Systemd&apos;s definition. This is what I have meant when I
referred to the scores as arbitrary. Services that run as root, for example,
can never be fully hardened as running as root is a security vulnerability
on its own. Trying to offload the service to a dedicated user, or using
&lt;code&gt;DynamicUser&lt;/code&gt; might break functionality, therefore some services are bound
to remain exposed as per Systemd. &lt;a href=&quot;https://notashelf.dev/posts/insecurities-remedies-i#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>linux</category><category>security</category><category>nixos</category><category>tutorial</category></item><item><title>Signing Git commits with SSH keys</title><link>https://notashelf.dev/posts/ssh-signing-commits</link><guid isPermaLink="true">https://notashelf.dev/posts/ssh-signing-commits</guid><description>Setting up SSH signing for your many, many Git commits</description><pubDate>Mon, 24 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.gitbutler.com/git-tips-and-tricks/&quot;&gt;https://blog.gitbutler.com/git-tips-and-tricks/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting up SSH signing for Git commits&lt;/h2&gt;
&lt;p&gt;For the longest of times, I have been running an obscure GPG setup using
Yubikeys that requires me to go back to my notes every time I wish to replicate
it on a new machine or a friend&apos;s machine to help them set up GPG signing on
their system. Today, after roughly 6 years of pushing GPG signed commits, I have
learned about SSH signing&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/ssh-signing-commits#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, and have decided to share my notes on setting it
up.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A basic prerequisite is that you need your SSH key pair added to the Git
service you will be using e.g., Github or Forgejo, so that you are able to
push and pull via SSH. &lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account&quot;&gt;Github Documentation&lt;/a&gt; on SSH keys shows you how to set
that up, and I imagine other platforms are not that more complicated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Traditional Distros&lt;/h3&gt;
&lt;p&gt;If you are using a traditional distribution like Fedora or Arch&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/ssh-signing-commits#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; then the
setup consists of two commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global gpg.format ssh # tell Git to use SSH signing keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Followed by the command to tell Git which key to use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git config --global user.signingkey ~/.ssh/my-key.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you wish to sign commits of a specific repository, then you may run the above
command in said repository and omit &lt;code&gt;--global&lt;/code&gt; to store the local (repository)
configuration in &lt;code&gt;.git/&lt;/code&gt; directory of the repository you can the command in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Remember to replace the example path I&apos;ve used above with the path to your
&lt;strong&gt;public&lt;/strong&gt; key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now it is time to tell Github about your signing key. Add your public key to
Github again, but this time change the key type to &quot;Signing Key&quot;. The process
should once again be similar for other public Git services.&lt;/p&gt;
&lt;p&gt;Last, you will want to actually use the signing key that you have configured Git
to use for &lt;em&gt;signed&lt;/em&gt; commits. That&apos;s right, all commits are unsigned until you
either pass &lt;code&gt;-S&lt;/code&gt; to &lt;code&gt;git commit&lt;/code&gt; or set
&lt;code&gt;git config --global commit.gpgsign true&lt;/code&gt;. Similarly, you can also sign selected
tags with &lt;code&gt;git tag -s&lt;/code&gt;. To sign &lt;em&gt;all&lt;/em&gt; git tags, you must run
&lt;code&gt;git config --global tag.gpgsign true&lt;/code&gt;, or the same command without &lt;code&gt;--global&lt;/code&gt;
to sign tags in a local repository.&lt;/p&gt;
&lt;h3&gt;On NixOS&lt;/h3&gt;
&lt;p&gt;In typical NixOS fashion, such a trivial process is mind-numbingly easy. You
will want to configure &lt;code&gt;programs.git&lt;/code&gt; under either NixOS or Home-Manager module
systems. As Git is rather a userspace application, I choose to use the one under
home-manager but NixOS also allows you to configure git globally with
&lt;code&gt;/etc/gitconfig&lt;/code&gt; being used. Please note that the steps will change slightly if
you are using the Git module under NixOS&apos; module system.&lt;/p&gt;
&lt;p&gt;First you would like to add &lt;code&gt;programs.git.signing&lt;/code&gt; to your home.nix as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# home.nix
programs.git = {
  signing = {
    key = &quot;${config.home.homeDirectory}/.ssh/my-key.pub&quot;;
    signByDefault = true;
  };

  extraConfig.gpg.format = &quot;ssh&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will be enough to tell Git where to look while also setting
&lt;code&gt;signByDefault&lt;/code&gt;, meaning all of your commits will be signed by default,
equivalent of passing &lt;code&gt;--gpg-sign=&amp;#x3C;key id&gt;&lt;/code&gt;. Great!&lt;/p&gt;
&lt;p&gt;If you wish to take this a bit further, you may consider setting allowed signers
to decide who can and cannot sign using the &lt;code&gt;ssh.allowedSignersFile&lt;/code&gt; option
under &lt;code&gt;extraConfig.gpg&lt;/code&gt;. Do keep in mind, however, that you need to have set an
allowed signers file beforehand. To do so, expand your &lt;code&gt;home.nix&lt;/code&gt; with the new
options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# home.nix
{config, pkgs, ...}: let
  key = &quot;&amp;#x3C;your public key&gt;&quot;;
  email = &quot;&amp;#x3C;your email&gt;&quot;;

  # Write signersFile to /nix/store/&amp;#x3C;store path&gt;
  signersFile = pkgs.writeText &quot;git-allowed-signers&quot; &apos;&apos;
    ${email} namespaces=&quot;git&quot; ${key}
  &apos;&apos;;
in {
  xdg.configFile.&quot;git/allowed_signers&quot;.source = signersFile;

  programs.git = {
    signing = {
      key = &quot;${config.home.homeDirectory}/.ssh/my-key.pub&quot;;
      signByDefault = true;
    };

    extraConfig.gpg = {
      format = &quot;ssh&quot;;
      ssh.allowedSignersFile = signersFile;
    };
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In above example, we have written a symlink to &lt;code&gt;~/.config/git/allowed_signers&lt;/code&gt;
using &lt;code&gt;xdg.configFile&lt;/code&gt; and &lt;code&gt;pkgs.writeText&lt;/code&gt;. Then,
&lt;code&gt;programs.git.extraConfig.gpg.ssh.allowedSignersFile&lt;/code&gt; tells Git which users will
be allowed to sign commits based on commit email.&lt;/p&gt;
&lt;p&gt;And that is all! You may now switch Home-Manager generations with either
&lt;code&gt;nixos-rebuild switch&lt;/code&gt; (if you have Home-Manager as a NixOS module) or
&lt;code&gt;home-manager switch&lt;/code&gt; (if you are using home-manager in standalone mode). Do
make sure to adapt the example to your own setup.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;To be fair, I have known about SSH signing for a while now, but the
sunk-cost fallacy has prevented me from ever looking into it. As a matter of
fact, I am still not convinced by it, but that is because I still use a
Yubikey based GPG setup which partially obsoletes SSH signing and prevents
me from looking further into the matter. Regardless, this documents my
experience with SSH signing, so I may return anytime. &lt;a href=&quot;https://notashelf.dev/posts/ssh-signing-commits#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Please don&apos;t. &lt;a href=&quot;https://notashelf.dev/posts/ssh-signing-commits#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>tutorial</category><category>linux</category><category>git</category></item><item><title>Nix Remote Builders</title><link>https://notashelf.dev/posts/nix-remote-builders</link><guid isPermaLink="true">https://notashelf.dev/posts/nix-remote-builders</guid><description>Taking full advantage of Nix&apos;s scalability.</description><pubDate>Mon, 17 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nix is a package manager--nay, a &lt;em&gt;build tool&lt;/em&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; that is capable of scaling
both vertically and horizontally through its &lt;strong&gt;Binary Caches&lt;/strong&gt; and &lt;strong&gt;Remote
Builders&lt;/strong&gt; respectively.&lt;/p&gt;
&lt;p&gt;You are introduced to the binary cache as soon as the moment you begin the
installation process of NixOS. The ISO image you download, minimal or one of the
graphical flavours, is built by Hydra &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; as a result of a particular
combination of module option combinations in Nixpkgs. In this sense, NixOS is a
build artifact. It consists of many derivations that are put in a particular
configuration, and built in a particular way to result in a NixOS ISO image. The
images, or a common NixOS system, consist of many derivations.&lt;/p&gt;
&lt;p&gt;Although Nix has a strange concept of dependencies, let us try to count
everything that contributes to the building of your final system.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ nix-store -q --requisites /run/current-system | cut -d- -f2- | sort | uniq | wc -l
2956
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is by no means 100% accurate, but it gets somewhat close. My system is the
result of around 3000 separate derivations, that would need to be built if not
for the binary caches Nix pulls built derivations from. Similar to the
installation ISO images, Hydra builds and caches most of the derivations &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; so
that you may pull them from &lt;code&gt;https://cache.nixos.org&lt;/code&gt;. All things considered,
the binary caches are distributed (and load balanced) for global reach. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Remote Builders&lt;/h2&gt;
&lt;p&gt;The topic I wanted to talk about today is &lt;strong&gt;Remote Builders&lt;/strong&gt;, a less frequently
employed method for scaling. Remote builders require additional setup, as they
execute builds on an authorized machine rather than pull built results from a
cache. While building things that might not be available in a public cache, you
may offload build steps to a remote machine instead of using your current
machine in the case your build process requires compilation, and higher resource
usage. In this case, a machine with more resources can speed-up the process.&lt;/p&gt;
&lt;p&gt;The structure of Nix remote builders consists of two components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Local Machine&lt;/li&gt;
&lt;li&gt;Remote Machine(s)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is no limit to how many machines you may distribute your builds upon.&lt;/p&gt;
&lt;h3&gt;Remote Machine&lt;/h3&gt;
&lt;p&gt;For the sake of simplicity, let us assume that there is one remote builders that
you would like to utilize for your builds. Nix utilizes remote builders by
opening SSH connection to the target machine, so you must first have a user with
SSH access on the target machine. Let&apos;s call this user &lt;code&gt;builder&lt;/code&gt;. You must mark
this user as a &quot;trusted user&quot; (&lt;em&gt;users that have additional rights when
connecting to the Nix daemon, such as the ability to specify additional binary
caches, or to import unsigned NARs.&lt;/em&gt;) to utilize this user for remote builds.&lt;/p&gt;
&lt;p&gt;On a typical NixOS setup, you would do so by adding it to &lt;code&gt;trusted-users&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  nix.config.trusted-users = [&quot;builder&quot;]; # you may also use @groups instead
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you make sure that the &lt;code&gt;builder&lt;/code&gt; user on the remote machine is accessible
via SSH, and is marked as a trusted user, try offloading a build to the remote
machine to test your connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nix build nixpkgs#hello --rebuild --builders &quot;ssh://builder@ssh.example.tld&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can replace &lt;code&gt;ssh.example.tld&lt;/code&gt; with a domain pointing at your server, or just
the raw IP address of your machine. If SSH is running a port other than &lt;code&gt;22&lt;/code&gt; on
your machine, then you may also specify an alternate port to be used with the
&lt;code&gt;NIX_SSHOPTS&lt;/code&gt; environment variable. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export NIX_SSHOPTS=&quot;-p 2222&quot;
nix build nixpkgs#hello --rebuild --builders &quot;ssh://builder@ssh.example.tld&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Would open a connection over port &lt;code&gt;2222&lt;/code&gt; instead of the default &lt;code&gt;22&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Local Machine&lt;/h3&gt;
&lt;p&gt;This is a good time to warn you that sometimes, especially if your connection is
shaky, adding remote builders to the mix might slow things down. Remember that
everything built on the server will have to be sent back to your local machine
to complete the final closure. However, if you are positive that remote builders
are good addition and the network effects are negligible, then you may configure
Nix to utilize certain builders on each build.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  # https://wiki.nixos.org/wiki/Distributed_build
  nix.buildMachines = [
    {
      hostName = &quot;ssh.example.tld&quot;;
      sshUser = &quot;builder&quot;;
      # &apos;ssh-ng&apos; is faster if both machines are NixOS but falls flat if the
      # machine Nix will attempt a connection to is not NixOS. In such a case
      # you must use &apos;ssh&apos; instead.
      protocol = &quot;ssh-ng&quot;;

      # This can be an absolute path to a private key or it can be managed
      # with something like Agenix, or SOPS.
      sshKey = &quot;/home/user/.ssh/builder-rsa&quot;;

      # Systems for which builds will be offloaded.
      systems = [&quot;x86_64-linux&quot; &quot;i686-linux&quot;];

      # Default is 1 but may keep the builder idle in between builds
      maxJobs = 3;
      # How fast is the builder compared to your local machine
      speedFactor = 2;

      supportedFeatures = [&quot;big-parallel&quot; &quot;kvm&quot; &quot;nixos-test&quot;];
    }
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above configuration sets up &lt;code&gt;/etc/nix/machines&lt;/code&gt;. If you are setting up
remote builders on non-NixOS, you will need to manually construct the contents
of that file.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A cool trick, if you are self-hosting Hydra, is that Hydra can pick up remote
builders if you tell it to. How you can do this is documented in the Hydra
manual, but you might consider self-hosting Hydra on your desktop or a weaker
machine like a Raspberry Pi while utilizing a stronger machine for remote
builds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;nix.buildMachines&lt;/code&gt; also plays very nicely with your SSH configuration. For
example in the case of unique ports, you may use &lt;code&gt;programs.ssh.extraConfig&lt;/code&gt;. For
example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  programs.ssh.extraConfig = &apos;&apos;
    Host remote-builder
      User builder
      HostName ssh.builder.tld
      Port 2222
      IdentityFile /home/user/.ssh/builder-rsa # As before, Agenix will work here
  &apos;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Specifying your connection details in &lt;code&gt;programs.ssh.extraConfig&lt;/code&gt; is a more
sophisticated way of configuring the SSH connection that will be established on
remote builds. As such, you may omit &lt;code&gt;NIX_SSHOPTS&lt;/code&gt; and connection information.
For the command-line example, you may now use &lt;code&gt;protocol://host&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nix build nixpkgs#hello --rebuild --builders &quot;ssh://remote-builder&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there are more than one builder that you would like to use, lets call them
&lt;code&gt;remote-builder1&lt;/code&gt; and &lt;code&gt;remote-builder2&lt;/code&gt;, you can specify them in the
&lt;code&gt;--builders&lt;/code&gt; flag, separated by spaces.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nix build nixpkgs#hello --rebuild --builders &quot;ssh://remote-builder1 ssh://remote-builder2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;That should be everything you need to configure remote builders on your system.
If you have a Tailnet, via Tailscale, you may utilize MagicDNS or something
similar to simply query builders by hostname and partially skip the SSH
configuration. E.g., &lt;code&gt;ssh-ng://builder@my-host&lt;/code&gt; where my-host is available on
your Tailnet, and systemd-resolved is asked to search your Tailnet domain.&lt;/p&gt;
&lt;p&gt;In the case your local system is too weak, you may also consider passing
&lt;code&gt;--max-jobs 0&lt;/code&gt; to the build command to execute &lt;em&gt;all&lt;/em&gt; build tasks on your
available remote machines. I pass &lt;code&gt;--max-jobs&lt;/code&gt; to the &lt;code&gt;nixos-rebuild&lt;/code&gt; command
when I am attempting a rebuild on my weakest machine, which only has 4gb RAM
available, to avoid running my system out of resources.&lt;/p&gt;
&lt;p&gt;A very cool, but somewhat unrelated trick, is that you can use &lt;code&gt;--build-host&lt;/code&gt; in
&lt;code&gt;nixos-rebuild&lt;/code&gt; to build on a remote host.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Nix, although most commonly referred to as a &quot;Package Manager&quot; is actually
a build tool. Some find this definition pedantic, but package management is
only a subset of what it is actually capable of. Calling it a package
manager is fair, but also a bit reductive in a way that it does not
communicate Nix&apos;s advantages over traditional package managers. &lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Hydra is a Nix-based continuous build system, tasked for evaluating and
building derivations from a jobset such as but not limited to Nixpkgs. In
the context of Nixpkgs, Hydra is what populates the cache, logs evaluation
or build failures, and makes channels available. &lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;&lt;em&gt;Some&lt;/em&gt; derivations are not built and therefore not available in the cache.
There are a few different conditions for this, but it is the case especially
when the derivation has been marked specifically not to be built by Hydra. &lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;This should show you how fragile the Nix infrastructure is. As much as I
am a huge fan of Nix, Nixpkgs and NixOS, without the proper funding Nix&apos;s
adoption would not be possible. &lt;em&gt;Nobody&lt;/em&gt; wants to build everything from
source, all of the time. Even though content-addressed Nix would make
rebuilds less troublesome, this is a problem to ponder on while considering
the future of Nix and NixOS. &lt;a href=&quot;https://notashelf.dev/posts/nix-remote-builders#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>nix</category><category>nixos</category><category>tutorial</category><category>linux</category></item><item><title>Full Disk Encryption and Impermanence on NixOS</title><link>https://notashelf.dev/posts/impermanence</link><guid isPermaLink="true">https://notashelf.dev/posts/impermanence</guid><description>Notes on setting up Impermanence with Full Disk Encryption on a NixOS system</description><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Impermanence is an interesting concept. Aside from its philosophical aspect, it
has strange but pleasant implications on a NixOS system. Not only is it a
display of sheer flexibility of NixOS as an operating system, it also provides a
system that cleans itself up on each reboot. Gone are the days of manually
clearing useless state some programs have left behind.&lt;/p&gt;
&lt;p&gt;This general setup concept utilizes NixOS&apos; ability to boot off of a disk that
contains only &lt;code&gt;/nix&lt;/code&gt; and &lt;code&gt;/boot&lt;/code&gt;, linking appropriate devices and blocks during
the boot process and deleting all state that programs may have left over my
system. The end result, for me, was a fully encrypted system that uses BTRFS
snapshots to restore &lt;code&gt;/&lt;/code&gt; to its original state on each boot.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;p&gt;This post is based on, and inspired by several resources. I tried to cover
everything you might need to set up a stateless NixOS system on your machines.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nix-community/impermanence&quot;&gt;Impermanence repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://discourse.nixos.org/t/impermanence-vs-systemd-initrd-w-tpm-unlocking/25167&quot;&gt;This discourse post on Impermanence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://elis.nu/blog/2020/06/nixos-tmpfs-as-home&quot;&gt;This blog post on setting up Impermanence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://guekka.github.io/nixos-server-1/&quot;&gt;This other blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html&quot;&gt;And this post that the previous post is based on&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reproduction steps&lt;/h2&gt;
&lt;p&gt;I&apos;ve had to go through a few guides before I could figure out a set up that I
really like. The final decision was that I would have an encrypted disk that
restores itself to its former state during boot. Is it fast? Absolutely not. But
it sure as hell is cool. And stateless!&lt;/p&gt;
&lt;p&gt;To return the root (and only the root) we use a systemd service that fires
shortly after the disk is encrypted but before the root is actually mounted.
That way, we can unlock the disk, restore the disk to its pristine state using
the snapshot we have taken during installation and mount the root to go on with
our day.&lt;/p&gt;
&lt;h3&gt;Partitioning&lt;/h3&gt;
&lt;p&gt;First you want to format your disk. If you are really comfortable with bringing
parted to your pre-formatted disks, by all means feel free to skip this section.
I, however, choose to format a fresh disk. It is also possible to switch to an
impermanent setup from your current installation, but you will be better off
starting from scratch. Remember to make a backup of important state!&lt;/p&gt;
&lt;p&gt;Start by partitioning disks into several sections. For example: &lt;code&gt;sda1&lt;/code&gt;, &lt;code&gt;sda2&lt;/code&gt;
and &lt;code&gt;sda3&lt;/code&gt;. This will differ if you are using nvme disks, for example,
&lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Set the disk name to make it easier
DISK=/dev/sdx # replace this with the name of the device you are using
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now set up the &lt;code&gt;boot&lt;/code&gt; partition. This is where your bootloader will live. If you
intend to persist a lot of generations, you may want to allocate more space. For
example 2 GiB instead of just 1.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;parted &quot;$DISK&quot; -- mklabel gpt
parted &quot;$DISK&quot; -- mkpart ESP fat32 1MiB 1GiB
parted &quot;$DISK&quot; -- set 1 boot on # assumes UEFI

mkfs.vfat -n BOOT &quot;$DISK&quot;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set up a swap partition You may choose to omit swap if your system has
sufficient RAM available. My machine has 16GB available, so I choose to allocate
8GBs for swap for my setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;parted &quot;$DISK&quot; -- mkpart Swap linux-swap 1GiB 9GiB
mkswap -L SWAP &quot;$DISK&quot;2
swapon &quot;$DISK&quot;2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;I do in fact use swap in the civilized year of 2023&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/impermanence#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. At the cost of
disabling hibernation, I also choose to encrypt my swap. This guide will not
cover how you may do so, but &lt;code&gt;swapDevices.*.randomEncryption&lt;/code&gt; is a good start.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Encrypt your partition, and open it to make it available under
&lt;code&gt;/dev/mapper/enc&lt;/code&gt;. &lt;code&gt;enc&lt;/code&gt; will be your logical volume name, you may wish to give
it a different name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cryptsetup --verify-passphrase -v luksFormat &quot;$DISK&quot;3 # /dev/sda3
cryptsetup open &quot;$DISK&quot;3 enc # the name enc is arbitrary, rename if you wish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now partition the encrypted device block.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;parted &quot;$DISK&quot; -- mkpart primary 9GiB 100%
mkfs.btrfs -L NIXOS /dev/mapper/enc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mount the disk, and set up subvolumes. I use BTRFS for its flexibility and the
robustness. You may do something similar using ZFS, but I will not cover that
here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mount -t btrfs /dev/mapper/enc /mnt

# First we create the subvolumes, those may differ as per your preferences
btrfs subvolume create /mnt/root
btrfs subvolume create /mnt/home
btrfs subvolume create /mnt/nix
btrfs subvolume create /mnt/persist # some people may choose to put /persist in /mnt/nix, I am not one of those people.
btrfs subvolume create /mnt/log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have created the BTRFS subvolumes, it is time for the &lt;em&gt;readonly&lt;/em&gt;
snapshot of the root subvolume. This is what we will use to roll back our system
on each boot. Compared to &lt;code&gt;/&lt;/code&gt; on tmpfs, rolling back manually on boot has the
added advantage of offering a way to restore unsaved data if your system shuts
down abruptly. For example if you suddenly loose power, you may boot into a
recovery system to recover anything that would be saved on &lt;code&gt;/&lt;/code&gt;. This is an edge
case of course, but I prefer having the option.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;btrfs subvolume snapshot -r /mnt/root /mnt/root-blank

# Make sure to unmount, otherwise nixos-rebuild will try to remove /mnt
# and fail
umount /mnt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mounting&lt;/h3&gt;
&lt;p&gt;After the subvolumes are created, we mount them with the options that we want.
Ideally, on NixOS, you want the &lt;code&gt;noatime&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/impermanence#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; option and zstd compression,
especially on your &lt;code&gt;/nix&lt;/code&gt; partition.&lt;/p&gt;
&lt;p&gt;The following is my partition layout. If you have created any other subvolumes
in the step above, you will also want to mount them here. Below setup assumes
that you have been following the steps as is.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# /
mount -o subvol=root,compress=zstd,noatime /dev/mapper/enc /mnt

# /home
mkdir /mnt/home
mount -o subvol=home,compress=zstd,noatime /dev/mapper/enc /mnt/home

# /nix
mkdir /mnt/nix
mount -o subvol=nix,compress=zstd,noatime /dev/mapper/enc /mnt/nix

# /persist
mkdir /mnt/persist
mount -o subvol=persist,compress=zstd,noatime /dev/mapper/enc /mnt/persist

# /var/log
mkdir -p /mnt/var/log
mount -o subvol=log,compress=zstd,noatime /dev/mapper/enc /mnt/var/log

# Do not forget to mount the boot partition!
mkdir /mnt/boot
mount &quot;$DISK&quot;1 /mnt/boot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, finally, we create Nix generate the appropriate hardware configuration for
our setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nixos-generate-config --root /mnt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generated configuration will be available at &lt;code&gt;/mnt/etc/nixos&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before we move on, we need to add the &lt;code&gt;neededForBoot = true;&lt;/code&gt; to some mounted
subvolumes in &lt;code&gt;hardware-configuration.nix&lt;/code&gt;. It will look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# Do not modify this file!  It was generated by ‘nixos-generate-config&apos;
# and may be overwritten by future invocations.  Please make changes
# to /etc/nixos/configuration.nix instead.
{
  config,
  lib,
  pkgs,
  modulesPath,
  ...
}: {
  imports = [
    (modulesPath + &quot;/installer/scan/not-detected.nix&quot;)
  ];

  boot.initrd.availableKernelModules = [&quot;xhci_pci&quot; &quot;ahci&quot; &quot;usb_storage&quot; &quot;sd_mod&quot; &quot;rtsx_pci_sdmmc&quot;];
  boot.initrd.kernelModules = [];
  boot.kernelModules = [&quot;kvm-intel&quot;];
  boot.extraModulePackages = [];

  fileSystems.&quot;/&quot; = {
    device = &quot;/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada&quot;;
    fsType = &quot;btrfs&quot;;
    options = [&quot;subvol=root&quot;];
  };

  boot.initrd.luks.devices.&quot;enc&quot;.device = &quot;/dev/disk/by-uuid/82144284-cf1d-4d65-9999-2e7cdc3c75d4&quot;;

  fileSystems.&quot;/home&quot; = {
    device = &quot;/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada&quot;;
    fsType = &quot;btrfs&quot;;
    options = [&quot;subvol=home&quot;];
  };

  fileSystems.&quot;/nix&quot; = {
    device = &quot;/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada&quot;;
    fsType = &quot;btrfs&quot;;
    options = [&quot;subvol=nix&quot;];
  };

  fileSystems.&quot;/persist&quot; = {
    device = &quot;/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada&quot;;
    fsType = &quot;btrfs&quot;;
    options = [&quot;subvol=persist&quot;];
    neededForBoot = true; # &amp;#x3C;- add this
  };

  fileSystems.&quot;/var/log&quot; = {
    device = &quot;/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada&quot;;
    fsType = &quot;btrfs&quot;;
    options = [&quot;subvol=log&quot;];
    neededForBoot = true; # &amp;#x3C;- add this
  };

  fileSystems.&quot;/boot&quot; = {
    device = &quot;/dev/disk/by-uuid/FDED-3BCF&quot;;
    fsType = &quot;vfat&quot;;
  };

  swapDevices = [
    {device = &quot;/dev/disk/by-uuid/0d1fc824-623b-4bb8-bf7b-63a3e657889d&quot;;}
    # if you encrypt your swap, it&apos;ll also need to be configured here
  ];

  nixpkgs.hostPlatform = lib.mkDefault &quot;x86_64-linux&quot;;
  powerManagement.cpuFreqGovernor = lib.mkDefault &quot;powersave&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do keep in mind that the NixOS hardware scanner &lt;strong&gt;cannot&lt;/strong&gt; pick up your mount
options. Which means that you should specify the options (i.e &lt;code&gt;noatime&lt;/code&gt;) for
each BTRFS subvolume that you have created in &lt;code&gt;hardware-configuration.nix&lt;/code&gt;. You
can simply add them in the &lt;code&gt;options = [ ]&lt;/code&gt; list in quotation marks. I recommend
adding at least zstd compression, and optionally &lt;code&gt;noatime&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Installing&lt;/h3&gt;
&lt;p&gt;And that should be all. By this point you are pretty much ready to install with
your existing config. I generally use my configuration flake to boot, so there
is no need to make any revisions. If you are starting from scratch, you may
consider tweaking your configuration.nix before you install the system. An
editor, such as Neovim, or your preferred DE/wm make good additions to your
configuration.&lt;/p&gt;
&lt;p&gt;Once it&apos;s all done, take a deep breath and &lt;code&gt;nixos-install&lt;/code&gt;. Once the
installation is done, you&apos;ll be prompted for the root password and after that
you can reboot. Now you are running NixOS on an encrypted disk. Nice!&lt;/p&gt;
&lt;p&gt;Next up, if you are feeling &lt;em&gt;really&lt;/em&gt; fancy today, is to configure disk erasure
and impermanence.&lt;/p&gt;
&lt;h3&gt;Impermanence&lt;/h3&gt;
&lt;p&gt;To handle BTRFS snapshots and automatic rollbacks, I use a systemd service. This
requires systemd to be enabled in stage1. You may enable it with
&lt;code&gt;boot.initrd.systemd.enable = true;&lt;/code&gt;. The schema for a systemd service in initrd
is the same as &lt;code&gt;systemd.services&lt;/code&gt;, except &lt;code&gt;restartTriggers&lt;/code&gt; and &lt;code&gt;reloadTriggers&lt;/code&gt;
will not available.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  boot.initrd.systemd = {
    enable = true; # this enabled systemd support in stage1 - required for the below setup
    services.rollback = {
      description = &quot;Rollback BTRFS root subvolume to a pristine state&quot;;
      wantedBy = [&quot;initrd.target&quot;];

      # LUKS/TPM process. If you have named your device mapper something other
      # than &apos;enc&apos;, then @enc will have a different name. Adjust accordingly.
      after = [&quot;systemd-cryptsetup@enc.service&quot;];

      # Before mounting the system root (/sysroot) during the early boot process
      before = [&quot;sysroot.mount&quot;];

      unitConfig.DefaultDependencies = &quot;no&quot;;
      serviceConfig.Type = &quot;oneshot&quot;;
      script = &apos;&apos;
        mkdir -p /mnt

        # We first mount the BTRFS root to /mnt
        # so we can manipulate btrfs subvolumes.
        mount -o subvol=/ /dev/mapper/enc /mnt

        # While we&apos;re tempted to just delete /root and create
        # a new snapshot from /root-blank, /root is already
        # populated at this point with a number of subvolumes,
        # which makes `btrfs subvolume delete` fail.
        # So, we remove them first.
        #
        # /root contains subvolumes:
        # - /root/var/lib/portables
        # - /root/var/lib/machines

        btrfs subvolume list -o /mnt/root |
          cut -f9 -d&apos; &apos; |
          while read subvolume; do
            echo &quot;deleting /$subvolume subvolume...&quot;
            btrfs subvolume delete &quot;/mnt/$subvolume&quot;
          done &amp;#x26;&amp;#x26;
          echo &quot;deleting /root subvolume...&quot; &amp;#x26;&amp;#x26;
          btrfs subvolume delete /mnt/root
        echo &quot;restoring blank /root subvolume...&quot;
        btrfs subvolume snapshot /mnt/root-blank /mnt/root

        # Once we&apos;re done rolling back to a blank snapshot,
        # we can unmount /mnt and continue on the boot process.
        umount /mnt
      &apos;&apos;;
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;You may opt in for &lt;code&gt;boot.initrd.postDeviceCommands = lib.mkBefore &apos;&apos;&lt;/code&gt; as
&lt;a href=&quot;https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html&quot;&gt;this blog post&lt;/a&gt;
suggests. I opt-in for a Systemd service as a service is a more powerful
option of handling service dependencies. With postDeviceCommands, we would be
sticking some bash code haphazardly in the stage-1 script, with a systemd
service we will be holding granular control over the service order.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Implications &amp;#x26; Workarounds&lt;/h3&gt;
&lt;p&gt;An impermanent setup has certain implications, for example, some files such as
saved networks for network-manager will be deleted on each reboot. While a
little clunky, &lt;a href=&quot;https://github.com/nix-community/impermanence&quot;&gt;Impermanence&lt;/a&gt; is
a great solution to our problem. Impermanence exposes to our system an
&lt;code&gt;environment.persistence.&quot;&amp;#x3C;dirName&gt;&quot;&lt;/code&gt; option with its NixOS module, which we can
use to make certain directories or files permanent. My setup is as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# inputs needs to be added to your &apos;specialArgs&apos; in the lib.nixosSystem call.
# In some setups, individual inputs are passed to specialArgs directly and as
# such your setup may differ just a little bit. Note that if you are not using
# flakes, inputs will not be available at all. In which case, you must manually
# fetch impermanence with fetchTarball, or use the appropriate channel. I
# recommend using flakes since they offer a better UX overall.
{inputs, ...}: {
  imports = [inputs.impermanence.nixosModules.impermanence];

  environment.persistence.&quot;/persist&quot; = {
    directories = [
      &quot;/etc/nixos&quot;
      &quot;/etc/NetworkManager/system-connections&quot;
      &quot;/etc/secureboot&quot;
      &quot;/var/db/sudo&quot;
    ];

    files = [
      &quot;/etc/machine-id&quot;

      # Required for SSH. If you have keys with different algorithms, then
      # you must also persist them here.
      &quot;/etc/ssh/ssh_host_ed25519_key&quot;
      &quot;/etc/ssh/ssh_host_ed25519_key.pub&quot;
      &quot;/etc/ssh/ssh_host_rsa_key&quot;
      &quot;/etc/ssh/ssh_host_rsa_key.pub&quot;
      # if you use docker or LXD, also persist their directories
    ];
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that is pretty much it. If everything went well, you should now be telling
your friends about your new system boasting full disk encryption &lt;em&gt;and&lt;/em&gt; root
rollbacks. They, in turn, should be looking at you like cavemen. Arch users
could never. May they bask in their supreme glory.&lt;/p&gt;
&lt;h2&gt;Closing Notes&lt;/h2&gt;
&lt;p&gt;As interesting as Impermanence is, it is also a little risky. The constant new
changes Nixpkgs receives on a daily basis might mean that a service may suddenly
stop using the state directory that you have configured it to use, and move to a
directory that you have not yet persisted. While running such a set up, you must
make sure to pay close attention to your system and back up everything
accordingly. Make sure to persist important directories, and have backups ready
in case something changes without your knowledge.&lt;/p&gt;
&lt;h3&gt;Home Impermanence&lt;/h3&gt;
&lt;p&gt;Silly. &lt;code&gt;$HOME&lt;/code&gt; is where state belongs. I will not cover this, and I do not
encourage that you make your home directory impermanent. If you insist on
setting it up, you may use a similar systemd service in &lt;code&gt;systemd.services&lt;/code&gt; and
Impermanence&apos;s Home-Manager module.&lt;/p&gt;
&lt;h3&gt;Why?&lt;/h3&gt;
&lt;p&gt;Honestly, why not?&lt;/p&gt;
&lt;p&gt;Okay real answer. All imperative distributions suffer from something called
&lt;em&gt;configuration drift&lt;/em&gt;. It is when your system constantly moving forward, through
updates and over time, leaving junk files that no longer have any use but still
remain on your disk. This is sometimes a security vulnerability, and sometimes
just general annoyance. Impermanence completely eliminates configuration drift,
and makes sure that your system is sparkly clean on each boot. Cool, right?&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;I could be using &lt;code&gt;tmpfs&lt;/code&gt; for &lt;code&gt;/&lt;/code&gt; at this point in time. Unfortunately,
since I share this setup on some of my low-end laptops, I&apos;ve got no RAM to
spare - which is exactly why I have opted out with BTRFS. It is a reliable
filesystem that I am used to, and it allows for us to use a script that
we&apos;ll see later on. &lt;a href=&quot;https://notashelf.dev/posts/impermanence#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Read about noatime
&lt;a href=&quot;https://opensource.com/article/20/6/linux-noatime&quot;&gt;here&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/impermanence#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>nix</category><category>nixos</category><category>tutorial</category><category>linux</category></item><item><title>When, Why and How to Extend Nixpkgs&apos; Standard Library</title><link>https://notashelf.dev/posts/extended-nixpkgs-lib</link><guid isPermaLink="true">https://notashelf.dev/posts/extended-nixpkgs-lib</guid><description>Reasons why and how you might create your own extended library</description><pubDate>Sat, 01 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In plenty of cases, you might need to write your own custom functions and store
them somewhere. While it is conceptually possible and easy to define them inside
an argument you might want to stick in for example &lt;code&gt;specialArgs&lt;/code&gt; in the context
of a NixOS configuration, there are easier and more ergonomic ways of doing so.&lt;/p&gt;
&lt;h2&gt;What is &lt;code&gt;nixpkgs.lib&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;In the context of Nix/OS, &lt;code&gt;nixpkgs.lib&lt;/code&gt; refers to a module within the Nixpkgs
repository that acts as a collection of helpful functions and other utilities
designed around usage in Nixpkgs and by extension NixOS configurations. We often
use those functions to simplify our configurations and the Nix package build
processes. It is available as a top-level attribute as &lt;code&gt;nixpkgs.lib&lt;/code&gt;, but also
inside &lt;code&gt;pkgs&lt;/code&gt; as &lt;code&gt;pkgs.lib&lt;/code&gt;. While using &lt;code&gt;lib.nixosSystem&lt;/code&gt;, it is also added to
your system&apos;s &lt;code&gt;specialArgs&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/extended-nixpkgs-lib#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; so that you can add it to the argument set (i.e.
the line that goes &lt;code&gt;{pkgs, lib, ...}&lt;/code&gt; at the top of a file) in your NixOS
configuration easily.&lt;/p&gt;
&lt;h2&gt;Why would you need to extend &lt;code&gt;nixpkgs.lib&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;While the library functions provided by nixpkgs is quite extensive and usually
suits my needs, I sometimes feel the need to define my own function or wrap an
existing function to complete a task. Normally we can handle the process of a
function inside a simple &lt;code&gt;let in&lt;/code&gt; and be well off, but there may be times you
need to re-use the existing function across your configuration file. In such
times, you might want to either write your own lib and inherit it at the source
of your &lt;code&gt;flake.nix&lt;/code&gt; to then inherit them across your configuration.&lt;/p&gt;
&lt;h2&gt;Extending &lt;code&gt;nixpkgs.lib&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I find the easiest way of extending nixpkgs.lib to be using an &quot;overlay&quot;,
enabled by &lt;code&gt;makeExtensible&lt;/code&gt;. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/extended-nixpkgs-lib#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# The file path is arbitrary, let&apos;s assume that this is in
# lib/default.nix for the sake of simplicity.
{
  inputs,
  ...
}: inputs.nixpkgs.lib.extend (
    final: prev: {
      # Your functions go here
    }
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above structure takes the existing &lt;code&gt;lib&lt;/code&gt; from &lt;code&gt;nixpkgs&lt;/code&gt;, which you&apos;ll
remember is defined as &lt;code&gt;nixpkgs.lib&lt;/code&gt;, and appends your own extensions to it. You
may then import this library in your &lt;code&gt;flake.nix&lt;/code&gt; to pass it to other imports and
definitions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# flake.nix
flake = let
  # Get the extended lib from ./lib/default.nix
  lib = import ./lib {inherit inputs;};
in {
  # Then you may pass it around, e.g. in imports by adding `lib`
  # to the argument set.
  nixosConfigurations = import ./hosts {inherit nixpkgs self lib;};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, the extended library is imported from &lt;code&gt;lib/default.nix&lt;/code&gt; in
repository root where the overlay is defined. It is then passed around to make
the extended &lt;code&gt;lib&lt;/code&gt; available within all files called by &lt;code&gt;flake.nix&lt;/code&gt;. For
example, in &lt;code&gt;hosts/default.nix&lt;/code&gt; it could be added to &lt;code&gt;specialArgs&lt;/code&gt; to make the
extended library the default in a NixOS configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# hosts/default.nix
{lib, ...}: {
  # Since this is called by nixosConfigurations = ./import ...
  # we just add the configuration attributes here, one for each
  # new configuration.
  fooSystem = lib.nixosSystem {
    modules = [ ... ];
    specialArgs = {lib;};
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now any and all new functions defined in the extended library will become
available in any files called by &lt;code&gt;modules&lt;/code&gt;, thanks to &lt;code&gt;specialArgs&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;The problem with this approach is that it may be confusing for other people
reviewing your configuration. With this approach, &lt;code&gt;lib.customFunction&lt;/code&gt; looks
identical to any lib function, which may lead to people thinking the function
exists in nixpkgs itself while it is only provided by your configuration. This
is not a problem per se, but if this is something that bothers you then the
solution is simple though. Instead of extending &lt;code&gt;nixpkgs.lib&lt;/code&gt;, you may define
your own lib that does not inherit from &lt;code&gt;nixpkgs.lib&lt;/code&gt; and only contains your
functions. The process would be similar, and you would not need to define an
overlay.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# flake.nix
flake = let
    # extended nixpkgs lib, contains my custom functions
    lib&apos; = import ./lib {inherit nixpkgs lib inputs;};
in {
    # entry-point for nixos configurations
    nixosConfigurations = import ./hosts {inherit nixpkgs self lib&apos;;};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where your &lt;code&gt;lib/default.nix&lt;/code&gt; looks like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# lib/default.nix
{nixpkgs, ...}: {
  # Define your functions here as you would do in an extension
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Example Implementations&lt;/h2&gt;
&lt;p&gt;If you have defined your own custom library based on this post, feel free to add
a project or your configuration as an example here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/NotAShelf/nvf/blob/main/lib/stdlib-extended.nix&quot;&gt;nvf&apos;s extended library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/snugnug/micros/blob/50db7e1c8e1633566c43190976bf2f6ac43f12ff/flake.nix#L86&quot;&gt;MicrOS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/03ae77ee2d193531819ae43711c8f168c7051e7b/nixos/lib/eval-config.nix#L32&quot;&gt;https://github.com/NixOS/nixpkgs/blob/03ae77ee2d193531819ae43711c8f168c7051e7b/nixos/lib/eval-config.nix#L32&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/extended-nixpkgs-lib#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/NixOS/nixpkgs/blob/03ae77ee2d193531819ae43711c8f168c7051e7b/lib/default.nix#L10C9-L10C22&quot;&gt;https://github.com/NixOS/nixpkgs/blob/03ae77ee2d193531819ae43711c8f168c7051e7b/lib/default.nix#L10C9-L10C22&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/extended-nixpkgs-lib#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>nix</category><category>tutorial</category></item><item><title>Lazy Evaluation in Nix: Where Your Conditionals Go to Die</title><link>https://notashelf.dev/posts/not-for-the-lazy</link><guid isPermaLink="true">https://notashelf.dev/posts/not-for-the-lazy</guid><description>Exploring lazy evaluation in relative depth</description><pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you have spent time in traditional programming languages, then you have
probably relied on conditionals (&lt;code&gt;if-else&lt;/code&gt;, &lt;code&gt;switch&lt;/code&gt;, &lt;code&gt;case&lt;/code&gt;) at least once in
In Nix, as a result of lazy evaluation, many of those constructs become less
relevant less relevant---or outright unnecessary. This post dives into &lt;em&gt;why&lt;/em&gt;
this is the case, and hopes to save you from a few pitfalls that come with this.&lt;/p&gt;
&lt;h2&gt;What is Lazy Evaluation?&lt;/h2&gt;
&lt;p&gt;I first need to explain what Lazy evaluation is. Not just within the context of
Nix, but in programming in general.&lt;/p&gt;
&lt;p&gt;Lazy evaluation means that expressions are not evaluated until their values are
actually needed. This is in contrast to strict (or eager) evaluation, where
expressions are computed as soon as they are bound to a variable. In more
practical terms, lazy evaluation helps avoid unnecessary computations by
delaying the evaluation of expressions. It&apos;s a powerful technique for improving
performance and managing resources efficiently.&lt;/p&gt;
&lt;h3&gt;Laziness in Haskell&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-haskell&quot;&gt;lazyNumbers :: [Int]
lazyNumbers = [1..]

firstFiveNumbers :: [Int]
firstFiveNumbers = take 5 lazyNumbers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example &lt;code&gt;lazyNumbers&lt;/code&gt; is an infinite list. However, the values are not
computed all at once. The &lt;code&gt;take 5 lazyNumbers&lt;/code&gt; expression only computes the
first 5 elements of the list when required. That is the most typical example of
lazy evaluation I can think of. Key point is that the numbers are only generated
when needed, instead of being computed upfront.&lt;/p&gt;
&lt;h3&gt;Laziness in Python&lt;/h3&gt;
&lt;p&gt;With some friction, we can implement lazy iteration in Python.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def lazy_numbers():
    num = 1
    while True:
        yield num
        num += 1

lazy_gen = lazy_numbers()
first_five_numbers = [next(lazy_gen) for _ in range(5)]
print(first_five_numbers)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, &lt;code&gt;lazy_numbers()&lt;/code&gt; is a generator that lazily yields numbers
starting from 1. Unlike a typical &lt;em&gt;list&lt;/em&gt;, the numbers are not computed all at
once. The &lt;code&gt;next(lazy_gen)&lt;/code&gt; call only computes the next number when needed. So,
when we request the first five numbers, only the first five numbers are
generated.&lt;/p&gt;
&lt;p&gt;In Nix, laziness manifests in several ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unused branches of an if-else are never evaluated.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function arguments are not evaluated unless explicitly used.&lt;/strong&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attribute sets can include self-referential definitions, leading to infinite
recursion errors only when an attribute directly refers to itself.&lt;/strong&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Death of Conditionals (or at Least Their Diminished Role)&lt;/h2&gt;
&lt;p&gt;In strict languages, conditionals are usually used to prevent expensive
computations from running unnecessarily. But in Nix, the very nature of laziness
means that an expensive computation inside an unused branch never executes.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  expensive = builtins.trace &quot;This should not print!&quot; (throw &quot;Error&quot;);
  value =
    if false
    then expensive
    else &quot;Safe&quot;;
in
  value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In an eagerly evaluated language, this would result in an error because
&lt;code&gt;expensive&lt;/code&gt; would be computed before &lt;code&gt;if&lt;/code&gt; even runs. In Nix, however, the
&lt;code&gt;false&lt;/code&gt; branch is never evaluated, so the program executes safely.&lt;/p&gt;
&lt;h2&gt;Functions: Only Compute What is Needed&lt;/h2&gt;
&lt;p&gt;Since function arguments are evaluated lazily, unnecessary computations can be
avoided naturally.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  alwaysReturnsOne = x: 1;
in
  # The throw becomes the function argument here, which is x.
  # Since x is never evaluated, nor is the throw so the program
  # will continue.
  alwaysReturnsOne (throw &quot;This should never be evaluated&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though we pass an expression that would normally cause an error, it is
never evaluated since &lt;code&gt;x&lt;/code&gt; is not used inside the function body.&lt;/p&gt;
&lt;h2&gt;Self-Referencing With and Without Infinite Loops&lt;/h2&gt;
&lt;p&gt;Nix allows for self-referential structures, but only if evaluated lazily. Direct
cyclic dependencies will cause infinite recursion unless structured carefully.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  # rec is a special form in Nix used for defining recursive
  # attribute sets, allowing circular references to resolve
  # lazily without infinite loops. You will find out very
  # quickly, however, that it is often discouraged.
  infiniteRec = rec {
    a = b + 1;
    b = let
      # This would cause an infinite recursion if it was evaluated
      # but the variable `infrec` is never referenced, so it passes.
      infrec = a - 1;
    in
      42;
  };
in
  infiniteRec.a  # Evaluates safely
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because what would cause the infrec is never evaluated. If the
evaluator even touched the variable &lt;code&gt;infrec&lt;/code&gt;, the program would immediately face
and infinite recursion. Instead, it uses the fixed value &lt;code&gt;42&lt;/code&gt; from &lt;code&gt;b&lt;/code&gt; so &lt;code&gt;a&lt;/code&gt;
can refer to &lt;code&gt;b&lt;/code&gt; safely.&lt;/p&gt;
&lt;p&gt;Here is an example that would fail.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  infiniteRec = rec {
    a = b + 1;
    b = a - 1;
  };
in
  infiniteRec.a  # Infinite recursion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here both &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; depend on each other without a base case, which results in
infinite recursion. The only way to prevent this is to explicitly anchor one of
the values with a concrete number or expression that doesn&apos;t rely on the cycle.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Infinite recursion is not as straightforward as it looks here. While working
with small sets and short files you can &lt;em&gt;easily&lt;/em&gt; identify where the infinite
recursion comes from. While using the module system to break your code into
multiple files, or even repositories, you might have more difficulty
identifying where exactly it comes from, because you will notice that error
messages are as clueless as you are. We&apos;ll talk about the costs of deep
abstractions in another post.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;What is &lt;code&gt;fix&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fix&lt;/code&gt; is a function used in some functional languages (such as Haskell) to
define recursive expressions without requiring explicit naming. It applies a
function to itself, allowing the expression to refer to itself.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hs&quot;&gt;fix f = f (fix f)
fact = fix (\rec n -&gt; if n == 0 then 1 else n * rec (n - 1))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In strict (non-lazy) languages, recursion requires named definitions, such as
explicitly defining fact and referencing itself. Without laziness, a function
cannot pass itself as an argument to another function without being fully
evaluated first---leading to a situation where the function reference does not
exist at the time of evaluation. The fix function allows recursion to be encoded
explicitly by ensuring that a function can reference itself even in strict
evaluation contexts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  factorials = rec {
    zero = 1;
    fact = n: if n == 0 then zero else n * fact (n - 1);
  };
in
  factorials.fact 5  # Evaluates to 120
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, fact refers to itself without requiring an explicit fixed-point combinator
like &lt;code&gt;fix&lt;/code&gt;. This is because Nix only evaluates values when needed, meaning
references can exist without being immediately resolved. In contrast, strict
languages would require fix to explicitly establish recursion. &lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const factorial = (n) =&gt; (n === 0 ? 1 : n * factorial(n - 1));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, factorial is explicitly named, allowing it to refer to itself. However, if
we wanted to define it without naming it explicitly, we would need something
like &lt;code&gt;fix&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Why yes I&apos;ve used Javascript as an example for a good reason.
// The reason is that if I&apos;ve suffered with writing Haskell, then
// you must suffer reading Javascript. Ha.

// `fix` is a function that helps a function call itself
// It takes a function `f` as an argument and returns `f` applied to itself
const fix = (f) =&gt; f((x) =&gt; fix(f)(x));

const factorial = fix(
  (rec) =&gt; (n) =&gt;
    // Base case: if n is 0, return 1 (because 0! = 1)
    n === 0 ? 1 : n * rec(n - 1), // recursive case: multiply n by the factorial of (n-1)
);

console.log(factorial(5)); // 120
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, while fix is necessary for recursion in strict evaluation, Nix&apos;s lazy
evaluation makes it unnecessary, allowing for powerful self-referential
structures with &lt;em&gt;careful&lt;/em&gt; structuring.&lt;/p&gt;
&lt;h3&gt;Other Examples&lt;/h3&gt;
&lt;p&gt;To help give you an idea of what lazy evaluation can do for you, e.g. in your
NixOS configuration, I&apos;d like to demonstrate laziness in action.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lazy Lists&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Nix, you can define infinite data structures like lazy lists without causing
infinite loops:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  lazyList = {
    head = 1;
    tail = lazyList.tail; # this is an infinite recursion, if it evaluates...
  };
in
  lazyList.head
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Delayed Computation in Attribute Sets&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;let
  config = rec {
    setting =
      if useAdvanced
      then builtins.throw &quot;Too expensive!&quot;
      else &quot;default&quot;;
    useAdvanced = false;
  };
in
  # evaluates to &quot;default&quot;, error is never thrown
  config.setting
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Laziness is powerful, but it can lead to surprises:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debugging is harder because errors might not surface until a deeply nested
expression is finally evaluated. &lt;code&gt;--show-trace&lt;/code&gt; is your friend most of the
time.&lt;/li&gt;
&lt;li&gt;Performance tuning requires careful observation of when expressions are
actually computed.&lt;/li&gt;
&lt;li&gt;Memory usage can balloon if large unevaluated thunks pile up, leading to
unexpected memory pressure. Though, Nix&apos;s performance woes come from
elsewhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A very good question would be &lt;em&gt;&quot;why did you write this post?&quot;&lt;/em&gt; Truth is, I want
to make it clear that Nix is &lt;em&gt;not&lt;/em&gt; one of your traditional languages. There also
seems to be a trend of new Nix/OS users failing to understand Nix &lt;em&gt;is&lt;/em&gt; a
programming language, and it is a functional one no less. I wanted to make it
very clear that some of the &quot;issues&quot; (such as dreadfully long error messages)
are an occupational hazard.&lt;/p&gt;
&lt;p&gt;To conclude, laziness in Nix removes the need for many traditional control flow
constructs, albeit with its own set of caveats. Rather than guarding expensive
computations with explicit conditionals, you often don&apos;t need to worry at
all---unused expressions simply never evaluate. However, laziness introduces its
own challenges, especially when debugging or managing performance. I want to
make it very clear that most of the time, you can overcome those challenges by
simply thinking about how you structure your program. In short, Nix is very
demanding from the user but it almost always returns your investment in full.&lt;/p&gt;
&lt;p&gt;Next time you are working with Nix, remember that you are using a domain
specific language that is bound to have its own quirks. Regardless of said
quirks, Nix is a very powerful language that trivializes Infrastructure as Code,
but &lt;em&gt;only&lt;/em&gt; if you treat it as code.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Static analysis tools, such as &lt;a href=&quot;https://github.com/oppiliappan/statix&quot;&gt;Statix&lt;/a&gt; will warn you when the function
argument is unused. For example in the &lt;code&gt;alwaysReturnOne&lt;/code&gt; function I&apos;ve used
as an example above, Statix would&apos;ve warned you about the unused argument
&lt;code&gt;x&lt;/code&gt;. &lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;This is possible due to lazy evaluation resolving circular dependencies
without causing crashes. However, an infinitely recursing attribute can
exist as long as it&apos;s not evaluated. &lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;I&apos;m very well aware that there is a &lt;code&gt;fix&lt;/code&gt; function in nixpkgs lib. I
mainly want to focus on core Nix language, so applications of &lt;code&gt;lib.fix&lt;/code&gt; are
omitted this time around. I&apos;d like to talk about &lt;code&gt;fix&lt;/code&gt; and recursion
specifically in another post, another time. This is already long as it is. &lt;a href=&quot;https://notashelf.dev/posts/not-for-the-lazy#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>nix</category><category>programming</category></item><item><title>Stop Scraping my Git Forge!</title><link>https://notashelf.dev/posts/stop-scraping-my-forge</link><guid isPermaLink="true">https://notashelf.dev/posts/stop-scraping-my-forge</guid><description>Just one of those that should be illegal, but is not.</description><pubDate>Tue, 07 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Not so long ago, while performing maintenance on my VPS and setting up
Prometheus, Grafana, and related tools, I noticed something unexpected yet not
entirely surprising. My Nginx instance was under constant bombardment of
requests from a specific subnet. Public-facing servers are no strangers to port
scans, but the intensity and specificity of these requests caught my attention
and prompted me to investigate further.&lt;/p&gt;
&lt;h2&gt;Hello, Old Friend&lt;/h2&gt;
&lt;p&gt;I have parted ways with Facebook before its acquisition of Instagram. I hate
both of those platforms and the constant infringement of privacy they curse us
with. You understand my surprise when I looked at my Nginx webserver log and
found &lt;em&gt;thousands&lt;/em&gt; of requests coming my way; all with the user agent
&lt;code&gt;facebookexternalhit&lt;/code&gt;. More specifically, they were sending &lt;code&gt;GET&lt;/code&gt; requests to
&lt;em&gt;my personal Git forge&lt;/em&gt;---a service where I store &lt;em&gt;personal&lt;/em&gt; projects that I
prefer not to entrust platforms such as Microsoft Github. Over &lt;strong&gt;30,000&lt;/strong&gt; lines
were reserved to nothing but unsolicited &lt;code&gt;GET&lt;/code&gt; requests in my access log. &lt;strong&gt;What
the hell&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;Instinctively, I put together a quick Python script that follows the service
logs for Forgejo, which at the time also logged router events, and logs &lt;em&gt;unique&lt;/em&gt;
IPs to a single file so that I can ban those IPs with firewall rules, using the
trusty nftables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import subprocess
import re
import time
from datetime import datetime

logfile = &quot;unique_ips.log&quot;
ipv4_pattern = re.compile(r&quot;\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b&quot;)

def read_logged_ips():
    try:
        with open(logfile, &quot;r&quot;) as file:
            return set(file.read().splitlines())
    except FileNotFoundError:
        return set()


def log_ip(ip, timestamp, logged_ips):
    with open(logfile, &quot;a&quot;) as file:
        file.write(f&quot;{timestamp} - {ip}\n&quot;)
    logged_ips.add(ip)


def main():
    logged_ips = read_logged_ips()
    process = subprocess.Popen(
        [&quot;journalctl&quot;, &quot;-xeu&quot;, &quot;forgejo.service&quot;, &quot;-f&quot;],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )

    while True:
        line = process.stdout.readline()
        if &quot;404 Not Found&quot; in line:
            ip_match = ipv4_pattern.search(line)
            if ip_match:
                ip = ip_match.group()
                if ip not in logged_ips:
                    timestamp = datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)
                    log_ip(ip, timestamp, logged_ips)
                    print(f&quot;Logged new IP: {ip} at {timestamp}&quot;)


if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I left the script, aptly and unimaginatively named &lt;code&gt;catch.py&lt;/code&gt; running for around
an hour while I went to fix a nice meal for myself--- which you need to deal
with Meta&apos;s bullshit. When I came back, there were hundreds of &lt;em&gt;unique&lt;/em&gt; IPs in
the logfile. Not a handful, &lt;em&gt;hundreds&lt;/em&gt;. Not only that, but they were coming from
several different subnets. This means&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Meta is using several different providers (as evident from IP queries) to
simply send bogus requests to people&apos;s webservers.&lt;/li&gt;
&lt;li&gt;Meta is, in fact, &lt;em&gt;not&lt;/em&gt; respecting &lt;code&gt;robots.txt&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/stop-scraping-my-forge#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;They feel not at all inclined to maybe stop and think sending thousands of
requests to people&apos;s servers &lt;em&gt;is malicious&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;To my demise, banning each and every single one of those subnets would take
too long, and block possibly legitimate traffic.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Get Out of My Lawn&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;57.141.0.16 - - [22/Nov/2024:00:36:29 +0300] &quot;GET /NotAShelf/nyx/commits/commit/4c82e5ee05fabd315a6e5c656fd72e11c93c4cfd/homes/notashelf/services/wayland/hyprpaper HTTP/2.0&quot; 403 146 &quot;-&quot; &quot;meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an example of a request from my webserver logs. Take a look at the body.
They are scraping my &lt;em&gt;NixOS configuration&lt;/em&gt; that is available on my Git forge and
they are doing it &lt;strong&gt;commit by commit&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Following this unfortunate encounter, I have decided to start blocking any
requests coming with &lt;code&gt;meta-externalagent*&lt;/code&gt; as their user agent. Such requests
now receive a 403 Forbidden response as per my adjusted Nginx configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;# Define access and error log paths.
access_log /var/log/nginx/forgejo_access.log;
error_log /var/log/nginx/forgejo_error.log;

# Tell Facebook&apos;s crawlers to fuck right off.
if ($http_user_agent ~* &quot;^meta-externalagent(/[\d\.]+)?$&quot;) {
  return 403;
}

if ($http_user_agent ~* &quot;^facebookexternalhit(/[\d\.]+)?$&quot;) {
  return 403;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, surely this will work. There is no way that a webcrawler is designed badly
or maliciously enough to disregard hundreds of error response 403s, indicating
that they are &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403&quot;&gt;forbidden from&lt;/a&gt; accessing that URL. Surely?&lt;/p&gt;
&lt;p&gt;Here is a request from today, shortly after I started writing this post.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;57.141.0.17 - - [07/Jan/2025:13:12:06 +0300] &quot;GET /NotAShelf/nyx/raw/commit/93d16a3edbaa8177c76ed84c4c5b489649faa609/modules/common/options/default.nix HTTP/2.0&quot; 403 146 &quot;-&quot; &quot;meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, I am not sure if this incompetence or plain stupidity.&lt;/p&gt;
&lt;h2&gt;Ban Them All&lt;/h2&gt;
&lt;p&gt;This has been &lt;del&gt;yet another&lt;/del&gt; a technical rant and a gentle nudge to you that
you should check &lt;em&gt;your&lt;/em&gt; webserver logs to see if your content is being scraped
for, well, as long as it has been online.&lt;/p&gt;
&lt;p&gt;My Forgejo instance is now inaccessible to those who have not logged in---even
for public repositories--- and my Nginx configuration responds with 403 to any
requests coming from Meta&apos;s known crawlers. I have also tried sending an-email
to their webmaster as listed from the documentation URL attached to their
crawlers. It has now been 4 months since, but I am yet to receive a response, or
to see an indication of those crawlers stopping soon. Meanwhile, I am left to
deal with their recklessness on my own accord.&lt;/p&gt;
&lt;p&gt;Instead, I will resort to banning ALL of them with nftables using IP ranges. It
is going to be annoying, but not fruitless, to write another Python script to
identify &lt;em&gt;each and every single subnet&lt;/em&gt; those requests are coming from. Once I
have a list, it will be trivial to draft the rule. I suggest that you do the
same.&lt;/p&gt;
&lt;p&gt;Stay safe.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Meta&apos;s own web-crawler documentation indicates that they may choose to
disregard your &lt;code&gt;robots.txt&lt;/code&gt; if they are performing &quot;integrity or security
checks&quot; which in truth is just a vague way to say that you may sometimes
decide to &lt;em&gt;not&lt;/em&gt; play by the rules. At this point your bet is banning them by
IP range, which turns out to be listed in the &lt;a href=&quot;https://developers.facebook.com/docs/sharing/webmasters/web-crawlers&quot;&gt;very same documentation&lt;/a&gt; as
the result of the command
&lt;code&gt;whois -h whois.radb.net -- &apos;-i origin AS32934&apos; | grep ^route&lt;/code&gt;. Naturally, I
do not trust the information granted &quot;graciously&quot; by Meta, so I will be
investigating my logfiles carefully to find if there is any more. &lt;a href=&quot;https://notashelf.dev/posts/stop-scraping-my-forge#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>rant</category><category>linux</category><category>web</category><category>software</category></item><item><title>Enter: Beginner&apos;s guide to Writing Error Messages</title><link>https://notashelf.dev/posts/well-done-verbosity</link><guid isPermaLink="true">https://notashelf.dev/posts/well-done-verbosity</guid><description>Thoughts and rants on key principles of writing polite, effective and user-friendly error messages</description><pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Programs fail, even the best-written ones. You know it; you&apos;ve experienced it.
You wish you could avoid it, but you can&apos;t. Computers are far from perfect. As a
beginner programmer, you quickly learn that you must account for both expected
and unexpected errors. Handling these errors isn&apos;t too difficult, and at this
point, you have two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Throw a stack trace that gives insight into &lt;em&gt;where&lt;/em&gt; the error occurred.&lt;/li&gt;
&lt;li&gt;Provide a human-readable error message explaining the issue (or its absence)
and offering debugging steps, if applicable.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;While you might feel inclined to favor only one of these approaches, the correct
solution is to provide &lt;em&gt;both&lt;/em&gt;---but under different circumstances. This is where
verbosity comes into play. Too much information can be overwhelming; too little
can be useless. So, what&apos;s the perfect middle ground?&lt;/p&gt;
&lt;h2&gt;Who asked?&lt;/h2&gt;
&lt;p&gt;When I visit your website and your backend is down (unbeknownst to me), I
&lt;em&gt;really&lt;/em&gt; don&apos;t care to see your unhandled error message in full detail. The end
user, who &lt;em&gt;likely&lt;/em&gt; has no idea what the error message means, should never
encounter it. It&apos;s &lt;em&gt;you&lt;/em&gt;---the one responsible for fixing the issue---who needs
that detailed information.&lt;/p&gt;
&lt;p&gt;The end user should instead see a simple, &lt;em&gt;polite&lt;/em&gt; message explaining that
something went wrong and reassuring them that steps are being taken to resolve
it. Offer them hope, not despair. Lie if you must, but don&apos;t scare them away.
Keep verbose error messages hidden—where they belong---in your backend.&lt;/p&gt;
&lt;p&gt;On the other hand, you &lt;em&gt;do&lt;/em&gt; need verbose stack traces or whatever form your
program&apos;s detailed crash information takes. You can&apos;t omit these, and you
shouldn&apos;t. Instead, tuck them behind a flag or log them in a way that&apos;s
accessible only to those who need them. Ensure that anyone actively
troubleshooting can easily find this detailed information, while those who don&apos;t
need it remain blissfully unaware.&lt;/p&gt;
&lt;h2&gt;The Good, The Bad, and Nix&lt;/h2&gt;
&lt;p&gt;This is a Nix blog, so of course, I&apos;m going to complain about it. Nix has a very
&lt;em&gt;unique&lt;/em&gt; (for the lack of a better word) way of reporting errors. If the author
of whatever project you are consuming is thoughtful enough to handle each case
(be it via an assertion, as part of the module system, or with an extensive
conditional), then you&apos;re likely to receive a &lt;em&gt;very&lt;/em&gt; informative explanation of
what went wrong and how to fix it. The module system is excellent at this.&lt;/p&gt;
&lt;p&gt;If the error is &lt;em&gt;not&lt;/em&gt; handled, however, you&apos;re left with an obscure message
pointing to where the error originates. &lt;em&gt;Hundreds&lt;/em&gt; of lines at the very least.
In the context of a NixOS system (i.e. the Nixpkgs module system), errors often
trace back to the entry point of the module system or something generic like
&lt;code&gt;config&lt;/code&gt; in &lt;code&gt;specialArgs&lt;/code&gt;. This, folks, is bad design. Not only are you
expecting the user to know exactly what they&apos;re looking at (minus points for the
infamous &lt;code&gt;--show-trace&lt;/code&gt;), but you&apos;re also presenting them with an intimidating
wall of text that they&apos;ll naturally avoid. It&apos;s almost as if you don&apos;t want the
error to be resolved...&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nix.dev/manual/nix/2.25/release-notes/rl-2.20&quot;&gt;Nix 2.20&lt;/a&gt; has made...
some attempts to improve this situation, but the language remains fundamentally
flawed, and such errors are, unfortunately, still unavoidable.&lt;/p&gt;
&lt;h3&gt;All your error are belong to us&lt;/h3&gt;
&lt;p&gt;Here is an example of how I would like to approach errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&quot;express&quot;);
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

const app = express();
const PORT = 3000;

function logErrorToFile(err, req) {
  const logFilePath = path.join(__dirname, &quot;error.log&quot;);
  const logMessage = `
[${new Date().toISOString()}]
Error: ${err.message}
Route: ${req.originalUrl}
Stack Trace: ${err.stack}\n`;

  fs.appendFile(logFilePath, logMessage, (error) =&gt; {
    if (error) {
      console.error(&quot;Failed to write to log file:&quot;, error.message);
    }
  });
}

function errorHandler(err, req, res, next) {
  logErrorToFile(err, req);

  res.status(500).json({
    message: &quot;Something went wrong. We are working to resolve the issue.&quot;,
  });
}

// A route that intentionally throws an error
app.get(&quot;/error&quot;, (req, res, next) =&gt; {
  try {
    throw new Error(&quot;This is a test error&quot;);
  } catch (err) {
    next(err); // we forward the error to the error-handling middleware
  }
});

app.use((req, res) =&gt; {
  res.status(404).json({
    message: &quot;The resource you are looking for does not exist.&quot;,
  });
});

// Centralized error-handling middleware
app.use(errorHandler);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on http://localhost:${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the example above, error-logging and user-response responsibilities are
clearly separated. The &lt;code&gt;errorHandler&lt;/code&gt; middleware returns a clean and polite JSON
response to the client: &lt;em&gt;&quot;Something went wrong. We are working to resolve the
issue.&quot;&lt;/em&gt; The user doesn&apos;t need to know anything technical, so they don&apos;t.
Letting technical information to the frontend is simply poor design.&lt;/p&gt;
&lt;p&gt;This, I think, is the golden spot. Easy to maintain, not awful and more
importantly &lt;em&gt;not&lt;/em&gt; a thousand lines of meaningless, unintelligible text.&lt;/p&gt;
&lt;h3&gt;404: According to all known laws of aviation...&lt;/h3&gt;
&lt;p&gt;Now lets consider a bad example. No, a &lt;em&gt;horrible&lt;/em&gt; example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&quot;express&quot;);
const app = express();
const PORT = 3000;

app.get(&quot;/error&quot;, (req, res) =&gt; {
  // Do you want my credit card details as well?
  res.status(500).send(`
        &amp;#x3C;h1&gt;Error&amp;#x3C;/h1&gt;
        &amp;#x3C;p&gt;Something  wrong!&amp;#x3C;/p&gt;
        &amp;#x3C;pre&gt;
            Error: Failed to fetch resource
            at /error:10:15
            at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
            at next (node_modules/express/lib/router/route.js:144:13)
            at Route.dispatch (node_modules/express/lib/router/route.js:114:3)
            at Layer.handle [as handle_request] (node_modules/express/lib/router/layer.js:95:5)
            at /error:10:15
        &amp;#x3C;/pre&gt;
        &amp;#x3C;pre&gt;
            Environment: ${JSON.stringify(process.env, null, 2)}
        &amp;#x3C;/pre&gt;
    `);
});

// No error handling middleware or logging mechanisms. Vomit everything as is.
app.listen(PORT, () =&gt; {
  console.log(`Server running at http://localhost:${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Don&apos;t ask me where I got that error message.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this godawful example, the end-user is bombarded with irrelevant technical
details like stack traces and environment variables. This is not only
overwhelming and confusing, but it also exposes sensitive information that
should never be seen by the user. The error message is not actionable and serves
only to frustrate the user. More importantly, it&apos;s a security risk—exposing
environment variables could allow a malicious party to take advantage of what
you might consider technical mumbo-jumbo (and therefore safe to spew.)&lt;/p&gt;
&lt;p&gt;Moreover, this approach is just plain ugly. A wall of text is neither helpful
nor appropriate for a user who just wants to know what went wrong and if
anything is being done to fix it. Imagine receiving a message like this on your
own, and ask yourself—would you ever come back to this website?&lt;/p&gt;
&lt;p&gt;If you take anything away from this, let it be that error handling should always
prioritize clarity, security and proper compartmentalization.&lt;/p&gt;
&lt;h3&gt;Final Thoughts: Verbosity as a Tool, Not a Burden&lt;/h3&gt;
&lt;p&gt;This has been translated from a far less technical rant I&apos;ve dropped earlier
today on some painful case of error handling&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/well-done-verbosity#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Looking back, it was not the
first time I&apos;ve faced bad error handling. Lots of software I use on a daily
basis simply print everything. In hindsight, this status quo is terrible and yet
we continue to endure.&lt;/p&gt;
&lt;p&gt;Error handling should not just about fixing problems. It must be moreso about
how you communicate them. I&apos;m sure the programmer who wrote that piece of code
wants to know what went wrong, and where, but I don&apos;t. Unnecessary verbosity is
just as harmful as a complete lack of information. So, for the sake of
establishing a standard let me provide the following as a baseline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Tailor to Your Audience: End-users want, nay, &lt;em&gt;need&lt;/em&gt; reassurance and
simplicity. Developers need depth and precision. Separate these layers
effectively. Verbosity should be &lt;em&gt;opt-in&lt;/em&gt;. Flags, environment variables,
configuration options and so on. Don&apos;t just vomit information.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Respect Security: Never expose sensitive or otherwise unnecessary details to
the frontend. Verbose logs belong in the backend, under restricted access.
Security through obscurity is not security.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keep It Manageable: Avoid overwhelming developers&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/well-done-verbosity#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; with too much data.
Structure logs well and keep error messages concise but informative.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This was all from me. If you have anything you want to add, do feel free to
contact me. Let me also know of any unique cases of good and bad error handling
that you might&apos;ve ran into. Cheers!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Now you know where I got the error message. &lt;a href=&quot;https://notashelf.dev/posts/well-done-verbosity#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;There are cases where I am both the developer and the end user. Assume,
for a moment, that I am working on &lt;em&gt;your&lt;/em&gt; codebase with minimal exposure.
Most of your codebase is completely foreign to me. When you throw me a stack
trace that is all over the place, I am immediately less inclined to
contribute to your software. &lt;strong&gt;&lt;a href=&quot;https://pterodactyl.io/&quot;&gt;Pterodactyl&lt;/a&gt;&lt;/strong&gt; (the game server management
panel) has a very special way of handling errors. It spews everything, but
&lt;a href=&quot;https://pterodactyl.io/panel/1.0/troubleshooting.html#reading-error-logs&quot;&gt;provides helpful steps&lt;/a&gt; for the user. This is a good alternative to
separating layers. &lt;a href=&quot;https://notashelf.dev/posts/well-done-verbosity#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><category>rant</category><category>software</category><category>thoughts</category></item><item><title>Hello World</title><link>https://notashelf.dev/posts/hello-world</link><guid isPermaLink="true">https://notashelf.dev/posts/hello-world</guid><description>New year, new blog and new obsessions; all in one place.</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After hours on end of reckless consideration and rewriting, I have finally
crafted my static site generator---a basic (but extremely inelegant) Bash
script---and my blog is now fully live. Not that I needed to tell you; after
all, you&apos;re already here.&lt;/p&gt;
&lt;p&gt;I will spare you the technical details, mostly because the code itself is
agonizing to look at, but this struck me as a more streamlined way to manage my
blog. Using Pandoc and jq for core functionality (plus sassc for styling), I
have effectively put together an extremely portable and simple static site
generator. Heavy emphasis on simple, however, as it lacks any customizability I
didn not need personally. Sure, it is slow---relying on Bash and a Haskell
program will do that---but it is also exactly how I wanted it. Not slow, mind
you. Just, very simple and straightforward.&lt;/p&gt;
&lt;p&gt;The script, unlike most of my projects, does not have one of my signature code
names I associate with &lt;em&gt;beloved&lt;/em&gt; projects. Still, it excels at being compact:
roughly 240 lines of code handle all functionality, fitting neatly into just a
few screenfuls of text. For what it&apos;s worth, it does its job and it does it well
enough above my expectations. Quite satisfied with how it turned out.&lt;/p&gt;
&lt;h2&gt;What&apos;s Next&lt;/h2&gt;
&lt;p&gt;Onto more exciting news: I plan to write more frequently now. All of my previous
writeups have been archived (though still accessible) until I have a chance to
review or rewrite them. The old blog was more of a renderer for my personal
notes, but this time around, I am aiming for more in-depth posts. After almost
two years, the groundwork is finally complete and it is about time I focused on
actual writing instead of over-engineering the technical side of things.&lt;/p&gt;
&lt;p&gt;If you feel inclined to reach out, let me know if there are any topics you would
like me to explore. My primary focus will be on Nix and Linux, but I might delve
into technical writeups about the languages I work with, as I work with them.&lt;/p&gt;
&lt;p&gt;Happy new year, and all the best!&lt;/p&gt;</content:encoded></item><item><title>Please Just Stop!</title><link>https://notashelf.dev/posts/just-stop</link><guid isPermaLink="true">https://notashelf.dev/posts/just-stop</guid><description>Picking the right tool for the job, Nix</description><pubDate>Sun, 02 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/casey/just&quot;&gt;Just&lt;/a&gt; is a fast and powerful command runner,
built with Rust. It is fast, simple and available
&lt;a href=&quot;https://github.com/casey/just?tab=readme-ov-file#packages&quot;&gt;almost everywhere&lt;/a&gt;
with top-notch editor and CI integration.&lt;/p&gt;
&lt;p&gt;So why do I have beef with Just?&lt;/p&gt;
&lt;h2&gt;Please, just stop&lt;/h2&gt;
&lt;p&gt;I actually have no beef with Just, I have briefly read the project page and
tested it somewhat extensively on my own machine. The tool itself is impressive,
however, it is also redundant when tools like Nix exist!&lt;/p&gt;
&lt;h3&gt;Just do better!&lt;/h3&gt;
&lt;p&gt;If you have previously read my blog, you probably know that I use the
&lt;a href=&quot;https://nixos.org/explore/&quot;&gt;Nix package manager&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/just-stop#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; on a daily basis, and
NixOS as my primary system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For those who have no idea what Nix is, it&apos;s a package manager and a build
tool that supports declarative, reproducible and reliable builds &amp;#x26; deployments
anywhere. I invite you to read more about it
&lt;a href=&quot;https://nix.dev/#what-can-you-do-with-nix&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Nix does not exactly compare with Just, as Just is a command runner but it
provides its full functionality in Nix devShells. You can use devShells not only
to declaratively install your development packages inside specific project
directories (which is built upon further by projects like
&lt;a href=&quot;https://direnv.net/&quot;&gt;direnv&lt;/a&gt;) but also write fully fledged scripts and helpers
in Bash, Python, Ruby or whatever interpreted or compiled language you want to
make them available in your projects.&lt;/p&gt;
&lt;p&gt;A common counter-argument to this is that they don&apos;t want to force people to
install yet another tool, however, Just &lt;em&gt;is&lt;/em&gt; that &quot;yet another tool&quot; you said
you don&apos;t want people to install. If I am installing something, I &lt;em&gt;might as well
opt in for the extensible one&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;In the wild&lt;/h3&gt;
&lt;p&gt;My intention is neither to gatekeep Just, nor to advertise Nix for new people.
What I want to get at is that if you have Nix installed, you &lt;strong&gt;do not need
Just&lt;/strong&gt;. Simple as that.&lt;/p&gt;
&lt;p&gt;While browsing GitHub, I have seen &lt;em&gt;countless&lt;/em&gt; Nix projects that recommend using
Just to run some simple commands (such as &lt;code&gt;nix build&lt;/code&gt; in large projects, or
&lt;code&gt;nixos-rebuild&lt;/code&gt; in system configurations). Please just stop.&lt;/p&gt;
&lt;p&gt;When given a powerful tool (which &lt;em&gt;is&lt;/em&gt; installed on your system) you should be
utilizing that tool to its fullest extent, &lt;em&gt;not&lt;/em&gt; introduce a completely
irrelevant tool that is inferior to what you have in every way.&lt;/p&gt;
&lt;p&gt;If you are using NixOS and want to provide bootstrapping commands to rebuild
your system, &lt;strong&gt;use devShells&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And this has been my rant. Please understand that my aim is not to discourage
you from using Just, but to encourage you to use the correct tool on your belt.&lt;/p&gt;
&lt;p&gt;If you use NixOS but have zero clue how to use devShells, then I implore you to
reconsider your choice of distro. NixOS is a powerful tool that needs, nay,
&lt;em&gt;demands&lt;/em&gt; your attention and understanding.&lt;/p&gt;
&lt;p&gt;If you do not intend to use the blessings provided by Nix to you, then you
probably do not need to suffer the many side effects of Nix (such as but not
limited to &lt;a href=&quot;https://github.com/gerg-l&quot;&gt;severe hairloss&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;What to do instead?&lt;/h3&gt;
&lt;p&gt;For my curious but equally lazy readers, here is how you may use a devShell.&lt;/p&gt;
&lt;p&gt;Start by creating a &lt;code&gt;shell.nix&lt;/code&gt; that contains your shell environment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{ pkgs ? import &amp;#x3C;nixpkgs&gt; {} }:
  pkgs.mkShell {
    # nativeBuildInputs is usually what you want -- tools you need to run
    nativeBuildInputs = with pkgs.buildPackages; [ ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This setup implies you are using channels, but similar instructions will apply
on flakes. Regardless, acquire your development packages from nixpkgs and put
them in your shell&apos;s &lt;code&gt;nativeBuildInputs&lt;/code&gt;. If you are trying to put, e.g., a
Python script available in &lt;a href=&quot;https://github.com/NixOS/nixpkgs&quot;&gt;nixpkgs&lt;/a&gt;, simply
place it in &lt;code&gt;nativeBuildInputs&lt;/code&gt;. shell, then you can write your script as
follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{ pkgs ? import &amp;#x3C;nixpkgs&gt; {} }:
  pkgs.mkShell {
    nativeBuildInputs = with pkgs.buildPackages; [
      (pkgs.writeShellScriptBin &quot;my-builder-script&quot; &apos;&apos;
        echo &quot;hello world&quot;
      &apos;&apos;)
    ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will place &lt;code&gt;my-builder-script&lt;/code&gt; in your &lt;code&gt;PATH&lt;/code&gt; after you enter the shell
with e.g. &lt;code&gt;nix develop&lt;/code&gt; or automagically if you use something like direnv.
Obviously the contents can be whatever you want, and you can write a script as
complex as you need. Aside from being a simple runner that did what Just does,
you will also have a fully reproducible script that fulfils your needs without
installing an additional tool thats sole purpose is to run commands.&lt;/p&gt;
&lt;p&gt;If you use Just to run your rebuild scripts, you can easily package your rebuild
scripts with Nix using &lt;code&gt;writeShellScript*&lt;/code&gt; from trivial builders collection and
completely ditch Just. Remember that someone observing your NixOS configuration
is less likely to use Just, and more likely to use Nix. Choose the appropriate
tool for the job, lest you over or underprepare.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Nix is actually not a package manager. It has been called that was to make
it more approachable for the casual users, but it is actually a build tool.
Or better yet, it is &lt;strong&gt;lambda calculus on files&lt;/strong&gt;. I feel the need to
clarify, because package management is &lt;em&gt;only one&lt;/em&gt; of Nix&apos;s features. Whereas
running commands is Just&apos;s only function. &lt;a href=&quot;https://notashelf.dev/posts/just-stop#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>On SysRq</title><link>https://notashelf.dev/posts/on-sysrq</link><guid isPermaLink="true">https://notashelf.dev/posts/on-sysrq</guid><description>Understanding, using or disabling SysRq as per your needs</description><pubDate>Sat, 11 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/keyboard_shortcuts&quot;&gt;https://wiki.archlinux.org/title/keyboard_shortcuts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Magic_SysRq_key&quot;&gt;https://en.wikipedia.org/wiki/Magic_SysRq_key&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/sysrq.html&quot;&gt;https://docs.kernel.org/admin-guide/sysrq.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysrq.rst&quot;&gt;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysrq.rst&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is SysRq?&lt;/h2&gt;
&lt;p&gt;SysRq is a &quot;&lt;em&gt;magical&lt;/em&gt;&quot;&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; key combination, or possibly a literal key on your
keyboard that is understood by the Linux kernel, which allows you to perform
various low-level commands regardless of the system&apos;s current state. Some
Linux-first computer manufacturers may include a literal SysRq key, or chances
are your PrtSc key (located somewhat close to your Numpad) doubles as it.&lt;/p&gt;
&lt;h2&gt;What can I do with it?&lt;/h2&gt;
&lt;p&gt;SysRq is most often used to recover from and debug an unresponsive system,
especially if you are trying to avoid doing a hard shutdown - which could
potentially cause data corruption - and is recommended over a hard shutdown.
Using the SysRq key, you can communicate with the Linux kernel directly and
reboot your &lt;em&gt;correctly&lt;/em&gt;, without potentially nuking active disk writes.&lt;/p&gt;
&lt;h2&gt;How do I use SysRq?&lt;/h2&gt;
&lt;p&gt;You should first check if your system is configured to support SysRq. Some
distros and users (such as myself) decide to disable SysRq because it is a
potential security flaw.&lt;/p&gt;
&lt;p&gt;To check whether your system is allowed to use SysRq, simply run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /proc/sys/kernel/sysrq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and you will receive a value that describes the functionality of your SysRq key,
or whether or not it is allowed.&lt;/p&gt;
&lt;h3&gt;Possible Values&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.kernel.org/admin-guide/sysrq.html&quot;&gt;kernel documentation&lt;/a&gt; gives
you a neat list of values and what they allow.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Bitmask&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0x0&lt;/td&gt;
&lt;td&gt;disable sysrq completely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0x1&lt;/td&gt;
&lt;td&gt;enable all functions of sysrq&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0x2&lt;/td&gt;
&lt;td&gt;enable control of console logging level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0x4&lt;/td&gt;
&lt;td&gt;enable control of keyboard (SAK, unraw)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0x8&lt;/td&gt;
&lt;td&gt;enable debugging dumps of processes etc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0x10&lt;/td&gt;
&lt;td&gt;enable sync command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;0x20&lt;/td&gt;
&lt;td&gt;enable remount read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0x40&lt;/td&gt;
&lt;td&gt;enable signalling of processes (term, kill, oom-kill)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;0x80&lt;/td&gt;
&lt;td&gt;allow reboot/poweroff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0x100&lt;/td&gt;
&lt;td&gt;allow nicing of all RT tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Setting SysRq Bitmask&lt;/h3&gt;
&lt;p&gt;You can set SysRq to &lt;code&gt;0&lt;/code&gt; in order to disable it, &lt;code&gt;1&lt;/code&gt; to allow ALL functionality
and to a value &lt;code&gt;&gt;= 1&lt;/code&gt; to set a bitmask&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; of allowed SysRq functions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;number&quot; &gt;/proc/sys/kernel/sysrq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To combine different functionalities in the SysRq permission bitmap, you will
need to perform bitwise OR operations on the values corresponding to the
functionalities you want to enable. Each functionality corresponds to a specific
bit in the bitmap.&lt;/p&gt;
&lt;p&gt;E.g., let&apos;s say you want to enable the functionalities for controlling console
logging level &lt;code&gt;(0x2)&lt;/code&gt;, enabling sync command &lt;code&gt;(0x10)&lt;/code&gt;, and allowing
reboot/poweroff &lt;code&gt;(0x80)&lt;/code&gt;. To combine these functionalities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x2 (console logging level) | 0x10 (sync command) | 0x80 (reboot/poweroff)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which would perform the following bitwise OR operation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x2 | 0x10 | 0x80 = 0x92
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;strong&gt;hexadecimal&lt;/strong&gt; value would then be converted to &lt;strong&gt;decimal&lt;/strong&gt; (which the
kernel commandline expects) as &lt;code&gt;146.&lt;/code&gt; Therefore you would run the following
command to set your desired bitmask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo 146 &gt; /proc/sys/kernel/sysrq
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Persisting SysRq&lt;/h3&gt;
&lt;p&gt;You can enable &lt;code&gt;SysRq&lt;/code&gt; with one command, as described above. Do keep in mind
that running e.g. &lt;code&gt;echo 1 &gt; /proc/sys/kernel/sysrq&lt;/code&gt; will enable SysRq only until
reboot and its state will not persist across reboots.&lt;/p&gt;
&lt;p&gt;On traditional distros, you can make it persistent by adding
&lt;code&gt;&quot;kernel.sysrq = 1&quot;&lt;/code&gt; at the end of &lt;code&gt;/etc/sysctl.d/99-sysctl.conf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On NixOS, you will need to set &lt;code&gt;boot.kernel.sysctl.&quot;kernel.sysrq&quot; = 1;&lt;/code&gt; in your
&lt;code&gt;configuration.nix&lt;/code&gt;. In some cases, you might need to &lt;em&gt;force&lt;/em&gt; this value in case
it is being overridden:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;boot.kernel.sysctl.&quot;kernel.sysrq&quot; = lib.mkForce 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Useful applications of SysRq&lt;/h2&gt;
&lt;p&gt;There are various applications of SysRq based on the bitmask range it was
allowed. Below are the two most common ones that I came across.&lt;/p&gt;
&lt;h3&gt;Rebooting with SysRq&lt;/h3&gt;
&lt;p&gt;Earlier I have mentioned that SysRq is often used to perform a proper reboot and
to recover a frozen system. With a permissive enough&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; bitmask, you can use
the following idiom: &quot;Reboot Even If System Utterly Broken&quot; (also referred to as
&quot;REISUB&quot;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;R: Turns off keyboard raw mode, and sets it to XLATE.&lt;/li&gt;
&lt;li&gt;E: Send a SIGTERM to all processes, except for init.&lt;/li&gt;
&lt;li&gt;I: Send a SIGKILL to all processes, except for init.&lt;/li&gt;
&lt;li&gt;S: Will attempt to sync all mounted filesystems.&lt;/li&gt;
&lt;li&gt;U: Will attempt to re-mount all mounted filesystems as read-only.&lt;/li&gt;
&lt;li&gt;B: Reboot!&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Please be aware that &quot;REISUB&quot; itself is just a mnemonic, not any kind of
general recommendation for the key press sequence to take back control of an
unresponsive system. You should not blindly press these sequences each time
without knowing their actual function as noted below&lt;sup&gt;&lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To do this on your system, you what you need to do is to hold down the &lt;code&gt;ALT&lt;/code&gt; key
and the &lt;code&gt;SysRq&lt;/code&gt; keys at the same time, hit the next queued letter while holding
the key combination, and then release. Repeat this for each letter (command) in
the idiom. To summarize, running the &lt;code&gt;REISUB&lt;/code&gt; sequence would require you to hold
down &lt;code&gt;ALT + SysRq&lt;/code&gt; 6 &lt;strong&gt;separate&lt;/strong&gt; times&lt;/p&gt;
&lt;h3&gt;Invoking OOM Killer&lt;/h3&gt;
&lt;p&gt;SysRq can also be used to invoke the OOM (out-of-memory) killer without causing
a kernel panic if there is nothing to kill. If your system is frozen due to
intense memory pressure, this could be an useful way to quickly get yourself out
of a status that might corrupt your data. Simply run &lt;code&gt;ALT + SysRq + f&lt;/code&gt; and hope
that OOM killer picks something recoverable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep in mind that the OOM killer, despite its well-meaning heuristics, can be
unpredictable and lead to irreversible damage. Do not use this key combination
casually.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Is SysRq worth it?&lt;/h2&gt;
&lt;p&gt;Those who have gone through my system configuration might have noticed that I
force SysRq to be disabled with &lt;code&gt;kernel.sysrq&lt;/code&gt; set to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is because I never had the need to use SysRq. After years on Linux, I never
had to recover from a system freezing over a long duration. It either lives, or
it dies. Recently I have configured OOM killer daemon to automatically kill
useless applications such as Electron, that tend to drain my memory - especially
on low-end systems because despite what devs might say, &lt;strong&gt;electron still
sucks&lt;/strong&gt;. As such, I do not think that SysRq is worth the potential security flaw
right, but that question is for you to answer yourself.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Quite literally what it is called in the
&lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/sysrq.rst&quot;&gt;kernel documentation&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;The permission bitmap is a 32-bit bitmap that determines which SysRq
commands are allowed to be executed. Each bit in the bitmap corresponds to a
specific SysRq command. When a SysRq command is issued, the kernel checks
the corresponding bit in the permission bitmap to determine whether the
command is allowed or not. &lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;The whole set of &lt;code&gt;REISUB&lt;/code&gt; functions can be enabled by setting SysRq value
to 244, although this will also enable additional functions which some may
find undesirable. If in doubt, do the math and choose your bitmask range
yourself. &lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/keyboard_shortcuts#Rebooting&quot;&gt;Archwiki on SysRq&lt;/a&gt; &lt;a href=&quot;https://notashelf.dev/posts/on-sysrq#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Headscale on NixOS, Tailscale anywhere</title><link>https://notashelf.dev/posts/using-headscale</link><guid isPermaLink="true">https://notashelf.dev/posts/using-headscale</guid><description>Brief introduction to running your own Tailscale coordination server, Headscale</description><pubDate>Sat, 11 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today&apos;s main attraction is the Headscale setup on my VPS running NixOS, which
I&apos;ve finally came around to self-host.&lt;/p&gt;
&lt;p&gt;There has been much talk about this new product called Tailscale recently around
the web, especially in the last few years. Tailscale is a VPN service that makes
the devices and applications we own accessible anywhere using the open source
WireGuard protocol to establish encrypted point-to-point connections. I have
been using Tailscale for a while now, but in an effort to move all of my
services to self-owned hardware some of my services have been moved over to my
NixOS server over time.&lt;/p&gt;
&lt;p&gt;Many of Tailscale&apos;s components are open-source, especially its clients, but the
server remains closed-source. Tailscale is a SaaS product and monetization
naturally is a big concern, however, we care more about controlling our own data
than their attempts of monetization.&lt;/p&gt;
&lt;p&gt;This is where the (very appropriately named) Headscale comes in; Headscale is an
open-source, self-hosted implementation of the Tailscale control server. The
configuration is extremely straightforward, as Headscale will handle everything
for us.&lt;/p&gt;
&lt;h2&gt;Running Headscale&lt;/h2&gt;
&lt;p&gt;Below is a simple configuration for the Headscale module of NixOS.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;services = let
  domain = &quot;example.com&quot;;
in {
  headscale = {
    enable = true;
    address = &quot;0.0.0.0&quot;;
    port = 8085;

    settings = {
      server_url = &quot;https://tailscale.${domain}&quot;;

      dns_config = {
        override_local_dns = true;
        base_domain = &quot;${domain}&quot;;
        magic_dns = true;
        domains = [&quot;tailscale.${domain}&quot;];
        nameservers = [
          &quot;9.9.9.9&quot; # no cloudflare, nice
        ];
      };

      ip_prefixes = [
        &quot;100.64.0.0/10&quot;
        &quot;fd7a:115c:a1e0::/48&quot;
      ];
    };
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using Headscale&lt;/h2&gt;
&lt;p&gt;We must first create a user, which we can do with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;headscale users create myUser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then on the machine that will be our client, we need to login.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;tailscale up --login-server tailscale.example.com # replace this URL with your own as configured abovea
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Followed by registering the machine.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;# machine key will be obtained visiting the URL that is returned from the above command
headscale --user myUser nodes register --key &amp;#x3C;MACHINE_KEY&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally logging into your Tailnet using the URL and your machine key.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;tailscale up --login-server https://tailscale.example.com --authkey &amp;#x3C;YOUR_AUTH_KEY&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And all done! Now try connecting to one of your machines using the hostname now
to test if the connection is actually working. If anything goes wrong, make sure
to check your DNS settings: remember, it&apos;s always the DNS.&lt;/p&gt;</content:encoded></item><item><title>Nix Remote Builders on non-standard OpenSSH ports</title><link>https://notashelf.dev/posts/openssh-custom-port</link><guid isPermaLink="true">https://notashelf.dev/posts/openssh-custom-port</guid><description>Setting up Nix remote builders on unique setups.</description><pubDate>Fri, 14 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My VPS, which hosts some of my infrastructure, has been running NixOS for a
while now. Although weak, I use it for distributed builds alongside the rest of
my NixOS machines on a Tailscale network.&lt;/p&gt;
&lt;p&gt;This server, due to it hosting my infrastructure that communicates with the rest
of the internet (i.e my mailserver), is somewhat responsive to queries from the
public - which includes &lt;em&gt;very&lt;/em&gt; aggressive port scans (thanks, skiddies!)&lt;/p&gt;
&lt;p&gt;To mitigate that, I have decided to change the ssh port from the default &lt;strong&gt;22&lt;/strong&gt;
... this is not exactly a panacea, it helps alleviate ... the insane log spam I
get from failed ssh requests.&lt;/p&gt;
&lt;h2&gt;The OpenSSH Configuration&lt;/h2&gt;
&lt;p&gt;First thing we&apos;ve done is to configure SSH daemon to listen on the new port on
your server configuration&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;services.openssh = {
  ports = [2222];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this set, openssh on the server will now be listening on the port &lt;strong&gt;2222&lt;/strong&gt;
instead of the default &lt;strong&gt;22&lt;/strong&gt;. For the changes to take effect after a rebuild,
you might need to run &lt;code&gt;systemctl restart sshd.socket&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then we want to configure our client to use the correct port for our server
instead of the default &lt;strong&gt;22&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;programs.ssh.extraConfig = &apos;&apos;
    Host nix-builder
      HostName nix-builder-hostname # if you are using Tailscale, this can just be the hostname of a device on your Tailscale network
    Port 2222
&apos;&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And done, that is all for the ssh side of things. Next up, we need to configure
Next up, we need to configure our builder to use the correct host.&lt;/p&gt;
&lt;h2&gt;Nix Builder Configuration&lt;/h2&gt;
&lt;p&gt;Assuming you already have a remote builder configured, you will only need to
patch the &lt;code&gt;hostName&lt;/code&gt; with the one on your &lt;code&gt;openssh.extraConfig&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;{
  nix.buildMachines = [
    {
      hostName = &quot;nix-builder-hostname&quot;;
      sshUser = &quot;nix-builder&quot;;
      sshKey = &quot;/path/to/key&quot;;
      systems = [&quot;x86_64-linux&quot;];
      maxJobs = 2;
      speedFactor = 2;
      supportedFeatures = [&quot;kvm&quot;];
    }
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have added the correct &lt;code&gt;hostName&lt;/code&gt; and &lt;code&gt;sshUser&lt;/code&gt;, the builder will be
picked up automatically on the next rebuild.&lt;/p&gt;
&lt;h3&gt;Home-Manager&lt;/h3&gt;
&lt;p&gt;If you are using Home-Manager, you might also want to configure your declarative
~/.config/ssh/config to use the new port. That can be achieved through
&lt;code&gt;programs.ssh.matchBlocks&lt;/code&gt; option under Home-Manager&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;programs.ssh.matchBlocks = {
  &quot;builder&quot; = {
    hostname = &quot;nix-builder-hostname&quot;;
    user = &quot;nix-builder&quot;;
    identityFile = &quot;~/.ssh/builder-key&quot;;
    port = 2222;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that will be all. You are ready to use your new non-default port, mostly
safe from port scanners.&lt;/p&gt;</content:encoded></item><item><title>Packaging NextJS Webapps with Nix</title><link>https://notashelf.dev/posts/packaging-nextjs-apps</link><guid isPermaLink="true">https://notashelf.dev/posts/packaging-nextjs-apps</guid><description>Quick tutorial/journal on packaging NextJS projects with Nix</description><pubDate>Sun, 21 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I have had to go through the misfortune of hosting some websites
written with &lt;em&gt;NextJS&lt;/em&gt; on my VPS running NixOS, this note entry shall document my
experience and the &quot;easy&quot; path I have chosen.&lt;/p&gt;
&lt;h2&gt;Packaging&lt;/h2&gt;
&lt;p&gt;The websites I hosted were of two variety: those statically exported, and those
that cannot be statically exported.&lt;/p&gt;
&lt;h3&gt;Statically Exported Webapps&lt;/h3&gt;
&lt;p&gt;Statically exported ones are easy to package, because it is a matter of running
&lt;code&gt;npm build&lt;/code&gt; (or whatever your build script is) with the following NextJS
settings&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// next.config.js
module.exports = {
  distDir: &quot;dist&quot;, // an arbitrary path for your export
  output: &quot;export&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will export a static website with a bunch of html files that you can then
serve with nodePackages.serve or a webserver like nginx or apache. And that is
the end of your worries for a statically exported website! No headache, just
write a simple derivation, such as the one below&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# default.nix
{
  buildNpmPackage,
  pkg-config,
  python3,
  ...
}:
buildNpmPackage {
  pname = &quot;your-website&quot;;
  version = &quot;0.1&quot;;

  src = ./.;
  # needs to be updated everytime you update npm dependencies
  npmDepsHash = &quot;sha256-some-hash&quot;;
  # some npm packages may need to be built from source, because nodejs is a *terrible* ecosystem
  nativeBuildInputs = [pkg-config python3];

 # move exported website to $out
 postInstall = &apos;&apos;
    cp -rf dist/* $out
  &apos;&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and serve its path with a simple tool after building the derivation, I find
nginx to be awfully convenient for doing so, but you may choose caddy if you
prefer.&lt;/p&gt;
&lt;h3&gt;Webapps that cannot be statically exported&lt;/h3&gt;
&lt;p&gt;If your website depends on API routes for some reasons, then Next will not allow
you to do static export. Which means you need to run &lt;code&gt;next start&lt;/code&gt; in some shape
or form. While a systemd service is certainly a way of doing it (one that I do
not recommend), a oci container works as well if not better.&lt;/p&gt;
&lt;p&gt;You can write a &quot;simple&quot; docker image for your oci container to use, such as the
one below&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;# dockerImage.nix
{
  pkgs,
  inputs,
  ...
}: {
  dockerImage = pkgs.dockerTools.buildImage {
    config = {
      WorkingDir = &quot;/your-website&quot;;
      Cmd = [&quot;npm&quot; &quot;run&quot; &quot;serve&quot;];
    };

    name = &quot;your-website&quot;;
    tag = &quot;latest&quot;;

    fromImage = pkgs.dockerTools.buildImage {
      name = &quot;node&quot;;
      tag = &quot;18-alpine&quot;;
    };

    copyToRoot = pkgs.buildEnv {
      name = &quot;image-root&quot;;

      paths = with pkgs; [
        # this package is called from a flake.nix alongside the derivation for the website
        inputs.self.packages.${pkgs.stdenv.system}.your-website
        nodejs
        bash
      ];

      pathsToLink = [
        &quot;/bin&quot;
        &quot;/your-website&quot;
      ];
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, configure oci-containers module option to pick up the Docker image that
you have built. This is a simplified version of my VPS&apos; container setup. An
example can be found in my
&lt;a href=&quot;https://github.com/NotAShelf/nyx/blob/a9e129663ac91302f2fd935351a71cbbd2832f64/modules/core/roles/server/system/services/mkm.nix&quot;&gt;server module&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;virtualisation.oci-containers = {
  backend = &quot;podman&quot;;
  containers = {
    &quot;website-container&quot; = {
      autoStart = true;
      ports = [
        &quot;3000:3000&quot; # bind container&apos;s port 3000 to the outside port 3000 for NextJS
      ];

      extraOptions = [&quot;--network=host&quot;];

      image = &quot;your-website&quot;;
      imageFile = inputs.website-flake.packages.${pkgs.stdenv.system}.dockerImage;
    };
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After a rebuild, your system will provision the container and start it on port
&lt;strong&gt;3000&lt;/strong&gt;. You can access it with &lt;code&gt;your-server-ip:3000&lt;/code&gt; in your browser, and even
configure nginx to set up a reverse proxy to assign your domain.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nix&quot;&gt;&quot;example.com&quot; = {
  locations.&quot;/&quot;.proxyPass = &quot;http://127.0.0.1:3000&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will assign your domain to your webserver, and allow outside visitors to
view your &quot;awesome&quot; NextJS webapp.&lt;/p&gt;</content:encoded></item><item><title>System Backlight</title><link>https://notashelf.dev/posts/system-backlight</link><guid isPermaLink="true">https://notashelf.dev/posts/system-backlight</guid><description>A quick encounter with System Backlight on a random Linux Kernel update.</description><pubDate>Sun, 22 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Following a large system upgrade two days ago, my HP Pavilion laptop has stopped
registering the &lt;code&gt;intel_backlight&lt;/code&gt; device interface in &lt;code&gt;/sys/class/backlight&lt;/code&gt; -
which is most often used by tools such as &lt;code&gt;brightnessctl&lt;/code&gt; or &lt;code&gt;light&lt;/code&gt; to control
active backlight level. Instinctively looking at the result of &lt;code&gt;dmesg&lt;/code&gt;, I came
across an insanely vague error message that the kernel was unable to load some
modules. Thanks, I guess?&lt;/p&gt;
&lt;h2&gt;The solution&lt;/h2&gt;
&lt;p&gt;After some &lt;em&gt;very extensive&lt;/em&gt; research, obviously on Google as every other
confused Linux user would do, I came across this &lt;a href=&quot;https://www.linuxquestions.org/questions/slackware-14/brightness-keys-not-working-after-updating-to-kernel-version-6-a-4175720728/&quot;&gt;forum post&lt;/a&gt; that mentioned a
change in backlight behaviour sometime after kernel version &lt;code&gt;6.1.4&lt;/code&gt;. Fortunately
for me, the article was also referring to the ever so informative Archwiki that
instructed passing one of the three &lt;a href=&quot;https://wiki.archlinux.org/title/backlight#Kernel_command-line_options&quot;&gt;kernel command-line options&lt;/a&gt; depending on
our needs.&lt;/p&gt;
&lt;p&gt;What I did not know at the time was that when I upgraded my kernel from &lt;code&gt;6.1.3&lt;/code&gt;
to &lt;code&gt;6.1.6&lt;/code&gt; with a &lt;code&gt;nix flake update&lt;/code&gt;, the &lt;code&gt;acpi_backlight=none&lt;/code&gt; parameter had
made it so that it would skip loading Intel backlight &lt;em&gt;entirely&lt;/em&gt;. Simply
switching this parameter to &lt;code&gt;acpi_backlight=native&lt;/code&gt; as per the article above has
fixed the issue.&lt;/p&gt;
&lt;h2&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;Linux devs are nerds.&lt;/p&gt;</content:encoded></item></channel></rss>