{
  "version": "https://jsonfeed.org/version/1",
  "title": "Ian's Digital Garden",
  "home_page_url": "https://ianwwagner.com/",
  "feed_url": "https://ianwwagner.com//archive-2025.json",
  "description": "",
  "items": [
    {
      "id": "https://ianwwagner.com//using-tar-with-your-favorite-compression.html",
      "url": "https://ianwwagner.com//using-tar-with-your-favorite-compression.html",
      "title": "Using tar with Your Favorite Compression",
      "content_html": "<p>Here's a fun one!\nYou may already know that tarball is a pure archive format,\nand that any compression is applied to the whole archive as a unit.\nThat is to say that compression is not actually applied at the <em>file</em> level,\nbut to the entire archive.</p>\n<p>This is a trade-off the designers made to limit complexity,\nand as a side-effect, is the reason why you can't randomly access parts of a compressed tarball.</p>\n<p>What you may not know is that the <code>tar</code> utility has built-in support for a few formats!\nGZIP is probably the most commonly used for historical reasons,\nbut <code>zstd</code> and <code>lz4</code> are built-in options on my Mac.\nThis is probably system-dependent, so check your local manpages.</p>\n<p>Here's an example of compressing and decompressing with <code>zstd</code>:</p>\n<pre><code class=\"language-shell\">tar --zstd -cf directory.tar.zst directory/\ntar --zstd -xf directory.tar.zst\n</code></pre>\n<p>You can also use this with <em>any</em> (de)compression program that operates on stdin and stdout!</p>\n<pre><code class=\"language-shell\">tar --use-compress-program zstd -cf directory.tar.zst directory/\n</code></pre>\n<p>Pretty cool, huh?\nIt's no different that using pipes at the end of the day,\nbut it does simplify the invocation a bit in my opinion.</p>\n<p>After I initially published this article,\n<code>@cartocalypse@norden.social</code> noted that some versions of tar include the\n<code>-a</code>/<code>--auto-compress</code> option which will automatically determine format and compression based on the suffix!\nCheck your manpages for details; it appears to work on FreeBSD, macOS (which inherits the FreeBSD implementation), and GNU tar.</p>\n",
      "summary": "",
      "date_published": "2025-12-14T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "shell",
        "compression",
        "tar"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//delightfully-simple-pipelines-with-nushell.html",
      "url": "https://ianwwagner.com//delightfully-simple-pipelines-with-nushell.html",
      "title": "Delightfully Simple Pipelines with Nushell",
      "content_html": "<p>I've been using <a href=\"https://www.nushell.sh/\">nushell</a> as my daily driver for about six months now,\nand wanted to show a few simple examples of why I'm enjoying it so much.\nI think it's a breath of fresh air compared to most shells.</p>\n<h1><a href=\"#why-a-new-shell\" aria-hidden=\"true\" class=\"anchor\" id=\"why-a-new-shell\"></a>Why a new Shell?</h1>\n<p>In case you've never heard of it before, nushell is a, well, new shell ;)\n<code>bash</code> has been the dominant shell for as long as I can remember,\nthough <code>zsh</code> have their fair share of devotees.\n<code>fish</code> is the only recent example I can think of as a &quot;challenger&quot; shell.\n<code>fish</code> gained enough traction that it's supported by tooling such as Python <code>virtualenv</code>\n(which only has integrations out of the box for a handful of shells).\nI think <code>fish</code> is popular because it had some slightly saner defaults out of the box,\nwas easier to &quot;customize&quot; with flashy prompts (which can make your shell SUPER slow to init),\nand had a saner scripting language than <code>bash</code>.\nBut it still retained a lot of the historical baggage from POSIX shells.</p>\n<p>Nushell challenges two common assumptions about shells\nand asks &quot;what if things were different?&quot;</p>\n<ol>\n<li>POSIX compliance is a non-goal.</li>\n<li>Many standard tools from GNU coreutils/base system (e.g. <code>ls</code> and <code>du</code>) are replaced by builtins.</li>\n<li>All nushell &quot;native&quot; utilities produce and consume <strong>structured data</strong> rather than text by default.</li>\n</ol>\n<p>By dropping the goal of POSIX compliance,\nnushell frees itself from decades of baggage.\nThis means you get a scripting language that feels a lot more like Rust.\nYou'll actually get errors by default when you try to do something stupid,\nunlike most shells which will happily proceed,\nusually doing something even more stupid.\nMaybe treating undefined variables as empty string make sense in the 1970s,\nbut that's almost never helpful.</p>\n<p>nushell also takes a relatively unique approach to utilities.\nWhen you type something like <code>ls</code> or <code>ps</code> in nushell,\nthis is handled by a shell builtin!\nIt's just Rust code baked into the shell rather than calling out to GNU coreutils\nor whatever your base system includes.\nThis means that whether you type <code>ps</code> on FreeBSD, Debian, or macOS,\nyou'll get the same behavior!</p>\n<p>I can already hear some readers thinking &quot;doesn't this just massively bloat the shell?&quot;\nNo, not really.\nThe code for these is for less than that of the typical GNU utility,\nbecause nushell actually (IMO) embraces UNIX philosophy even better than the original utilities.\nThey are all extremely minimal and work with other builtins.\nFor example, there are no sorting flags for <code>ls</code>,\nand no format/unit flags for <code>du</code>.</p>\n<p>The reason that nushell <em>can</em> take this approach is because they challenge the notion that\n&quot;text is the universal API.&quot;\nYou <em>can't</em> meaningfully manipulate text without lots of lossy heuristics.\nBut you <em>can</em> do this for structured data!\nI admit I'm a bit of a chaos monkey, so I love to see a project taking a rare new approach\nin space where nothing has fundamentally changed since the 1970s.</p>\n<p>Okay, enough about philosophy... here are a few examples of some shell pipelines I found delightful.</p>\n<h1><a href=\"#elastic-snapshot-status\" aria-hidden=\"true\" class=\"anchor\" id=\"elastic-snapshot-status\"></a>Elastic snapshot status</h1>\n<p>First up: I do a lot of work with Elasticsearch during <code>$DAYJOB</code>.\nOne workflow I have to do fairly often is spin up a cluster and restore from a snapshot.\nThe Elasticsearch API is great... for programs.\nBut I have a hard time grokking hundreds of lines of JSON.\nHere's an example of a pipeline I built which culls the JSON response down to just the section I care about.</p>\n<pre><code class=\"language-nu\">http get &quot;http://localhost:9200/myindex/_recovery&quot;\n  | get restored-indexname\n  | get shards\n  | get index\n  | get size\n</code></pre>\n<p>In case it's not obvious, the <code>http</code> command makes HTTP requests.\nThis is another nushell builtin that is an excellent alternative to <code>curl</code>.\nIt's not as feature-rich (<code>curl</code> has a few decades of head start),\nbut it brings something new to the table: it understands from the response that the content is JSON,\nand converts it into structured data!\nEverything in this pipeline is nushell builtins.\nAnd it'll work on <em>any</em> OS that nushell supports.\nEven Windows!\nThat's wild!</p>\n<p>Pro tip: you can press option+enter to add a new line when typing in the shell.</p>\n<h1><a href=\"#disk-usage-in-bytes\" aria-hidden=\"true\" class=\"anchor\" id=\"disk-usage-in-bytes\"></a>Disk usage in bytes</h1>\n<p>Here's an example I hinted at earlier.\nIf you type <code>help du</code> (to get built-in docs),\nyou won't find any flags for changing the units.\nBut you can do it using formatters like so:</p>\n<pre><code class=\"language-nu\">du path/to/bigfile.bin | format filesize B apparent\n</code></pre>\n<p>The <code>du</code> command <em>always</em> shows human-readable units by default. Which I very much appreciate!\nAnd did you notice <code>apparent</code> at the end there?\nWell, the version of <code>du</code> you'd find with a typical Linux distro doesn't <em>exactly</em> lie to you,\nbut it withholds some very important information.\nThe physical size occupied on disk is not necessarily the same as how large the file\n(in an abstract platonic sense) <em>actually</em> is.</p>\n<p>There are a bunch of reasons for this, but the most impactful one is compressed filesystems.\nIf I ask Linux <code>du</code> how large a file is in an OpenZFS dataset,\nit will report the physical size by default, which may be a few hundred megabytes\nwhen the file is really multiple gigabytes.\nNot <em>necessarily</em> helpful.</p>\n<p>Anyways, the nushell builtin always gives you columns for both physical and apparent.\nSo you can't ignore the fact that these sizes are often different.\nI like that!</p>\n<h1><a href=\"#some-other-helpful-bits-for-switching\" aria-hidden=\"true\" class=\"anchor\" id=\"some-other-helpful-bits-for-switching\"></a>Some other helpful bits for switching</h1>\n<p>If you want to give nushell a try,\nthey have some great documentation.\nRead the basics, but also check out their specific pages on, e.g.\n<a href=\"https://www.nushell.sh/book/coming_from_bash.html\">coming from bash</a>.</p>\n<p>Finally, here are two more that tripped me up at first.</p>\n<ul>\n<li>If you want to get the PID of your process, use <code>$nu.pid</code> instead of <code>$$</code>.</li>\n<li>To access environment <em>variables</em>, you need to be explicit and go through <code>$env</code>. On the plus side, you can now explicitly differentiate from environment variables.</li>\n</ul>\n",
      "summary": "",
      "date_published": "2025-12-13T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "shell"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//faster-ssh-file-transfers-with-rsync.html",
      "url": "https://ianwwagner.com//faster-ssh-file-transfers-with-rsync.html",
      "title": "Faster SSH File Transfers with rsync",
      "content_html": "<p>If you're a developer or sysadmin, there's a pretty good chance you've had to transfer files back and forth.\nBack in the old days, you may have used the <code>ftp</code> utility or something similar\n(I think my first one was probably CuteFTP).\nThen you probably thought better of doing things in plaintext,\nand switched to SFTP/SCP, which operate over an SSH connection.</p>\n<p>My quick post today is not exactly a bombshell of new information,\nbut these tools are not the fastest way to transfer.\nThere's often not a huge difference if you're transferring between machines in the same datacenter,\nbut you can do many times better then transferring from home or the office.</p>\n<p>Of course I'm talking about <code>rsync</code>, which has a lot of features like compression, partial transfer resume,\nchecksumming so just the deltas are sent in a large file, and more.\nI don't know why, but it's rarely in my consciousness, and always strikes me as a bit quirky.\nYou need quite a few flags for what I would consider to be reasonable defaults.\nBut if you remember those (or use shel history, like me),\nit can save you a ton of time.</p>\n<p>In fact, this morning, using rsync saved me more time than it took to write this blog post.\nI was transferring a ~40GB file from a server halfway around the world,\nbut I only had to transfer bytes equivalent to 20% of the total.</p>\n<p>Here's a look at the <code>rsync</code> command I often use for pulling files from a remote server\n(I usually do this to download a large build artifact without going through a cloud storage intermediary,\nwhich is both slower and more eexpensive):</p>\n<pre><code class=\"language-shell\">rsync -Pzhv example.com:/remote/path/to/big/file ~/file\n</code></pre>\n<p>It's not really that bad compared to an <code>scp</code> invocation, but those flags make all the difference.\nHere's what they do:</p>\n<ul>\n<li><code>-P</code> - Keeps partially transferred files (in case of interruption) and shows progress during the transfer.</li>\n<li><code>-z</code> - Compresses data on the source server before sending to the destination. This probably isn't a great idea for an intra-datacenter transfer (it just wastes CPU), but it's perfect for long distance transfers over &quot;slower&quot; links (where I'd say slower is something less than like 100Mbps between you and the server... the last part is important because you may have a gigabit link, but the peering arrangements or other issues conspire to limit effective transfer rates to something much lower).</li>\n<li><code>-h</code> - Makes the output more &quot;human-readable.&quot; This shows previfexs like K, M, or G. You can add it twice if you'd like 1024-based values instead of 1000-based. To be honest, I don't know why this isn't the default.</li>\n<li><code>-v</code> - Verbose mode. By default, <code>rsync</code> is silent. This is another behavior that I find strange in the present, but probably made more sense in the era of teletype terminals and very slow links. It's not really that verbose; it just tells you what files are being transferred and a brief summary at the end. You actually have to give <em>two</em> v's (<code>-vv</code>) for <code>rsync</code> to tell you which files it's skipping!</li>\n</ul>\n<p>Hope this helps speed up your next remote file transfer.\nIf there's any options you like which I may have missed, hit me up on Mastodon with a suggestion!</p>\n<p>Bonus pro tip: I had (until I recently switched to nushell) over a decade of accumulated shell history,\nand sometimes it's hard to keep the options straight.\nNaturally my history has a few different variations on a command like <code>rsync</code>.\nRather than searching through manpages with the equivalent of <code>grep</code>,\nI usually go to <a href=\"https://explainshell.com\">explainshell.com</a>.\nIt seems to be a frontend to the manapages that understands the various sections,\nproviding a much quicker explanation of what your switches do!</p>\n",
      "summary": "",
      "date_published": "2025-12-08T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "shell"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//my-current-favorite-headphones-and-amp-for-listening.html",
      "url": "https://ianwwagner.com//my-current-favorite-headphones-and-amp-for-listening.html",
      "title": "My Current Favorite Headphones (and Amp) for Listening",
      "content_html": "<p>It occurred to me that things which work well are often under-represented online.\nI absolutely love my listening headphones, so I thought I'd write a quick post about them.</p>\n<p>To say I love music is an understatement.\nMy iTunes... er... Apple Music library is has 61.5 <em>days</em> of it.\nAnd I am reasonably picky.\nAnd unlike my Steam library, I have listened to all of it!\nMany times over.</p>\n<p>I also tend to listen a lot in situations where headphones are the best output device.\nEither nobody else is awake (I'm listening to Arthur Rbenstein's performance of Chopin's Nocturnes right now, if that gives you a clue)\nor others are very much around who may not appreciate classic trance blasting at 100dB.\nHeadphones are a really fantastic way to listen to music,\nand if you care about quality, they are some of the best bang for your buck.\nYou can get some AMAZING quality cans for a fraction of what you'd pay for a comparable speaker setup.</p>\n<p>But headphones are also hard to get right, and <em>highly personal</em>.\nWhat follows is my current favorite, and the journey to here.\nI'll try to note cases where I'm aware of my own subjectivity.\nbut I probably won't get everything.</p>\n<h1><a href=\"#what-makes-a-good-headphone-for-me\" aria-hidden=\"true\" class=\"anchor\" id=\"what-makes-a-good-headphone-for-me\"></a>What Makes a Good Headphone for Me</h1>\n<p>I've worn headphones for a significant chunk of my computer time (an inordinate portion of my life) since 2011 or so.\nThat's about 15 years at the time of this writing.\nHere are some things that are important for me (or not important), which may help frame my opinion:</p>\n<ul>\n<li><strong>Excellent sound quality</strong> is non-negotiable. No AirPods, sorry.</li>\n<li><strong>Relatively natural frequency response</strong> is also important to me. I really dislike units which skew the response (usually to the bass side for popular brands).</li>\n<li><strong>I strongly dislike closed back designs</strong> since they kinda kill the sound stage depth and make me feel like I have ear muffs on dampening everything around me.\nFor reference, I tend to work in more or less quiet environments, and that's where I do much of my listening.\nI also strongly dislike <em>active</em> noise cancelling as it makes me feel like I've got a cold.</li>\n<li>They need to be light enough to wear for ~2 hours.</li>\n</ul>\n<h1><a href=\"#previous-cans\" aria-hidden=\"true\" class=\"anchor\" id=\"previous-cans\"></a>Previous Cans</h1>\n<p>Over the years, I've really liked Sennheiser.\nI had several mid to high end pairs over the years.\nI rather liked my HD558s from years past.\nThey were mostly neutral response and open back.\nBut those were a long time ago (I bought them in 2013).\nAnd current Sennheiser isn't what they once were.</p>\n<p>More recently, I bought the Grado 325x.\nThey were <em>crisp</em> on the high end.\nBut after about 8 months, I couldn't stand them any more.\nThery were far too unforgiving of all but a perfectly mastered track.\nThe lower frequencies were off, especially for electronic music (but sounded great for rock).\nThey were heavy.\nThey were uncomfortable (I hated the headband and the weird angle of the earpieces,\nwhich look cool as a piece of steampunk concept art but are highly impractcial IMO).</p>\n<p>To make matters worse, the cable is non-detatchable and soldered all the way through.\nIt also weighs a ton and is super inflexible.\nI hear there are a bunch of people that do cable mods,\nbut by the time my pair developed a loose connection (probably due to the above)\nthat made them practically unlistenable, I was ready for something else.</p>\n<h1><a href=\"#current-setup\" aria-hidden=\"true\" class=\"anchor\" id=\"current-setup\"></a>Current setup</h1>\n<p>Moderately annoyed, I decided to buy some new headphones almost a year ago.\nI didn't find much help in reviews honestly, and you probably shouldn't expect to here either.\nBut I ended up going for some Beyerdynamic DT 900 Pro X's.\nI definitely over-indexed on the detatchable cable by the way\n(I haven't had a single issue with the cabling, nor have I bothered detaching it, but it's the principle of the thing).\nThe sound quality on these is excellent, and they have a better sound stage than my old Sennheiser's.\nI liked some things about the Grados better, but these are way more versatile.\nThey are also comfortable to wear for 2-3 hours:\nthey are lighter, and the cushioning is amazing.\nEspecially with my glasses.\nMy only complaint is they are a bit tight fitting for my large head,\nand I can actually hear some squeaks from my glasses frame when moving around sometimes.</p>\n<p>Headphones also need a solid amp (+ DAC if you're using it like most people).\nI currently use a Schiit Magni.\nA ridiculously under-priced amp+DAC.\nI really don't have much to say except that it's an excellent little unit that sounds great.\nI haven't had any issues in the 2 years I've owned it,\nbeyond the decade and change issues with certain music players not filling buffers fast enough\nduring a cold start of a new album.</p>\n",
      "summary": "",
      "date_published": "2025-11-25T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "gear"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//elecom-deft-pro-two-months-in.html",
      "url": "https://ianwwagner.com//elecom-deft-pro-two-months-in.html",
      "title": "Elecom Deft Pro - Two Months In",
      "content_html": "<p>It's not quite two months in, but I said I'd give an update on the Elecom Deft Pro after some use, so here I am!</p>\n<h1><a href=\"#bad-stuff\" aria-hidden=\"true\" class=\"anchor\" id=\"bad-stuff\"></a>Bad Stuff</h1>\n<p>Bad news first...</p>\n<h2><a href=\"#initial-adjustment-period\" aria-hidden=\"true\" class=\"anchor\" id=\"initial-adjustment-period\"></a>Initial Adjustment Period</h2>\n<p>I'm not gonna lie, the initial adjustent period on this was rougher than I expected.\nThis trackballl was extremely uncomfortable for the first week,\nand not particularly comfortable for the following 2 or 3 weeks either.\nExpect to take some time getting used to this.</p>\n<h2><a href=\"#form-factor\" aria-hidden=\"true\" class=\"anchor\" id=\"form-factor\"></a>Form Factor</h2>\n<p>My biggest complaint is probably that it's too small and too light.\nIt should be 2-3 times heavier in my opinion to stay put.\nAnd I guess I should have guessed it from the name, but it's really small.\nAlmost too small for my hands.</p>\n<h2><a href=\"#ergonomics\" aria-hidden=\"true\" class=\"anchor\" id=\"ergonomics\"></a>Ergonomics</h2>\n<p>I'm not sure what it is with Elecom, but a lot of the angles make no sense to me.\n&quot;Straight&quot; as measured from the USB port puts the angle for the main (&quot;left&quot;) click button off by like 5-10 degrees.\nSo I always want to have it off center, but that's challenging on my admittedly small keyboard tray.\nI'm also not really sure whose hand they designed whatever is further right than the right (alt-right?) button for.\nI never planned on using this or the middle click button anyways, but they are just weirdly positioned.\nThe forward and back buttons are also weirdly positioned.</p>\n<p>These were also weirdly positioned on the HUGE, but the Deft Pro seems worse.\nIt's exacerbated by the fact that the thing weighs basically nothing,\nwhich is not a positive thing when you're trying to poke it at weird angles that will definitely move it on your desk.\nAnd I'm not sure what the other weird button and switch are closest to the hand rest and on level with the left click button,\nbut they are in such weird positions I will NEVER use them.</p>\n<h2><a href=\"#scrolling\" aria-hidden=\"true\" class=\"anchor\" id=\"scrolling\"></a>Scrolling</h2>\n<p>Oh my, this was a huge (lol) adjustment...\nThe scroll wheel is SO CRISP!\nI also kinda hate it, since even at max speed it's way too slow.\nI literally reach for my MacBook trackpad any time I want to scroll fast through a large document.\nThe scroll wheel <em>is</em> precise, but it's also kinda terrible IMO.\nAnd unlike the HUGE where you could sort of scroll horizontally by awkwardly nudging the wheel up or down,\nthe Deft is so tiny and light, and the wheel is so stiff, that you can just forget even the most awkward horizontal scrolling.\nUse your laptop trackpad for this.</p>\n<h1><a href=\"#good-stuff\" aria-hidden=\"true\" class=\"anchor\" id=\"good-stuff\"></a>Good Stuff</h1>\n<p>I'm sure I sound VERY negative at this point.\nAnd I really do have a lot of bad to say about this trackball.\nBut I'm still using it and actually do have some good things to say.</p>\n<h2><a href=\"#ball--mouse-behavior\" aria-hidden=\"true\" class=\"anchor\" id=\"ball--mouse-behavior\"></a>Ball / mouse behavior</h2>\n<p>Honestly I don't have any complaints about this.\nI saw some very polarizing opinions about the ball not rolling smoothly but I have had relatively few issues.\nIt's not sticking or anything for me and feels pretty good overall.\nMy precision feels slightly better than with the HUGE.</p>\n<h2><a href=\"#buttons-actually-working\" aria-hidden=\"true\" class=\"anchor\" id=\"buttons-actually-working\"></a>Buttons actually working!</h2>\n<p>The reason I replaced my HUGE was the unreliable buttons.\nMaybe my HUGE was a lemon, but the Deft Pro has been great.\nNo missed / duplicate click events.\nIt really just works.</p>\n<h1><a href=\"#overall\" aria-hidden=\"true\" class=\"anchor\" id=\"overall\"></a>Overall</h1>\n<p>Well, I'm still using it ;)\nSeveral things annoy me, but it's better than <em>my</em> HUGE.\nThe other trackball I considered was a Kensigton.\nI might still buy one, but only after I either:</p>\n<ol>\n<li>Replace my Kinesis Advantage2 with a smaller keyboard with fully disconnected halves\n(I'm eyeing the Advantage360 or one of the Cosmos Keyboards designs), or...</li>\n<li>Get a bigger keyboard drawer.</li>\n</ol>\n<p>Overall it's not a bad trackball, and you might even like it if you're primarily working mobile.\nBut I use this at my large desk and I have a few complaints.\nDespite my ergonomic complaints, it's still better than most mice,\nand your experience will <em>probably</em> be better than mine since my hands are large enough to make serious piano players envious.</p>\n",
      "summary": "",
      "date_published": "2025-11-19T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "gear"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//korean-music-beyond-k-pop-demon-hunters.html",
      "url": "https://ianwwagner.com//korean-music-beyond-k-pop-demon-hunters.html",
      "title": "Korean Music Beyond K-Pop Demon Hunters",
      "content_html": "<p>Pretty much overnight, it seems that a movie I had never even heard of became the most-watched movie ever on Netflix.\nPeople everywhere are now singing the songs from K-Pop Demon Hunters.\nIt originally seemed more popular outside Korea\n(the first dozen or so people I heard about it from were not Korean, nor were they living here).\nBut now it's all over the promotional marketing locally too.</p>\n<p>I absolutely <em>love</em> living here,\nand am immersed in many aspects of the culture,\nbut have never been a big K-pop fan.\nIn fact, I don't actually like <em>most</em> pop music anywhere in the world ;)</p>\n<p>If you, like me, have different musical tastes from the mainstream,\nthen this post is for you!\nIt's a collection of some of my favorite Korean music\nthat you've probably never heard of.</p>\n<p>There is no particular order to this post, so don't take this as some sort of ranking.\nIt's just a list of music and artists I like which can broaden your horizons.</p>\n<h1><a href=\"#candy-and-lollipop\" aria-hidden=\"true\" class=\"anchor\" id=\"candy-and-lollipop\"></a>Candy and Lollipop</h1>\n<p>Okay, let's kick off with some light... hip-hop?\nI don't really even know how to categorize this pair of songs,\nseparated by almost a decade but having a lot of similarities in both style and name.\nBefore the current American-influenced styles of rap and hip-hop started to dominate,\nthere was this more fun and weird side.</p>\n<p>First up is boy band H.O.T. with <a href=\"https://www.youtube.com/watch?v=3NUaXU1d-NY\">Candy</a>.\nThis was 1996, so it's probably the oldest on my list.\nIt's just light and fun.\nAnd very 90s... lighter side of hip hop.</p>\n<p>Over the next decade, hip-hop and rap grew even more popular.\nThis next song, Lollipop, has one of the more bizarre music videos,\nand is a collaboration with more people than you'll be able to keep track of.\nHere's the <a href=\"https://www.youtube.com/watch?v=zIRW_elc-rY\">MV</a>.</p>\n<h1><a href=\"#소찬휘---tears\" aria-hidden=\"true\" class=\"anchor\" id=\"소찬휘---tears\"></a>소찬휘 - Tears</h1>\n<p>This song was released in the year 2000.\nThis style of dance+rock fusion was actually quite popular in Korea at the time,\nbut you don't hear very many producers putting out stuff like this today.\nThis song is well-known and loved by many in their late 30s or older,\nand is one of the most popular 노래방 (singing room = Karaoke) songs from that era,\neven today.\nThe song is full of passion and energy,\nand is very technically challenging.\nHere's a recent <a href=\"https://youtu.be/3UZm-Mm7WDs?si=FkgXCFuvA_Mil4k3\">live performance</a>.</p>\n<h1><a href=\"#윤종신---좋니\" aria-hidden=\"true\" class=\"anchor\" id=\"윤종신---좋니\"></a>윤종신 - 좋니</h1>\n<p>The gist of the song is a post-breakup story.\nIt's more of a power ballad, which is NOT my usual style of music.\nBut this song is just SO good, it has to be on my list.\nWhen it was released in 2017, it did extremely well,\nand got loads of radio play, despite not fitting the typical pop mold of the time.\nHere's a <a href=\"https://www.youtube.com/watch?v=jy_UiIQn_d0&amp;list=RDjy_UiIQn_d0&amp;start_radio=1\">live performance</a>\nthat also has some English subtitles.</p>\n<h1><a href=\"#frontier-leaders---new-stage\" aria-hidden=\"true\" class=\"anchor\" id=\"frontier-leaders---new-stage\"></a>Frontier Leaders - New Stage</h1>\n<p>Okay, this song probably does qualify as K-pop...\nThere is <em>some</em> good stuff in there ;)</p>\n<p>This is also the newest release on the list.\nIt grabbed my attention immediately\nbecause the musical structure, choice of scales, and rhythms\naren't typical in current K-pop.</p>\n<p>Here's the <a href=\"https://youtu.be/KpzAm3QFB2A?si=vEALVclevY1_koew\">music video</a>.\nIt's definitely an idol group,\nbut the MV is artistically solid and generally quite creative\n(it alludes to both streamer culture and Korean-style photo booths)!</p>\n<h1><a href=\"#moon-kyoo\" aria-hidden=\"true\" class=\"anchor\" id=\"moon-kyoo\"></a>Moon Kyoo</h1>\n<p>I have a hard time recommending a <em>specific</em> track,\nbecause this guy is a pretty wide ranging artist,\nand much of what he does is live.\nHe does everything from <a href=\"https://www.youtube.com/watch?v=WUOUcT2aAtU\">deep house on one of my favorite labels</a>\nto <a href=\"https://youtu.be/poT_M6bVW7c?si=beq9pGK4x4vHb_2m\">ambient sets like this with a Eurorack modular</a>.</p>\n<h1><a href=\"#lazenca-save-us\" aria-hidden=\"true\" class=\"anchor\" id=\"lazenca-save-us\"></a>Lazenca, Save Us</h1>\n<p>This song is getting REALLY out of the mold now...\nWe're in metal territory.</p>\n<p>The original was released back in 1997 by the group N.E.X.T,\nbut a newer in 2016 on a Korean TV show &quot;The King of Mask Singer&quot; made the song much more popular.\n(The concept of the show is that some famous singer puts on a mask, sings a song,\nand then celebrity judges have to guess who's behind the mask at the end).</p>\n<p>On one of these episodes, 하현우 does an absolutely <em>stellar</em> cover,\nwhich I liked even better than the original.\nHis vocal style is really on point for the song,\nand the high notes are absolutely killer!</p>\n<p>You can watch a <a href=\"https://youtu.be/A5qx1_9yMZo?si=eR1v1z3ezByt_Qcx\">video of the performance on YouTube</a>.\nIt's truly something with a full orchestra, drums, guitar... the whole bit.</p>\n<h1><a href=\"#cherryfilter---오리-날다\" aria-hidden=\"true\" class=\"anchor\" id=\"cherryfilter---오리-날다\"></a>cherryfilter - 오리 날다</h1>\n<p>Last but definitely not least,\nthis song is fun, upbeat, and a bit punk-y in style.\nIt was released back in 2003 and bears a lot of resemblance to J-Rock at the time.</p>\n<p>The song is about a duck who dreams he can fly.\nHis mom scolds him and says ducks can't fly,\nbut that doesn't stop him from dreaming.</p>\n<p>It's such a beautiful song!\nAnd I love the metaphor about chasing your dreams.\nSomething that I really hope can continue to inspire the younger generation.</p>\n<p>And the vocal style of the lead singer is pretty unique.\nShe's happy and has piercing high notes, but also has that rock edge at several points.\nAnd in <a href=\"https://www.youtube.com/watch?v=1tR17y0lE-o&amp;list=RD1tR17y0lE-o&amp;start_radio=1\">live performances</a>\nshe does this cute little hand motion throughout,\nmimicking wing flapping motion, which I thought was a cool and endearing!</p>\n<p>I had this song stuck in my head for <em>weeks</em> after I discovered it.</p>\n",
      "summary": "",
      "date_published": "2025-11-09T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "music",
        "culture",
        "korea"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//const-assertions.html",
      "url": "https://ianwwagner.com//const-assertions.html",
      "title": "Const Assertions",
      "content_html": "<p>I'm currently working on a <a href=\"https://github.com/stadiamaps/valinor\">project</a> which involves a lot of lower level\ndata structures.\nBy lower level I mean things like layout and bit positions, and exact sizes being important.\nAs such, I have a number of pedantic lints enabled.</p>\n<p>One of the lints I use is <a href=\"https://rust-lang.github.io/rust-clippy/master/index.html#cast_precision_loss\"><code>cast_precision_loss</code></a>.\nFor example, casting from <code>usize</code> to <code>f32</code> using the <code>as</code> keyword is not guaranteed to be exact,\nsince <code>f32</code> can only precisely represent integrals up to 23 bits of precision (due to how floating point is represented).\nAbove this you can have precision loss.</p>\n<p>This lint is pedantic because it can generate false positives where you <em>know</em> the input can't ever exceed some threshold.\nBut wouldn't it be nice if we could go from &quot;knowing&quot; we can safely disable a lint to actually <em>proving</em> it?</p>\n<p>The first thing that came to mind was runtime assertions, but this is kind of ugly.\nIt requires that we actually exercise the code at runtime, for one.\nWe <em>should</em> be able to cover this in unit tests, but even if we do that,\nan assertion isn't as good as a compile time guarantee.</p>\n<h1><a href=\"#const\" aria-hidden=\"true\" class=\"anchor\" id=\"const\"></a><code>const</code></h1>\n<p>One thing I didn't mention, and the reason I &quot;know&quot; that the lint would be fine is that I'm using a <code>const</code> declaration.\nHere's a look at what that's like:</p>\n<pre><code class=\"language-rust\">pub const BUCKET_SIZE_MINUTES: u32 = 5;\npub const BUCKETS_PER_WEEK: usize = (7 * 24 * 60) as usize / BUCKET_SIZE_MINUTES as usize;\n</code></pre>\n<p>This isn't the same as a <code>static</code> or a <code>let</code> binding.\n<code>const</code> expressions are actually evaluated at compile time.\n(Well, most of the time... there's a funny edge case where <code>const</code> blocks which can <em>never</em> executed at runtime\n<a href=\"https://doc.rust-lang.org/reference/expressions/block-expr.html#const-blocks\">is not guaranteed to evaluate</a>.)</p>\n<p>You can't do everything in <code>const</code> contexts, but you can do quite a lot, including many kinds of math.\nNot all math; some things like square root and trigonometry are not yet usable in <code>const</code> contexts\nsince they are not reproducible across architectures (and sometimes even on the same machine, it seems).</p>\n<h1><a href=\"#assert-in-a-const-block\" aria-hidden=\"true\" class=\"anchor\" id=\"assert-in-a-const-block\"></a><code>assert!</code> in a <code>const</code> block</h1>\n<p>And now for the cool trick!\nI want to do a division here, and to do so, I need to ensure the types match.\nThis involves casting a <code>usize</code> to <code>f32</code>,\nwhich can cause truncation as noted above.</p>\n<p>But since <code>BUCKETS_PER_WEEK</code> is a constant value,\nwe can actually do an assertion against it <em>in our <code>const</code> context</em>.\nThis lets us safely enable the lint, while ensuring we'll get a compile-time error if this ever changes!\nThis has no runtime overhead.</p>\n<pre><code class=\"language-rust\">#[allow(clippy::cast_precision_loss, reason = &quot;BUCKETS_PER_WEEK is always &lt;= 23 bits&quot;)]\nconst PI_BUCKET_CONST: f32 = {\n    // Asserts the invariant; panics at compile time if violated\n    assert!(BUCKETS_PER_WEEK &lt; 2usize.pow(24));\n\t// Computes the value\n    std::f32::consts::PI / BUCKETS_PER_WEEK as f32\n};\n</code></pre>\n<p>This is all possible in stable Rust at the time of this writing (tested on 1.89).\nI saw some older crates out there which appeared to do this,\nbut as far as I can tell, they are no longer necessary.</p>\n<p>Here's a <a href=\"https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2024&amp;gist=dd294501c156f8d67f72a21f7dea27c4\">Rust Playground</a>\npreloaded with the sample code\nwhere you can verify that changing <code>BUCKETS_PER_WEEK</code> to a disallowed value causes a compile-time error.</p>\n",
      "summary": "",
      "date_published": "2025-10-07T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "rust"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//elecom-deft-pro-first-impressions.html",
      "url": "https://ianwwagner.com//elecom-deft-pro-first-impressions.html",
      "title": "Elecom Deft Pro - First Impressions",
      "content_html": "<p>I recently picked up a new trackball to replace my prematurely aging Elecom HUGE.\nHere are my first impressions after only a few hours, since there wasn't much I could find online comparing them properly.</p>\n<p>NOTE: I have an updated review after ~2 months with the unit <a href=\"elecom-deft-pro-two-months-in.html\">here</a>.</p>\n<h1><a href=\"#why-i-use-a-trackball\" aria-hidden=\"true\" class=\"anchor\" id=\"why-i-use-a-trackball\"></a>Why I use a Trackball</h1>\n<p>To get the obvious question out of the way first,\nI have used a trackball for about 5 years as my primary pointer device.\nI switched from all manner of mice and trackpads due to ergonomics.\nI had pretty bad RSI for many years, due to excessive computer use,\nand a trackball was one of the key changes that solved my issues.</p>\n<p>The trackball actually had a larger impact than switching to Dvorak (minimal improvement)\nor switching to a better ergonomic keyboard (Kinesis Advantage 2 QD; incremental, but not order of magnitude improvement).\nAnd yes, I'm aware of vertical mice.\nI tried a Logitech one for about 2 years and it actually made things worse.</p>\n<h1><a href=\"#size\" aria-hidden=\"true\" class=\"anchor\" id=\"size\"></a>Size</h1>\n<p>The biggest difference is probably the size.\nThe Deft Pro has a much smaller footprint than the HUGE.\nPart of the reason for this early replacement is that I ordered a replacement keyboard drawer online the other week.\nBeing too lazy to measure, and just going off the scale photos, it looked <em>plenty</em> big.</p>\n<p><figure><img src=\"media/huge-on-the-tray.jpeg\" alt=\"An Elecom HUGE trackball, with the side hanging off the edge of a white keyboard tray\" /></figure></p>\n<p>Narrator: it was, in fact, far too small and barely fits the keyboard.</p>\n<p>So, with the HUGE practically falling off,\ncombined with my bad habit of snacking and drinking at my desk late at night contributing to click malfunction\n(weirdly it would DOUBLE register clicks!), I needed a replacement.</p>\n<p><figure><img src=\"media/deft-on-the-tray.jpeg\" alt=\"An Elecom Deft Pro trackball, which barely fits on the keyboard tray\" /></figure></p>\n<p>The Deft Pro is pretty much the size I wanted.\nIt fits (just barely) on my tiny keyboard tray,\nand it is quite comfortable to use.</p>\n<h1><a href=\"#weightgrip\" aria-hidden=\"true\" class=\"anchor\" id=\"weightgrip\"></a>Weight/grip</h1>\n<p>I have to say I wish this thing were a bit heavier.\nI want the thing to sit on my desk like a rock and not move.\nI tend to manipulate the ball with my fingertips,\nand keep other fingers on the buttons, but this tends to push the base off to the left.\nMaybe I'll look at a sticky setup or something...</p>\n<h1><a href=\"#control-positions\" aria-hidden=\"true\" class=\"anchor\" id=\"control-positions\"></a>Control positions</h1>\n<p>I can only give a partial review here, since I do not use most of the extra buttons.\nPartly because I can't be bothered to configure them (most apps won't support them easily),\nand partly because I don't like installing proprietary software with low level system access for marginal benefit.\n(This is a subtoot at Logitech Options, but Elecom is not much better.)\nSo, I only use the left+right click, and the forward/back buttons.\nThe forward/back buttons are plug-and-play\nwith my web browser and many dev tools like JetBrains IDEs.</p>\n<p>I think the HUGE positioning was a bit better for the forward/back buttons.\nThe scroll and left click position are fine for my usage style.\nThe right click though... That will take some getting used to.\nI think they designed the Deft to be held in your hand pretty closely,\nbut that isn't how I got used to using the HUGE.</p>\n<p>I'll give this a shot and see how it goes after a few weeks trying a different grip.\nI think right click is positioned fine for the intended style of gripping.</p>\n<h1><a href=\"#scroll-wheel\" aria-hidden=\"true\" class=\"anchor\" id=\"scroll-wheel\"></a>Scroll wheel</h1>\n<p>The Deft allows for much better precision, with each step being a nice crisp click.\nI personally liked the HUGE better.\nIt was pretty fluid, smooth, and not &quot;clicky.&quot;\nIt also could generate acceptable scrolling speeds.\nNot as good as an Apple trackpad, but acceptable.\nI had to jack up the scroll speed to max for the Deft,\nand will probably continue using my trackpad for gestures like this when I want to go fast.</p>\n<p>This is not a criticism of Elecom specifically; it's a universal gripe I have with most things\n<em>except</em> the Apple trackpad.</p>\n<h1><a href=\"#the-ball\" aria-hidden=\"true\" class=\"anchor\" id=\"the-ball\"></a>The ball</h1>\n<p>The ball itself is great!\nI found some reviews online complaining that it was sticky,\nor hard to manipulate precisely.\nI have not had any issues so far; it feels as good or better than the HUGE.</p>\n<h1><a href=\"#connectivity\" aria-hidden=\"true\" class=\"anchor\" id=\"connectivity\"></a>Connectivity</h1>\n<p>If I recall, the HUGE required you to select a connectivity option at purchase time.\nThe Deft Pro includes all in the same package: bluetooth, RF via USB wireless dongle, and cabled USB connection.\nThis is great if you like to travel with an external input device!</p>\n<p>I personally don't since I love my MacBook trackpad.\nBut if you do, the only thing I'd flag is that there is no internal battery.\nYou need a single AA battery rather than an internal rechargeable like the higher end Logitech mice.</p>\n<h1><a href=\"#overall-impressions\" aria-hidden=\"true\" class=\"anchor\" id=\"overall-impressions\"></a>Overall impressions</h1>\n<p>It lives up to the name: it's Deft.\nI like a few things about the HUGE better (the scroll wheel and right mouse button position being the main ones),\nbut I <em>did</em> buy this for a smaller footprint,\nand I think I can get used to the button position with a tighter grip.</p>\n<p>I'll post a follow up later once I have a chance to use it more.</p>\n",
      "summary": "",
      "date_published": "2025-09-28T00:00:00-00:00",
      "image": "media/huge-on-the-tray.jpeg",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "gear"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//finishing-dragonball-in-korean.html",
      "url": "https://ianwwagner.com//finishing-dragonball-in-korean.html",
      "title": "Finishing Dragonball in Korean",
      "content_html": "<p>Over the past few days of vacation I finally had the chance to finish reading Dragonball!\nI started this a few years ago and have gone in spurts, sometimes not reading for months at a time as work got in the way.</p>\n<p>This post is a short reflection on what I learned,\nwhich I hope will be useful for other language learners.</p>\n<h1><a href=\"#why-the-challenge\" aria-hidden=\"true\" class=\"anchor\" id=\"why-the-challenge\"></a>Why the challenge?</h1>\n<p>I did this because I wanted to improve my Korean for various practical reasons,\nbut <a href=\"learning-a-language-the-lazy-way.html\">I'm also very lazy when it comes to studying</a>.\nI also tend to like Japanese media a bit better than Korean\n(a few Korean webtoons and comics are good, but I'm more a fan of the Japanese manga art style).\nSo I split the difference and tried to find manga that had high quality Korean translations.\nI also have fond memories of devouring comics as a kid (mostly Calvin &amp; Hobbes),\nand am sure that did something to improve my reading.</p>\n<h1><a href=\"#picking-the-series\" aria-hidden=\"true\" class=\"anchor\" id=\"picking-the-series\"></a>Picking the series</h1>\n<p>With the general medium and genre decided, I looked for a series.\nI had watched several of the classics in anime form, including One Piece and Fairy Tail,\nbut they were a bit too advanced for my Korean level ~4 years ago when I started this.\nThen an acquaintance who is fluent in Japanese mentioned that he had read Dragonball in Japanese\nmultiple times.</p>\n<p>I took a look at the preview and was surprised to find that it was not TOO far above my own level.\nIt was definitely challenging, but I could understand something like half the words,\nand most of the grammatical constructions, which was something!\nI should also not that I had never read any of the English translation,\nnor had I watched any of the English dubbed anime / films.</p>\n<h1><a href=\"#the-journey\" aria-hidden=\"true\" class=\"anchor\" id=\"the-journey\"></a>The journey</h1>\n<p>I was <em>extremely</em> lazy the first few years, and it was difficult to get going.\nReading was also a bit demoralizing at first with a failure rate of something like 50-60% for a while.\nBut the vocabulary and patterns were pretty repetitive so I did improve pretty quickly.</p>\n<p>I found it helpful to force myself to NOT use a dictionary after the first few volumes.\nThis is how most kids learn a language anyways, and it had a few benefits for me:</p>\n<ul>\n<li>It made the reading more enjoyable and easier to focus on, since I wasn't stopping multiple times per page to consult a (terrible) Korean-English dictionary.</li>\n<li>It made obvious which words were important to look up, since they kept popping up...</li>\n<li>But I didn't actually need to look them all up, since the meaning sometimes became more obvious from the context.</li>\n</ul>\n<p>The story was (IMO) significantly more engaging for the first 16 volumes,\nand the earlier chapters also had more attention to artwork detail in many respects.\nI don't regret reading all the way to the end, but it got <em>quite</em> repetitive and predictable to be honest.</p>\n<p>To complement the reading (and provide a reward for the slog initially),\nI also watched Dragonball and Dragonball Z Kai on <a href=\"https://laftel.net/\">Laftel</a> (NOTE: This may be region blocked),\nreading the chapters BEFORE watching them most of the time.\nI think that helped me focus on the text and improve my (weaker) reading.</p>\n<h1><a href=\"#retrospective-what-did-i-learn\" aria-hidden=\"true\" class=\"anchor\" id=\"retrospective-what-did-i-learn\"></a>Retrospective: what did I learn?</h1>\n<p>I significantly improved my Korean reading comprehension and vocabulary.\nI also improved my knowledge of more advanced grammatical structures,\nbut to a much lesser extent.\nDragonball is extremely repetitive, which was great for reinforcement learning,\nbut it didn't dramatically expand my grammatical prowess.</p>\n<p>One unexpected area I also improved was my ability to understand humor and make jokes!\nEspecially word plays.\nDragonball's Korean translation is excellent, and given the cultural similarity,\na lot of the subtler jokes around things like age and respect translate well.</p>\n<p>I can also sound like a cheeky teenager and know a lot of ridiculous sounding insults!</p>\n<p>This will NOT help so much if you're interested primarily in learning household / everyday conversational patterns,\nbut it IS conversational, so it can be a good complement to other TV shows and books.</p>\n<h1><a href=\"#the-next-chapter\" aria-hidden=\"true\" class=\"anchor\" id=\"the-next-chapter\"></a>The next chapter</h1>\n<p>What will I do next?\nI haven't really decided on the next series,\nbut One Piece and Fairy Tail are probably reasonable options now.</p>\n<p>I think I might also work on my Japanese a bit on the side.\nMy Japanese is nowhere near as good as my Korean,\nbut the success of this experiment makes me think I could probably work my way slowly through the first 16 volumes.\nAs it's somewhat aimed at kids, it helpfully has Furigana so I could really level up my Kanji recognition.</p>\n",
      "summary": "",
      "date_published": "2025-09-08T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "learning",
        "languages",
        "self-improvement"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//unzip-utf-8-docker-and-c-locales.html",
      "url": "https://ianwwagner.com//unzip-utf-8-docker-and-c-locales.html",
      "title": "Unzip, UTF-8, Docker, and C Locales",
      "content_html": "<p>Today's episode of &quot;things that make you go 'wat'&quot; is sponsored by <code>unzip</code>.\nYes, the venerable utility ubiquitous on UNIX-like systems.\nI mean, what could possibly go wrong?</p>\n<p>This morning I was minding my own business sipping a coffee,\nwhen suddenly the &quot;simple&quot; CI pipeline I was working on failed.\nI literally copied the command that failed out of a shell script.\nWhich ran on the exact same machine previously.\nAnd the command that failed was a simple <code>unzip</code>.\nHuh?</p>\n<p>I initially assumed the file may have been corrupted, or maybe the archive had failed in a weird way\n(I don't control the process, and was downloading it from the internet).\nWhile it was running again, I started hitting <code>Page Down</code> looking for oddities in the log.\nI was greeted by this near the end of the output:</p>\n<pre><code>se/municipality_of_savsj#U00f6-addresses-city.geojson:  mismatching &quot;local&quot; filename (se/municipality_of_savsjö-addresses-city.geojson),\n         continuing with &quot;central&quot; filename version\n</code></pre>\n<p>Weird, huh?\nIt looks like it's trying to unzip a file with a UTF-8 name, which should be supported.</p>\n<p>I <code>ssh</code>'d into the remote machine to give it a try in the terminal.\nI've run this command dozens of times, and wanted to see if I got the same output in a standard <code>bash</code> remote terminal.\nGiven that log output is sometimes inscrutable, I thought &quot;maybe it wasn't the unzip itself that failed directly?&quot;\nOr something...</p>\n<p>Well, the <code>unzip</code> worked in the regular bash login shell on the same host.\nSo there must be <em>something</em> different about the CI environment.</p>\n<h1><a href=\"#searching-for-the-root-cause\" aria-hidden=\"true\" class=\"anchor\" id=\"searching-for-the-root-cause\"></a>Searching for the root cause</h1>\n<p>As a first resort, I started digging through the help and man pages.\nThe first amusing factoid I learned was that apparently the last official release of this utility happened in 2009.\nI guess zip doesn't evolve much, eh?</p>\n<p>The man page was actually pretty detailed,\nbut it didn't have a lot to say about Unicode, or anything else obvious related to the error.\nAnd the project doesn't exactly have an active central issue tracker (there is one on SourceForge, but replies are few and far between).\nI couldn't find anyone else talking about this specific error in the usual places on the internet either.</p>\n<p>I did however find this interesting <a href=\"https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=918894\">bug report via a Debian mailing list</a>\nfrom 2019.\nThe thing that caught my eye was the code sample and mention of locales.</p>\n<p>Oh no...\nWhat if this actually changes behavior based on your system locale?\nThen it started coming back to me... something about a dozen or so environment variables\nthat determine the behavior of C programs in bizarre ways...</p>\n<p>So I ran <code>locale</code> on the <code>ssh</code> session and got something reasonable back:</p>\n<pre><code>$ locale\nLANG=C.UTF-8\nLANGUAGE=\nLC_CTYPE=&quot;C.UTF-8&quot;\nLC_NUMERIC=&quot;C.UTF-8&quot;\nLC_TIME=&quot;C.UTF-8&quot;\nLC_COLLATE=&quot;C.UTF-8&quot;\nLC_MONETARY=&quot;C.UTF-8&quot;\nLC_MESSAGES=&quot;C.UTF-8&quot;\nLC_PAPER=&quot;C.UTF-8&quot;\nLC_NAME=&quot;C.UTF-8&quot;\nLC_ADDRESS=&quot;C.UTF-8&quot;\nLC_TELEPHONE=&quot;C.UTF-8&quot;\nLC_MEASUREMENT=&quot;C.UTF-8&quot;\nLC_IDENTIFICATION=&quot;C.UTF-8&quot;\nLC_ALL=\n</code></pre>\n<p>Then, to avoid wasting even more hours of CI time (it's a long job whose source is a ~100GB ZIP file),\nI set about replicating the environment of the runner.\nFortunately it left a Docker volume behind for me.\nSo, I launched an interactive Docker container using the same image that the CI job used (<code>python:3.13</code>).\nSomewhat surprisingly, the output of <code>locale</code> was a bunch of variables set to.... nothing!\nNow we're getting somewhere!</p>\n<p>To confirm that I could indeed reproduce the failure, I ran <code>unzip</code> again in the container,\nand sure enough, I got the same log message, and the exit code was <code>1</code>.\nNow we've confirmed how to reproduce the issue at least, so we can test a fix.</p>\n<h1><a href=\"#how-locale-affects-unzip\" aria-hidden=\"true\" class=\"anchor\" id=\"how-locale-affects-unzip\"></a>How locale affects <code>unzip</code></h1>\n<p>Since <code>unzip</code> is single threaded (read: SLOW),\nI spent my time during the tests looking through the source code to try and confirm my theory about the locale variables being the issue.\nThe official version seems to be <a href=\"https://sourceforge.net/projects/infozip/\">hosted on SourceForge</a>, which is apparently still a thing.\n(I'm sure there are a lot of Debian patches, but I just wanted a quick way to peruse the code).\nAmusingly, there were 47,074 downloads/week of <code>unzip60.tar.gz</code>, and only 36 downloads/week of the ZIP version.</p>\n<p>Eventually, I found what I was looking for in <code>unzip.c</code>:</p>\n<pre><code class=\"language-c\">int unzip(__G__ argc, argv)\n    __GDEF\n    int argc;\n    char *argv[];\n{\n#ifndef NO_ZIPINFO\n    char *p;\n#endif\n#if (defined(DOS_FLX_H68_NLM_OS2_W32) || !defined(SFX))\n    int i;\n#endif\n    int retcode, error=FALSE;\n#ifndef NO_EXCEPT_SIGNALS\n#ifdef REENTRANT\n    savsigs_info *oldsighandlers = NULL;\n#   define SET_SIGHANDLER(sigtype, newsighandler) \\\n      if ((retcode = setsignalhandler(__G__ &amp;oldsighandlers, (sigtype), \\\n                                      (newsighandler))) &gt; PK_WARN) \\\n          goto cleanup_and_exit\n#else\n#   define SET_SIGHANDLER(sigtype, newsighandler) \\\n      signal((sigtype), (newsighandler))\n#endif\n#endif /* NO_EXCEPT_SIGNALS */\n\n    /* initialize international char support to the current environment */\n    SETLOCALE(LC_CTYPE, &quot;&quot;);\n\n#ifdef UNICODE_SUPPORT\n    /* see if can use UTF-8 Unicode locale */\n# ifdef UTF8_MAYBE_NATIVE\n    {\n        char *codeset;\n#  if !(defined(NO_NL_LANGINFO) || defined(NO_LANGINFO_H))\n        /* get the codeset (character set encoding) currently used */\n#       include &lt;langinfo.h&gt;\n\n        codeset = nl_langinfo(CODESET);\n#  else /* NO_NL_LANGINFO || NO_LANGINFO_H */\n        /* query the current locale setting for character classification */\n        codeset = setlocale(LC_CTYPE, NULL);\n        if (codeset != NULL) {\n            /* extract the codeset portion of the locale name */\n            codeset = strchr(codeset, '.');\n            if (codeset != NULL) ++codeset;\n        }\n#  endif /* ?(NO_NL_LANGINFO || NO_LANGINFO_H) */\n        /* is the current codeset UTF-8 ? */\n        if ((codeset != NULL) &amp;&amp; (strcmp(codeset, &quot;UTF-8&quot;) == 0)) {\n            /* successfully found UTF-8 char coding */\n            G.native_is_utf8 = TRUE;\n        } else {\n            /* Current codeset is not UTF-8 or cannot be determined. */\n            G.native_is_utf8 = FALSE;\n        }\n        /* Note: At least for UnZip, trying to change the process codeset to\n         *       UTF-8 does not work.  For the example Linux setup of the\n         *       UnZip maintainer, a successful switch to &quot;en-US.UTF-8&quot;\n         *       resulted in garbage display of all non-basic ASCII characters.\n         */\n    }\n# endif /* UTF8_MAYBE_NATIVE */\n</code></pre>\n<p>And there we have it, right at the start of the program...</p>\n<p>This is a bit rough to grok if you don't regularly read C,\nbut the gist of it is that it tries to look at the system locale\nusing <a href=\"https://en.cppreference.com/w/c/locale/setlocale.html\"><code>setlocale</code></a>.\nThe <code>LC_CTYPE</code> specifies the types of character used in the locale.\nThe second argument is... very C.\nThe function will &quot;install the specified system locale... as the new C locale.&quot;</p>\n<p>It defaults to <code>&quot;C&quot;</code> at startup, per the docs.\nIn <code>unzip.c</code> though, the authors use the magic value <code>&quot;&quot;</code>,\nwhose behavior is to set the locale to the &quot;user-preferred locale&quot;.\nThis comes from those environment variables.\nA few lines down, past the <code>#ifdefs</code> gating unicode support,\nthey parse the codeset portion by passing <code>NULL</code>, which is ANOTHER magic value\nthat simply returns the current value.\n(So this two-step dance loads the preferred locale from the environment variables,\nwhich are empty in this case, and then inspects the codeset.).</p>\n<p>If your environment variables do not explicitly specify UTF-8,\nyou will get some.... strange and undesirable behavior, apparently,\nif your archive contains unicode file names!</p>\n<p>I'm pretty sure there's a good historical reason for this.\nGiven that many C library functions are locale-sensitive,\nand UTF-8 support is much younger than UNIX and (obviously) C.\nBut I found this behavior from <code>unzip</code> to be a bit surprising nonetheless.\nAnd rather than setting a sensible default like <code>C.UTF-8</code>,\nit turns out most Docker containers start with none!</p>\n<h1><a href=\"#the-fix\" aria-hidden=\"true\" class=\"anchor\" id=\"the-fix\"></a>The Fix</h1>\n<p>Fortunately this is pretty easy to fix.\nJust <code>export LC_ALL=&quot;C.UTF-8&quot;</code> before using <code>unzip</code>.\n(There is probably a more granular approach, but the <code>LC_ALL</code> sledgehammer does the job.)\nKinda crazy that you have to do this,\nbut hopefully this saves someone else an afternoon of debugging!</p>\n",
      "summary": "",
      "date_published": "2025-08-27T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "unicode"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//rootless-gitlab-ci-container-builds-with-buildkit.html",
      "url": "https://ianwwagner.com//rootless-gitlab-ci-container-builds-with-buildkit.html",
      "title": "Rootless GitLab CI Container Builds with BuildKit",
      "content_html": "<p>Forgive me in advance, but this post will probably be a bit rant-y.\nIf you're looking for a way to do container builds in GitLab CI without a lot of fuss,\nthis article is for you.</p>\n<h1><a href=\"#rip-kaniko\" aria-hidden=\"true\" class=\"anchor\" id=\"rip-kaniko\"></a>RIP Kaniko</h1>\n<p>I'm writing this post because Google recently canned yet another project: Kaniko.\nThis used to be pretty much the only way to build container images in Kubernetes.\nThis probably hasn't been the case for quite some time,\nbut devops is a chore for me, and my level of involvement is usually\njust enough to get my actual work done.</p>\n<p>Anyways, there are a few possible replacements, including podman buildah and BuildKit.\nWith not so much as a finger to the wind, I decided that BuildKit looked like the more polished and capable option,\nso I went with that (even though I actually use podman on servers way more often than Docker proper).\nI found the documentation for switching to be a bit lacking,\nso you all get this post!</p>\n<h1><a href=\"#gitlab-runner-setup\" aria-hidden=\"true\" class=\"anchor\" id=\"gitlab-runner-setup\"></a>GitLab Runner Setup</h1>\n<p>I've used GitLab CI for years and find it to be an extremely capable and easy to configure system.\nRunners are <a href=\"https://docs.gitlab.com/runner/install/\">extremely easy to set up</a>.\nRead the docs for details, but I'll whizz through a Docker-based setup (I haven't gotten around to k8s, so this is plain Docker).</p>\n<p>First, create a volume to persist the configuration:</p>\n<pre><code class=\"language-shell\">docker volume create buildkit-gitlab-runner-config\n</code></pre>\n<p>Then, start the runner for the first time.\nI've added a few flags to set up the required volumes, and ensure it restarts automatically.</p>\n<pre><code class=\"language-shell\">docker run -d --name buildkit-gitlab-runner --restart always -v /var/run/docker.sock:/var/run/docker.sock -v buildkit-gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest\n</code></pre>\n<p>The runner will launch, ish, but won't do anything useful without configuration + registration.\nYou can use an ephemeral instance of the runner image to register it.\nThis prompts you for your GitLab server URL and a token.\nYou get a token by logging into the GitLab admin section and registering a new runner.\nI also set the default image to <code>moby/buildkit:rootless</code>, but this is optional.</p>\n<pre><code class=\"language-shell\">docker run --rm -it -v buildkit-gitlab-runner-config:/etc/gitlab-runner gitlab/gitlab-runner:latest register\n</code></pre>\n<p>Next, get the path to the volume with the config using <code>docker volume inspect buildkit-gitlab-runner-config</code>.\nInside that directory (probably only readable by root),\nyou'll see the newly generated <code>config.toml</code>.</p>\n<p>To enable running container builds, you'll need this in a runners.docker section:</p>\n<pre><code class=\"language-toml\">security_opt = [&quot;seccomp:unconfined&quot;, &quot;apparmor:unconfined&quot;]\n</code></pre>\n<p>This is a bit confusing, since the GitLab docs <em>do</em> document this but,\nat the time of this writing, there are no examples and the description of the format is confusing.\nSee <a href=\"https://github.com/moby/buildkit/blob/master/docs/rootless.md#docker\">the BuildKit docs</a>\nsection on Docker for an explanation of why these options are necessary but safe enough for rootless builds.\nThere's also a third flag, <code>systempaths=unconfined</code>, which I've omitted (we'll revisit in a moment).</p>\n<p>In short, the above gets you rootless image builds from within a dockerized GitLab CI runner,\n<strong>without resorting to privileged containers or docker-in-docker</strong>.</p>\n<h1><a href=\"#example-gitlab-ciyml\" aria-hidden=\"true\" class=\"anchor\" id=\"example-gitlab-ciyml\"></a>Example <code>.gitlab-ci.yml</code></h1>\n<p>Now let's look at what you need to get this working in your CI configuration.\nI'll assume your repo has a Dockerfile in it already and you want to build + push to your GitLab Container Registry.</p>\n<pre><code class=\"language-yaml\">stages:\n  - build\n\ncontainerize:\n  stage: build\n  image:\n    name: moby/buildkit:rootless\n    entrypoint: [ &quot;&quot; ]  # !!\n  variables:\n    BUILDKITD_FLAGS: --oci-worker-no-process-sandbox\n  tags:\n    - docker\n    - buildkit-rootless\n  before_script:\n    # We have some more elaborate logic to our tag naming, but that's irrelevant...\n    - export IMAGE_TAG=&quot;$CI_COMMIT_TAG&quot;\n    # Container registry credentials\n    - mkdir -p ~/.docker\n    - echo &quot;{\\&quot;auths\\&quot;:{\\&quot;$CI_REGISTRY\\&quot;:{\\&quot;username\\&quot;:\\&quot;$CI_REGISTRY_USER\\&quot;,\\&quot;password\\&quot;:\\&quot;$CI_REGISTRY_PASSWORD\\&quot;}}}&quot; &gt; ~/.docker/config.json\n  script:\n    - buildctl-daemonless.sh build\n        --frontend dockerfile.v0\n        --local context=.\n        --local dockerfile=.\n        --output type=image,name=$CI_REGISTRY_IMAGE:$IMAGE_TAG,push=true\n        --opt build-arg:CI_JOB_TOKEN=$CI_JOB_TOKEN\n</code></pre>\n<p>This should be pretty much copy+paste for most projects.\nA few things to note:</p>\n<ol>\n<li>The GitLab documentation does not (presently) state that you need to explicitly clear the entrypoint.\nI am not sure if I've made a mistake elsewhere in my Kaniko migration, but we needed this for Kaniko,\nand we seem to need it for BuildKit too. Without this, it launches a build daemon or something\nand sits there patiently waiting for instructions.\nUpon trying to make an MR myself, I realized <a href=\"https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199319\">there was one already open</a>.</li>\n<li>I've added some tags.\nWe operate a heterogenous bunch of self-hosted runners, and use tags to ensure the right capabilities.\nThis is optional / can be adapted for your needs.</li>\n<li>We set <code>BUILDKITD_FLAGS</code> using the flag that BuildKit discourages.\nSince we'd like the same config to work on k8s runners too, we have to use this.</li>\n<li>This config propagates the CI job token to the Docker builder as an <code>ARG</code>. Why? I'm glad you asked...</li>\n</ol>\n<h1><a href=\"#bonus-transitive-dependencies-private-repos-and-cargo\" aria-hidden=\"true\" class=\"anchor\" id=\"bonus-transitive-dependencies-private-repos-and-cargo\"></a>Bonus: Transitive dependencies, Private Repos, and Cargo</h1>\n<p>We have a lot of internal crates in private repos.\nRecently I hit a snag with our previous approach to authenticated pulls though.\nOur <code>Cargo.toml</code> files use git SSH links, which won't work in CI.\nBut you can authenticate using HTTPS and a job token!</p>\n<p>Our previous approach was to use <code>sed</code> to rewrite <code>Cargo.toml</code> and <code>Cargo.lock</code>.\nIt worked until we had a transitive dependency (a direct dependency on one private crate,\nwhich in turn had a dependency on another private crate).\nI don't know why this broke exactly, since we do use <code>Cargo.lock</code>,\nbut regardless, it was brittle.</p>\n<p>The solution was to amed our <code>Dockerfile</code> with an <code>ARG CI_JOB_TOKEN</code>.\nThe build script then does some <code>git</code> magic to rewrite the SSH requests into HTTPS ones.\nI don't know why this feature exists,\nbut I'm happy I don't need to figure out how to run a private crate registry!</p>\n<pre><code class=\"language-shell\"># Git hacks\ngit config --global credential.helper store\necho &quot;https://gitlab-ci-token:${CI_JOB_TOKEN}@git.mycompany.com&quot; &gt; ~/.git-credentials\ngit config --global url.&quot;https://gitlab-ci-token:${CI_JOB_TOKEN}@git.mycompany.com&quot;.insteadOf ssh://git@git.mycompany.com\n</code></pre>\n<p>Just add this to your build script and replace the domain with your actual GitLab domain,\nand you'll have no more issues with transitive dependencies and authenticated pulls.</p>\n",
      "summary": "",
      "date_published": "2025-07-31T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "devops"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//optimizing-rust-builds-with-target-flags.html",
      "url": "https://ianwwagner.com//optimizing-rust-builds-with-target-flags.html",
      "title": "Optimizing Rust Builds with Target Flags",
      "content_html": "<p>Recently I've been doing some work using <a href=\"https://datafusion.apache.org/\">Apache DataFusion</a> for some high-throughput data pipelines.\nOne of the interesting things I noticed on the user guide was the suggestion to set\n<code>RUSTFLAGS='-C target-cpu=native'</code>.\nThis is actually a pretty common optimization (which I periodically forget about and rediscover),\nso I thought I'd do a quick writeup on this.</p>\n<h1><a href=\"#background-cpu-features\" aria-hidden=\"true\" class=\"anchor\" id=\"background-cpu-features\"></a>Background: CPU features</h1>\n<p>A compiler translates your &quot;idiomatic&quot; code into low-level instructions.\nModern optimizing compilers are pretty good at figuring out ways to cleverly rewrite your code\nto make it faster, while still being functionally equivalent at execution time.\nThe instructions may be reordered from what your simple mental model expects,\nand they may even have no resemblance.\nThis includes rewriting some loop-like (or iterator) patterns into &quot;vectorized&quot; code using SIMD instructions\nthat perform some operation on multiple values at once.</p>\n<p>Special instruction families like this often vary within a single architecture,\nwhich may be surprising at first.\nThe compiler can be configured to enable (or disable!) specific &quot;features&quot;,\noptimizing for compatibility or speed.</p>\n<p>In <code>rustc</code>, each <em>target triple</em> has a default set of CPU features enabled.\nIn the case of my work laptop, that's <code>aarch64-apple-darwin</code>.\nSince this architecture doesn't have a lot of variation among chips,\nthe compiler can make some pretty good assumptions about what's available.\n(In fact, for my specific CPU, the M1 Max, it's perfect!)\nBut we'll soon see this is not the case for the most common target: x86_64 Linux.</p>\n<h1><a href=\"#checking-available-features\" aria-hidden=\"true\" class=\"anchor\" id=\"checking-available-features\"></a>Checking available features</h1>\n<p>To figure out what features we could theoretically enable,\nwe need some CPU info from the machine we intend to deploy on.\nThe canonical way of checking CPU features on Linux is probably to <code>cat /proc/cpuinfo</code>.\nThis gives a lot more output that you probably need though.\nHelpfully, <code>rustc</code> includes a simple command that shows you the config\nfor the native CPU capabilities: <code>rustc --print=cfg -C target-cpu=native</code>.\nHere's what it looks like on one Linux machine:</p>\n<pre><code>debug_assertions\npanic=&quot;unwind&quot;\ntarget_abi=&quot;&quot;\ntarget_arch=&quot;x86_64&quot;\ntarget_endian=&quot;little&quot;\ntarget_env=&quot;gnu&quot;\ntarget_family=&quot;unix&quot;\ntarget_feature=&quot;adx&quot;\ntarget_feature=&quot;aes&quot;\ntarget_feature=&quot;avx&quot;\ntarget_feature=&quot;avx2&quot;\ntarget_feature=&quot;bmi1&quot;\ntarget_feature=&quot;bmi2&quot;\ntarget_feature=&quot;cmpxchg16b&quot;\ntarget_feature=&quot;f16c&quot;\ntarget_feature=&quot;fma&quot;\ntarget_feature=&quot;fxsr&quot;\ntarget_feature=&quot;lzcnt&quot;\ntarget_feature=&quot;movbe&quot;\ntarget_feature=&quot;pclmulqdq&quot;\ntarget_feature=&quot;popcnt&quot;\ntarget_feature=&quot;rdrand&quot;\ntarget_feature=&quot;rdseed&quot;\ntarget_feature=&quot;sse&quot;\ntarget_feature=&quot;sse2&quot;\ntarget_feature=&quot;sse3&quot;\ntarget_feature=&quot;sse4.1&quot;\ntarget_feature=&quot;sse4.2&quot;\ntarget_feature=&quot;ssse3&quot;\ntarget_feature=&quot;xsave&quot;\ntarget_feature=&quot;xsavec&quot;\ntarget_feature=&quot;xsaveopt&quot;\ntarget_feature=&quot;xsaves&quot;\ntarget_has_atomic=&quot;16&quot;\ntarget_has_atomic=&quot;32&quot;\ntarget_has_atomic=&quot;64&quot;\ntarget_has_atomic=&quot;8&quot;\ntarget_has_atomic=&quot;ptr&quot;\ntarget_os=&quot;linux&quot;\ntarget_pointer_width=&quot;64&quot;\ntarget_vendor=&quot;unknown&quot;\nunix\n</code></pre>\n<p><del>Aside: I'm not quite sure why, but this isn't a 1:1 match with <code>/proc/cpuinfo</code> on this box!\nIt definitely does support some AVX512 instructions,\nbut those don't show up in the native CPU options.\nIf anyone knows why, let me know!</del></p>\n<p><strong>UPDATE:</strong> Shortly after publishing this post, Rust 1.89 was released.\n<a href=\"https://github.com/rust-lang/rust/pull/138940\">This PR</a> linked in the release notes caught my eye.\nApparently the target features for AVX512 were not actually stable at the time of writing,\nbut they are now.\nRe-running the above command with version 1.89 of rustc now includes the AVX512 instructions.</p>\n<h1><a href=\"#checking-the-default-features\" aria-hidden=\"true\" class=\"anchor\" id=\"checking-the-default-features\"></a>Checking the default features</h1>\n<p>Perhaps the more interesting question which motivates this investigation is\nwhat the <em>defaults</em> are.\nYou can get this with <code>rustc --print cfg</code>.\nThis shows what you get when you run <code>cargo build</code> without any special configuration.\nHere's the output for the same machine:</p>\n<pre><code>debug_assertions\npanic=&quot;unwind&quot;\ntarget_abi=&quot;&quot;\ntarget_arch=&quot;x86_64&quot;\ntarget_endian=&quot;little&quot;\ntarget_env=&quot;gnu&quot;\ntarget_family=&quot;unix&quot;\ntarget_feature=&quot;fxsr&quot;\ntarget_feature=&quot;sse&quot;\ntarget_feature=&quot;sse2&quot;\ntarget_has_atomic=&quot;16&quot;\ntarget_has_atomic=&quot;32&quot;\ntarget_has_atomic=&quot;64&quot;\ntarget_has_atomic=&quot;8&quot;\ntarget_has_atomic=&quot;ptr&quot;\ntarget_os=&quot;linux&quot;\ntarget_pointer_width=&quot;64&quot;\ntarget_vendor=&quot;unknown&quot;\nunix\n</code></pre>\n<p>Well, that's disappointing, isn't it?\nBy default, you'd only get up to SSE2, which is over 20 years old by now!\nThis is a consequence of the diversity of the <code>x86_64</code> architecture.\nIf you want your binary to run <em>everywhere</em>, this is the price you'd have to pay.</p>\n<h1><a href=\"#enabling-features-individually\" aria-hidden=\"true\" class=\"anchor\" id=\"enabling-features-individually\"></a>Enabling features individually</h1>\n<p>While <code>-C target-cpu=native</code> will usually make your code faster on the build machine,\na lot of modern software is built by a CI pipeline on cheap runners, but deployed elsewhere.\nTo reliably target a specific set of features, use the <code>target-feature</code> flag.\nThis lets you specifically enable features you know will be available on the machine running the code.\nHere's an example of <code>RUSTFLAGS</code> that incorporates all of the above features.\nThis should enable builds to proceed from <em>any</em> other x86_64 Linux machine and while producing a binary\nthat supports the exact features of the deployment machine.</p>\n<pre><code class=\"language-shell\">RUSTFLAGS=&quot;-C target-feature=+adx,+aes,+avx,+avx2,+bmi1,+bmi2,+cmpxchg16b,+f16c,+fma,+fxsr,+lzcnt,+movbe,+pclmulqdq,+popcnt,+rdrand,+rdseed,+sse,+sse2,+sse3,+sse4.1,+sse4.2,+ssse3,+xsave,+xsavec,+xsaveopt,+xsaves&quot;\n</code></pre>\n<h1><a href=\"#enabling-features-by-x86-microarchitecture-level\" aria-hidden=\"true\" class=\"anchor\" id=\"enabling-features-by-x86-microarchitecture-level\"></a>Enabling features by x86 microarchitecture level</h1>\n<p>A few days after writing this, I accidentally stumbled upon something else when working out target flags\nfor a program I knew would have wider support across several datacenters.\nIt sure would be nice if there were some &quot;groups&quot; of commonly supported features, right?</p>\n<p>Turns out this exists, and it was staring right at me in the CPU list: microarchitecture levels!\nIf you list out all the available target CPUs via <code>rustc --print target-cpus</code> on a typical x86_64 Linux box,\nyou'll see that your default target CPU is <code>x86-64</code>.\nThis means it will run on all x86_64 CPUs, and as we discussed above, this doesn't give much of a baseline.\nBut there are 4 versions in total, going up to <code>x86-64-v4</code>.\nIt turns out that AMD, Intel, RedHat, and SUSE got together in 2020 to define these,\nand came up with some levels which are specifically designed for our use case of optimizing compilers!\nYou can find the <a href=\"https://en.wikipedia.org/wiki/X86-64\">full list of supported features by level on Wikipedia</a>\n(search for &quot;microarchitecture levels&quot;).</p>\n<p><code>rustc --print target-cpus</code> will also tell you which <em>specific</em> CPU you're on.\nYou can use this info to find which &quot;level&quot; you support.\nBut a more direct way to map to level support is to run <code>/lib64/ld-linux-x86-64.so.2 --help</code>.\nThanks, internet!\nYou'll get some output like this on a modern CPU:</p>\n<pre><code>Subdirectories of glibc-hwcaps directories, in priority order:\n  x86-64-v4 (supported, searched)\n  x86-64-v3 (supported, searched)\n  x86-64-v2 (supported, searched)\n</code></pre>\n<p>And if you run on slightly older hardware, you might get something like this:</p>\n<pre><code>Subdirectories of glibc-hwcaps directories, in priority order:\n  x86-64-v4\n  x86-64-v3 (supported, searched)\n  x86-64-v2 (supported, searched)\n</code></pre>\n<p>This should help if you're trying to aim for broader distribution rather than enabling specific features for some known host.\nThe line to target an x86_64 microarch level is a lot shorter.\nFor example:</p>\n<pre><code>RUSTFLAGS=&quot;-C target-cpu=x86-64-v3&quot;\n</code></pre>\n<p><strong>NOTE:</strong> As mentioned above, Rust 1.89 was released shortly after this post.\nThis incidentally brings support for AVX512 CPU features in the <code>x86-64-v4</code> target CPU,\nwhich were previously marked unstable.</p>\n<h1><a href=\"#dont-forget-to-measure\" aria-hidden=\"true\" class=\"anchor\" id=\"dont-forget-to-measure\"></a>Don't forget to measure!</h1>\n<p>Enabling CPU features doesn't always make things faster.\nIn fact, in some cases, it can even do the opposite!\nThis <a href=\"https://internals.rust-lang.org/t/slower-code-with-c-target-cpu-native/17315\">thread</a>\nhas some interesting anecdotes.</p>\n<h1><a href=\"#summary-of-helpful-commands\" aria-hidden=\"true\" class=\"anchor\" id=\"summary-of-helpful-commands\"></a>Summary of helpful commands</h1>\n<p>In conclusion, here's a quick reference of the useful commands we covered:</p>\n<ul>\n<li><code>rustc --print cfg</code> - Shows the compiler configuration that your toolchain will use by default.</li>\n<li><code>rustc --print=cfg -C target-cpu=native</code> - List the configuration if you were to specifically target for your CPU. Use this to see the delta between the defaults and the featurse supported for a specific CPU.</li>\n<li><code>rustc --print target-cpus</code> - List all known target CPUs. This also tells you what your current CPU and what the default CPU is for your current toolchain.</li>\n<li><code>/lib64/ld-linux-x86-64.so.2 --help</code> - Specifically for x86_64 Linux users, will show you what microarchitecture levels your CPU supports.</li>\n<li><code>rustc --print target-features</code> - List <em>all available</em> target features with a short description. You can scope to a specific CPU with <code>-C target-cpu=</code>. Useful mostly to see what you're missing, I guess.</li>\n</ul>\n",
      "summary": "",
      "date_published": "2025-07-28T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "rust",
        "devops"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//ownership-benefits-beyond-memory-safety.html",
      "url": "https://ianwwagner.com//ownership-benefits-beyond-memory-safety.html",
      "title": "Ownership Benefits Beyond Memory Safety",
      "content_html": "<p>Rust's ownership system is well-known for the ways it enforces memory safety guaranteees.\nFor example, you can't use some value after it's been freed.\nFurther, it also ensures that mutability is explicit,\nand it enforces some extra rules that make <em>most</em> data races impossible.\nBut the ownership system has benefits beyond this which don't get as much press.</p>\n<p>Let's look at a fairly common design pattern: the builder.\nA builder typically takes zero or few arguments to create.\nIn Rust, it's often implemented as a <code>struct</code> that implements the <code>Default</code> trait.\nThen, you progressively &quot;chain&quot; method invocations to ergonomically\nto specify how to build the thing you want.\nFor example:</p>\n<pre><code class=\"language-rust\">let client = reqwest::Client::builder()\n    .user_agent(APP_USER_AGENT)\n    .timeout(Duration::from_secs(3))\n    .build()?;\n</code></pre>\n<p>This pattern is useful because it avoids bloated constructors with dozens of arguments.\nIt also lets you encode <strong>failability</strong> into the process:\nsome combinations of arguments may be invalid.</p>\n<p>If you look at the signature for the <code>timeout</code> function,\nyou'll find it takes <code>self</code> as its first parameter and returns a value of the same type.\nThe key thing to note is that a non-reference <code>self</code> parameter\nwill &quot;consume&quot; the receiver!\nSince it takes ownership, you can't hold on to a reference to the original value!</p>\n<p>This prevents a whole class of subtle bugs.\nPython, for example, doesn't prevent you from modifying inputs,\nand it's not always clear if a function/method is supposed to return a new value,\nwhether that value has the same contents as the original reference (which is still valid!)\nor if it's completely fresh,\nand so on.</p>\n<p>A few other languages, mostly in the purely functional tradition (Haskell comes to mind)\nalso have a similar property.\nThey don't use a concept of &quot;ownership&quot; but rather remove mutability from the language.\nRust makes what I consider to be a nice compromise\nwhich retains most of the benefits while being easier to use.</p>\n<p>In summary, the borrow checker is a powerful ally,\nand you can leverage it to make truly better APIs,\nsaving hours of debugging in the future.</p>\n",
      "summary": "",
      "date_published": "2025-05-31T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "rust",
        "functional programming"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//unicode-normalization.html",
      "url": "https://ianwwagner.com//unicode-normalization.html",
      "title": "Unicode Normalization",
      "content_html": "<p>Today I ran into an <a href=\"https://www.openstreetmap.org/node/9317391311/history/2\">amusingly named place</a>,\nthanks to some sharp eyes on the OpenStreetMap US Slack.\nThe name of this restaurant is listed as &quot;𝐊𝐄𝐁𝐀𝐁 𝐊𝐈𝐍𝐆 𝐘𝐀𝐍𝐆𝐎𝐍&quot;.\nThat isn't some font trickery; it's a bunch of Unicode math symbols\ncleverly used to emphasize the name.\n(Amusingly, this does not actually show up properly on most maps, but that's another story for another post).</p>\n<p>I was immediately curious how well the geocoder I spent the last few months building handles this.</p>\n<p><figure><img src=\"media/kebab-king-duplicates.png\" alt=\"A screenshot of a search result list showing two copies of the Kebab King Yangon, one in plain ASCII and the other using the math symbols\" /></figure></p>\n<p>Well, at least it found the place, despite the very SEO-unfriendly name!\nBut what's up with the second search result?</p>\n<p>Well, that's a consequence of us pulling in data from multiple sources.\nIn this case, the second result comes from the <a href=\"https://opensource.foursquare.com/os-places/\">Foursquare OS Places</a> dataset.\nIt seems that either the Placemaker validators decided to clean this up,\nor the Foursquare user who added the place didn't have that key on their phone keyboard.</p>\n<p>One of the things our geocoder needs to do when combining results is deduplicating results.\n(Beyond that, it needs to decide which results to keep, but that's a much longer post!)\nWe use a bunch of factors to make that decision, but one of them is roughly\n&quot;does this place have the same name,&quot; where <em>same</em> is a bit fuzzy.</p>\n<p>One of the ways we can do this is normalizing away things like punctuation and diacritics.\nThese are quite frequently inconsistent across datasets, so two nearby results with similar enough names\nare <em>probably</em> the same place.\nFortunately, Unicode provides a few standardized transformations into canonical forms\nthat make this easier.</p>\n<h1><a href=\"#composed-and-decomposed-characters\" aria-hidden=\"true\" class=\"anchor\" id=\"composed-and-decomposed-characters\"></a>Composed and decomposed characters</h1>\n<p>What we think of as a &quot;character&quot; does not necessarily have a single representation in Unicode.\nFor example, there are multiple ways of encoding &quot;서울&quot; which will look the same when rendered,\nbut have a different binary representation.\nThe Korean writing system is perhaps a less familiar case for many,\nbut characters with diacritical marks such as accents are usually the same.\nThey can be either &quot;composed&quot; or &quot;decomposed&quot; into the component parts\nat the binary level.</p>\n<p>This composition and decomposition transform is useful for (at least) two reasons:</p>\n<ol>\n<li>It gives us a consistent form that allows for easy string comparison when multiple valid encodings exist.</li>\n<li>It lets us strip away parts that we don't want to consider in a comparison, like diacritics.</li>\n</ol>\n<p>I use the <a href=\"https://docs.rs/unicode-normalization/latest/unicode_normalization/\"><code>unicode_normalization</code></a> crate\nto do this &quot;decompose and filter&quot; operation.\nSpecifically, the <a href=\"https://docs.rs/unicode-normalization/latest/unicode_normalization/trait.UnicodeNormalization.html\"><code>UnicodeNormalization</code> trait</a>,\nwhich has helpers which will work on most string-like types.</p>\n<h1><a href=\"#normalization-forms\" aria-hidden=\"true\" class=\"anchor\" id=\"normalization-forms\"></a>Normalization forms</h1>\n<p>You might notice there are four confusingly named methods in the trait:\n<code>nfd</code>, <code>nfkd</code>, <code>nfc</code>, and <code>nfkc</code>.\nThe <code>nf</code> stands for &quot;normalization form&quot;.\nThese functions <em>normalize</em> your strings.\n<code>c</code> and <code>d</code> stand for composition and decomposition.\nThe composed form is, roughly, the more compact form,\nwhereas the decomposed form is the version where you separate the base from the modifiers,\nthe <a href=\"https://en.wikipedia.org/wiki/List_of_Hangul_jamo\">jamo</a> from the syllables, etc.</p>\n<p>We were already decomposing strings so that we could remove the diacritics, using form NFD.\nThis works great for diacritics and even Hangul,\nbut 𝐊𝐄𝐁𝐀𝐁 𝐊𝐈𝐍𝐆 𝐘𝐀𝐍𝐆𝐎𝐍 shows that we were missing something.</p>\n<p>That something is the <code>k</code>, which stands for &quot;compatibility.&quot;\nYou can refer to <a href=\"https://www.unicode.org/reports/tr15/#Canon_Compat_Equivalence\">Unicode Standard Annex #15</a>\nfor a full definition,\nbut the intuition is that <em>compatibility</em> equivalence of two characters\nis a bit more permissive than the stricter <em>canonical</em> equivalence.\nBy reducing two characters (or strings) to their canonical form,\nyou will be able to tell if they represent the same &quot;thing&quot; with the same visual appearance,\nbehavior, semantic meaning, etc.\nCompatibility equivalence is a weaker form.</p>\n<p>Compatibility equivalence is extremely useful in our quest for determining whether two nearby place names\nare a fuzzy match.\nIt reduces things like ligatures, superscripts, and width variations into a standard form.\nIn the case of &quot;𝐊𝐄𝐁𝐀𝐁 𝐊𝐈𝐍𝐆 𝐘𝐀𝐍𝐆𝐎𝐍,&quot; compatibility decomposition transforms it into the ASCII\n&quot;KEBAB KING YANGON.&quot;\nAnd now we can correctly coalesce the available information into a single search result.</p>\n<p>Hopefully this shines a light on one small corner of the complexities of unicode!</p>\n",
      "summary": "",
      "date_published": "2025-05-09T00:00:00-00:00",
      "image": "media/kebab-king-duplicates.png",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "unicode",
        "rust"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//edns-client-subnet-and-geographic-dns.html",
      "url": "https://ianwwagner.com//edns-client-subnet-and-geographic-dns.html",
      "title": "EDNS Client-Subnet and Geographic DNS",
      "content_html": "<p>DNS is a complex beast,\nso it's little surprise when I learn something new.\nToday I learned a bit more about the internals of how DNS works\nat various privacy-centric providers.</p>\n<p>It all started a few weeks ago when someone I follow on Mastodon\n(I'm very sorry I can't remember who)\nmentioned <a href=\"https://quad9.net\">Quad9</a> as a privacy-friendly DNS solution.\nI think the context was that they were looking for alternatives\nto the US &quot;big tech&quot; vendors, like Cloudflare and Google.\nI eventually remembered it and switched by DNS a few weeks ago from 1.1.1.1,\nCloudflare's similar service.</p>\n<p>Fast forward to the present.\nI was frustrated that some page loads were REALLY slow.\nI couldn't figure out a clear pattern, but two sites which I visit a LOT\nare <a href=\"https://stadiamaps.com/\">stadiamaps.com</a> and <a href=\"https://docs.stadiamaps.com/\">docs.stadiamaps.com</a>,\nsince it's kinda my job ;)\nThis had been going on far at least a week or two,\nbut I thought it was just something funky with my ISP,\nor maybe they were throttling me (I'm sure I'm a top 1% bandwidth user).</p>\n<p>I had enough of the slowness this morning and was about to type up a thread in our internal Zulip chat\nasking our network guy to look into it on Monday.\nSo like any decent engineer, I popped up a web inspector and captured a HAR file\nso they could &quot;see what I saw.&quot;</p>\n<p>And what did I see?\nAfter a few minutes looking over it for obvious problems,\nI noticed that our marketing site was loading from our edge server in...\nJohannesburg?!\nAnd our docs site was coming from a server in Texas!\n(We include some HTTP headers which assist in debugging this sort of thing.)</p>\n<p>Well, that's not right...\nI popped up <code>dig</code> in my terminal and verified that, indeed, the A records were resolving to servers\nlocated on the other side of the world.\nAnd then it hit me.\nI had changed my DNS settings recently!\nThat must have something to do with it!</p>\n<p>We use AWS Route53 for our geographically aware DNS resolution needs.\nIt's the best product I've seen in the industry,\nso I assumed it wasn't their fault.\nThen I remembered something I read in the <a href=\"https://quad9.net/support/faq/#edns\">Quad9 FAQ</a>\nabout EDNS Client-Subnet (also known as EDNS0 and ECS).\nThat seems relevant...</p>\n<p>The quick version is that some DNS resolvers can use a truncated version of your IP address\nto improve the quality of the results (giving a server near you).\nAmazon has a great <a href=\"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-edns0.html\">detailed writeup</a>.</p>\n<p>The trouble is that this info could theoretically be used (with some more details) to identify you,\nso many privacy-focused DNS solutions (including Quad9) disable this by default</p>\n<p>Quad9 operates an alternate server with EDNS Client-Subnet enabled.\nI tried that and it gave the results I expected.\nBut this is not where the story ends!</p>\n<p>It turns out, Cloudflare, which I had been using previously,\nalso gave the &quot;expected&quot; results.\nBut they state very clearly in their <a href=\"https://developers.cloudflare.com/1.1.1.1/faq/#does-1111-send-edns-client-subnet-header\">FAQ</a>\nthat they do not use EDNS Client-Subnet.\nWhat gives?</p>\n<p>At this point I'm speculating,\nbut I think that their network setup is a bit different.\nCloudflare is famous for having an extensive edge network,\nand I have a server very close by.\nMy guess is that they make all upstream queries to authoritative servers\nAND cache any results in the same nearby datacenter.\nThis would easily explain why they can still give a geographically relevant result,\nwithout sending your subnet to the authoritative server.</p>\n<p>Quad9 on the other hand either doesn't have as many servers nearby (for fast routing),\nor perhaps they are sharing cache results globally.</p>\n<p>As I said though, this is all just speculation.\nIf anyone has more knowledge of how Cloudflare and Quad9 operate,\nlet me know and I'll update this post!</p>\n",
      "summary": "",
      "date_published": "2025-05-03T00:00:00-00:00",
      "image": "",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "dns",
        "networking"
      ],
      "language": "en"
    }
  ]
}