﻿<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text" xml:lang="en">Development</title>
  <link type="application/atom+xml" href="https://d.moonfire.us/categories/development/atom.xml" rel="self" />
  <link type="text/html" href="https://d.moonfire.us/categories/development/" rel="alternate" />
  <updated>2026-10-14T17:35:38Z</updated>
  <id>https://d.moonfire.us/categories/development/</id>
  <author>
    <name>D. Moonfire</name>
  </author>
  <rights>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</rights>
  <entry>
    <title>Leicmin</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2026/04/04/leicmin/" />
    <updated>2026-04-04T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2026/04/04/leicmin/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="leicmin" scheme="https://d.moonfire.us/tags/" label="Leicmin" />
    <category term="covid" scheme="https://d.moonfire.us/tags/" label="Covid" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="age-verification" scheme="https://d.moonfire.us/tags/" label="Age Verification" />
    <category term="300-weeks" scheme="https://d.moonfire.us/tags/" label="300 Weeks" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-writing" scheme="https://d.moonfire.us/tags/" label="MfGames Writing" />
    <category term="patreon" scheme="https://d.moonfire.us/tags/" label="Patreon" />
    <category term="subscribe-star" scheme="https://d.moonfire.us/tags/" label="Subscribe Star" />
    <category term="allegro" scheme="https://d.moonfire.us/tags/" label="Allegro" />
    <category term="large-language-model" scheme="https://d.moonfire.us/tags/" label="Large Language Model" />
    <summary type="html">I spent six months working on a flight or fight response and created a self-hosted web application to calm it down.
</summary>
    <content type="html">&lt;p&gt;I haven't posted in three months. That isn't to nothing has been happening, only that I was so focused on the &amp;ldquo;now&amp;rdquo; that I didn't really have a chance for any retrospection (which is effectively what a lot of my posts are) or idle thoughts (which cover most of the rest). Part of that is how my thoughts work, I withdraw from everything when I'm struggling with problems.&lt;/p&gt;
&lt;p&gt;There is always the usual family crises (we lost a family member last week and there is a good chance we're going to lose a close one &amp;ldquo;soon&amp;rdquo;), work pressure, and the difficulties of being a parent.&lt;/p&gt;
&lt;h2&gt;Age Verification&lt;/h2&gt;
&lt;p&gt;On top of that, the various news about &lt;a href="https://action.freespeechcoalition.com/age-verification-bills/"&gt;age verification laws&lt;/a&gt;, the casual tossing around of felonies and crippling monetary fines, and the rise of privacy-busting legislation were hitting my communities. Too many legislation were salivating at the openings that the Texas got with the Supreme Court ruling, the idea of forcing all cases to be dragged into the state with the most restrictions, and generally finding Machiavellian ways to force creators from closing up shop without explicitly saying that porn was illegal (because it isn't).&lt;/p&gt;
&lt;p&gt;I know &lt;a href="https://fedran.com"&gt;Fedran&lt;/a&gt; is &amp;ldquo;mostly&amp;rdquo; excluded from most of those age verification laws. There are a million words on the site and less than 10% of them are what I would consider &amp;ldquo;adult&amp;rdquo; or graphic. I could easily say it isn't pornographic, but then Tennessee's &lt;a href="https://wapp.capitol.tn.gov/apps/BillInfo/Default?BillNumber=HB1614&amp;amp;GA=113"&gt;Protect Tennessee Minors Act&lt;/a&gt; got put into effect and it appears to &lt;a href="https://ondato.com/blog/tennessee-age-verification/"&gt;apply to my fiction&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It was that law that triggered a &amp;ldquo;fight or flight&amp;rdquo; response. I seriously considered just closing down the shop and giving up writing. I was already in the middle of the burnout from my &lt;a href="/blog/2021/03/17/three-hundred-weeks/"&gt;300 consecutive week&lt;/a&gt; challenge and my struggles after Covid. It felt like an easy escape, just cut off that part of my life.&lt;/p&gt;
&lt;p&gt;But then I didn't. I'm terrible at giving up, I mean that was pretty much the major thread throughout &lt;a href="/tags/sand-and-blood/"&gt;Sand and Blood&lt;/a&gt;, &lt;a href="/tags/sand-and-ash/"&gt;Sand and Ash&lt;/a&gt;, and &lt;a href="/tags/sand-and-bone/"&gt;Sand and Bone&lt;/a&gt;. That part is part of me. I might take a while to get there, but I don't give up easily. I also saw other creators who were also struggling with the same problems, but they were clearly in the arenas the laws were targeting. Creators who were writing things that were more erotic (it's never about violence, which I universally consider more disgusting than sexuality).&lt;/p&gt;
&lt;p&gt;So the flight (giving up) turned into fight (doing something). I already knew that I wasn't going to be great about anything I wrote, but it was something I could do. I have to be honest, I didn't come out of Covid unscathed and I'm painfully aware of how much I struggle with things that I found pleasurable less than ten years ago.&lt;/p&gt;
&lt;p&gt;Also, quite a few years before all this, I had an idea of creating a self-hosted site that could coordinate various subscription/patron services. Basically, taking the supporter badges from forums, the ability to have &lt;a href="/tags/patreon/"&gt;Patreon&lt;/a&gt; or &lt;span class="missing-link" data-path="/tags/ko-fi/"&gt;Ko-Fi&lt;/span&gt; supporters (or for a while, cryptocurrency), and be able to control access to sites. Ideally, in a manner that would not require exposing personal information (because no one needs to know those details). And no gamification or social interactions, just something between a creator and someone who likes their work.&lt;/p&gt;
&lt;p&gt;That also would give me the ability to integrate age verification systems or even geo-blocking or VPN-blocking (there were a few bills that required periodic verification of age or the need to ban all VPN usage) in a central place.&lt;/p&gt;
&lt;p&gt;Would it be helpful? Maybe. But if anything, I could implement something to protect &lt;em&gt;my&lt;/em&gt; ass and maybe it would help others.&lt;/p&gt;
&lt;h2&gt;Leicmin&lt;/h2&gt;
&lt;p&gt;And that ended up being &lt;a href="/tags/leicmin/"&gt;Leicmin&lt;/a&gt; (more detail on the &lt;a href="https://mfgames.com/leicmin/"&gt;development site&lt;/a&gt; and the &lt;a href="https://mfgames.com/leicmin/"&gt;forge&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Well, multiple iterations of Leicmin. I started with trying to implement a C# version since I thought I was really good at the language and it would be &amp;ldquo;easy&amp;rdquo; to bang up something.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I didn't do it because it was easy. I did it because I thought it was easy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After a few months, I realized that I had too many things rattling in my head, and the ideas that I toyed with for years thinking they would make a great system (event sourcing, automated auditing, API-first) ended up crumbling once I started to do significant things. I spent more time fighting libraries and tools than I was moving forward.&lt;/p&gt;
&lt;p&gt;Somewhere in December, I realized it wasn't going to work out so I decided to burn it down and start from scratch. This time, I jettisoned most of the fancy ideas and went with something simple: a website that required no Javascript, had no API, and was about as plain and simple as I could make it. I also decided to go with Rust, but avoided the fancier front ends and went with straight, old-school templating.&lt;/p&gt;
&lt;p&gt;There were some things that I wasn't going to get rid of: there are &lt;a href="https://github.com/jetify-com/typeid"&gt;type-safe identifiers&lt;/a&gt; everywhere, almost everything is CLI first, it is based on Postgres, and there are almost no &lt;a href="https://xkcd.com/974/"&gt;general solutions&lt;/a&gt; anywhere.&lt;/p&gt;
&lt;p&gt;And I got progress. I could see things moving forward and I didn't feel like I was writing myself into a corner. True, the C# attempt taught me a lot, but it was really nice seeing slow but steady progress to something that works. I felt bad that it wasn't &amp;ldquo;fancy&amp;rdquo; or more capable but I was happy that I was creating something I thought could benefits others.&lt;/p&gt;
&lt;p&gt;My goal was the end of February, but it ended up being the end of March, that I got something with a very simple functionality.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Folks could log in and change passwords&lt;/li&gt;
&lt;li&gt;They could connect Patreon and Subscribe Star (a feature requested online)&lt;/li&gt;
&lt;li&gt;They could set up a password that could be used basic authentication&lt;/li&gt;
&lt;li&gt;The creator could export a &lt;code&gt;.htpasswd&lt;/code&gt; file, hook it up to a static site, and be able to authenticate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Through that, I could get a &amp;ldquo;basic&amp;rdquo; age verification since Patreon and Subscribe Star both do age verification on their sides. That means I could mark my patreon as &amp;ldquo;mature&amp;rdquo; (I haven't done that yet), wire up my site in &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; to use it, and have what I need to quell that &amp;ldquo;fight&amp;rdquo; response in the back of my head.&lt;/p&gt;
&lt;p&gt;And someone (the person who asked for Subscribe Star) is already using it. And reporting bugs and defects as they onboard their subscribers onto it. Which is exciting, because I rarely see someone actually using my tools (Nitride and &lt;a href="/tags/mfgames-writing/"&gt;MfGames Writing&lt;/a&gt; mostly).&lt;/p&gt;
&lt;h2&gt;Going Slow&lt;/h2&gt;
&lt;p&gt;Some of the painful parts is that progress was slow. I've been getting a lot of pressure from work to embrace &lt;a href="/tags/large-language-model/"&gt;large language models&lt;/a&gt; but I didn't want to do that with Leicmin. I want to lay down the code, to come up with the ideas, and work through them because I cherish those little insights when I find some pattern I never thought about before.&lt;/p&gt;
&lt;h2&gt;Protecting Minors&lt;/h2&gt;
&lt;p&gt;I'm not against the idea of limiting minors from accessing content. What I don't like is how law makers was going about it, since little of what they are producing is about protecting children, but very clearly attacking something they consider immoral.&lt;/p&gt;
&lt;p&gt;(I also think it should be something parents should decide on, not law makers making decisions for everyone.)&lt;/p&gt;
&lt;p&gt;Even listening to them talk about it, it was just another attack on people they don't like, they just use minors as their shield to defend their own biases.&lt;/p&gt;
&lt;p&gt;That also means as I have the bandwidth, I'll find alternatives to handing over personal information to for-profit companies. Or find a way not to need a subscription service. I read about and bookmarked a couple promising alternatives, but they aren't quite there yet.&lt;/p&gt;
&lt;h2&gt;Going Forward&lt;/h2&gt;
&lt;p&gt;There are a ton of things that still need to be done before I consider it &amp;ldquo;done&amp;rdquo; in terms of user experience and usability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Light theme was the first thing I was asked about&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Better authentication options&lt;/li&gt;
&lt;li&gt;Emails&lt;/li&gt;
&lt;li&gt;More age verification options&lt;/li&gt;
&lt;li&gt;More geo blocking options&lt;/li&gt;
&lt;li&gt;Auditing compliance (okay, that one is me)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://src.mfgames.com/leicmin/leicmin/issues"&gt;Many more&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On my personal side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up an instance of my own&lt;/li&gt;
&lt;li&gt;Go through my Fedran stories and mark the &amp;ldquo;adult&amp;rdquo; ones&lt;/li&gt;
&lt;li&gt;Set up the instance to put a password on the adult stories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And, keep slowly moving forward. It was almost immediately after I was able to lean back and say &amp;ldquo;I got the MVP&amp;rdquo; that I was able to relax. Even not setting it up myself (which I will do), the knowledge that I had a clear path forward to handle that fight or flight was a major relief.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just keep swimming.&lt;/p&gt;
&lt;p&gt;&amp;mdash;Dory, &lt;em&gt;Finding Nemo&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Allegro&lt;/h2&gt;
&lt;p&gt;Speaking of writing, I'm slowly submitting &lt;a href="/tags/allegro/"&gt;Allegro&lt;/a&gt; through the writing group. Of the alpha readers, about half of them came back with some good feedback and half of them never finished. It probably means that it will end up as well as &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt;, but I'm still going to try publishing the best book I can.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Other Smarter People</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2026/01/04/other-smarter-people/" />
    <updated>2026-01-04T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2026/01/04/other-smarter-people/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="uuidv7" scheme="https://d.moonfire.us/tags/" label="UUIDv7" />
    <category term="identifiers" scheme="https://d.moonfire.us/tags/" label="Identifiers" />
    <category term="typeid" scheme="https://d.moonfire.us/tags/" label="TypeID" />
    <category term="purl" scheme="https://d.moonfire.us/tags/" label="PURL" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.
</summary>
    <content type="html">&lt;p&gt;I don't like reinventing the wheel. At the same time, I'll look for something and not find it, go through the effort to create something of my own, only to find out that my search skills weren't high enough and someone has already figured that out.&lt;/p&gt;
&lt;p&gt;In a matter of twenty-four hours, I found that two such situations had happened. Which means, I can close the topic on identifiers and simply point to other folk's work and build the little quirk I want on to of those.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/05/30/identifiers/"&gt;2024-05-30 Generating and Using Identifiers&lt;/a&gt; - A little discussion on unique identifiers, including formatting and presentation. A small database lesson on page splitting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/06/29/identifiers-update/"&gt;2024-06-29 Generating and Using Identifiers (Part 2)&lt;/a&gt; - A few expanded ideas about identifiers after discussing it with others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2026/01/04/other-smarter-people/"&gt;2026-01-04 Other Smarter People&lt;/a&gt; - I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Identifiers&lt;/h2&gt;
&lt;p&gt;Let's start with the last six months (it's been that last since the last of this series&amp;hellip; actually, of most of my posts): I actually used my ideas to see if they were viable in practice as they were in my head.&lt;/p&gt;
&lt;p&gt;I found that when I elided the identifiers, which means only showing the last group of the identifier along with the prefix, it didn't give the benefit I thought it would. For example, &lt;code&gt;group__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/code&gt; being elided to &lt;code&gt;group__rv1y5c52&lt;/code&gt;. When seen on screen, the &lt;code&gt;group__&lt;/code&gt; was still the bulk of the screen estate and the content was sufficient to make it unimportant.&lt;/p&gt;
&lt;p&gt;Then while I was working on &lt;a href="/tags/leicmin/"&gt;Leicmin&lt;/a&gt;, I stumbled onto a series of Rust libraries that implemented a standard that was almost completely the same as mine, including using Crockford32 encoding: &lt;a href="https://github.com/jetify-com/typeid"&gt;TypeID&lt;/a&gt; (or my &lt;a href="/tags/typeid/"&gt;local tag&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;TypeID uses the underscore separator, so double-click and selection works. It defaults to UUIDv7 but can also be used with other UUIDs. It only has one between the prefix and identifier block and no grouping. Which means the above example would be &lt;code&gt;group_01hz613s22k8nr6w6grv1y5c52&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I lose the grouping and there no rules for eliding, but those are easy to handle. Just take the left &lt;code&gt;X&lt;/code&gt; characters and not worry about anything else. So, in C#, eliding would simply be &lt;code&gt;identifier.ToString()[^8..]&lt;/code&gt; and getting &lt;code&gt;rv1y5c52&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Good enough.&lt;/p&gt;
&lt;p&gt;Fortunately, there are a lot of implementations of TypeID. None of them work the way I want them too, or they are &lt;em&gt;quite&lt;/em&gt; there, but it's a much better start and works with a standard that someone else has already established.&lt;/p&gt;
&lt;h2&gt;Package URLs&lt;/h2&gt;
&lt;p&gt;Speaking of finding standard, when I was giving my thoughts on &lt;a href="/tags/bakfu/"&gt;Bakfu&lt;/a&gt;, I was trying to come up with a useful approach for referencing packages across different ecosystems.&lt;/p&gt;
&lt;p&gt;While learning about TypeID, I noticed that the &lt;a href="https://crates.io/crates/mti"&gt;crates.io packages&lt;/a&gt; has a &lt;a href="/tags/purl/"&gt;PURL&lt;/a&gt; link on the sidebar. I had never seen that before, but it basically lead me down to learning about a &lt;a href="https://github.com/package-url/purl-spec"&gt;&amp;ldquo;mostly universal&amp;rdquo;&lt;/a&gt; URL format for packages so&amp;hellip; other people were trying to solve the same thing and they had some interest behind their own (as opposed to none behind mine).&lt;/p&gt;
&lt;p&gt;Some examples (from their website):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pkg:deb/debian/curl@7.50.3-1?arch=i386&amp;amp;distro=jessie&lt;/li&gt;
&lt;li&gt;pkg:docker/cassandra@sha256:244fd47e07d1004f0aed9c&lt;/li&gt;
&lt;li&gt;pkg:gem/jruby-launcher@1.1.2?platform=java&lt;/li&gt;
&lt;li&gt;pkg:golang/google.golang.org/genproto#googleapis/api/annotations&lt;/li&gt;
&lt;li&gt;pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?repository_url=repo.spring.io%2Frelease&amp;amp;packaging=sources&lt;/li&gt;
&lt;li&gt;pkg:npm/%40angular/animation@12.3.1&lt;/li&gt;
&lt;li&gt;pkg:nuget/EnterpriseLibrary.Common@6.0.1304&lt;/li&gt;
&lt;li&gt;pkg:pypi/django@1.11.1&lt;/li&gt;
&lt;li&gt;pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&amp;amp;distro=fedora-25&lt;/li&gt;
&lt;li&gt;pkg:rpm/opensuse/curl@7.56.1-1.1?arch=i386&amp;amp;distro=opensuse-tumbleweed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Well, that covers pretty much the same scope I was trying to implement, so another thing I can move to an accepted standard and stop working on my own.&lt;/p&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;A lot of my personal work is in isolation. I've always struggled to join communities and I have a relatively small &amp;ldquo;footprint&amp;rdquo; when it comes to social networks. And, because of my fragmented nature, I'm rarely in touch of people who are in the &amp;ldquo;know&amp;rdquo; who can tell me that someone else is already working on, or has solved, a problem I'm working on.&lt;/p&gt;
&lt;p&gt;That all said, I'm glad I still went down the rabbit hole of trying to solve it. More so when my solution is only trivially different from the more popular one. It tells me I'm in the right direction.&lt;/p&gt;
&lt;p&gt;I also think as a thought exercise, it gives me more appreciation for the work that goes behind these efforts and frequently makes me a fan or even advocate because I'm emotional behind the reasons.&lt;/p&gt;
&lt;p&gt;So, time not wasted but also relief that someone else figured it out instead.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Git For Authors (v0.1.1)</title>
    <link rel="alternate" href="https://d.moonfire.us/garden/git-for-authors/" />
    <updated>2025-08-08T23:41:54Z</updated>
    <id>https://d.moonfire.us/garden/git-for-authors/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <summary type="html">An evolution of how I use Git for managing writing projects.</summary>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;🛈 This isn't done. Not even close. As you can see from the front page, a lot of the bullet items aren't leading to links, which I full intend to flesh out into more detailed sections. Plus I'm still in the process of dumping information and the organization isn't there. If this was the 90s, I would have a &amp;ldquo;Under Construction&amp;rdquo; GIF.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I enjoy writing as much as I like coding. While I used entirely separate tools to do either, over the years, those two processes have converge to my writing projects look like my coding projects. They use pipelines to generate the final output, source control, automated versioning, and the same suite of tools. In many cases, these tools are designed to automate tedious parts of publishing or coordinate work with others, something that can be difficult with existing tools.&lt;/p&gt;
&lt;p&gt;The inspiration of this is &lt;a href="//d.moonfire.us/tags/fast-trip/"&gt;Fast Trip&lt;/a&gt;, a story by &lt;a href="https://www.sectorgeneral.com/shortstories/fasttrip.html"&gt;James White&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Organization&lt;/h2&gt;
&lt;p&gt;I don't like just saying &amp;ldquo;this is how I do things&amp;rdquo; because this isn't, nor will it ever be, the One True Way™ of writing with Git. Instead, I've attempted to organize the topics from high-level ones, to global concepts, and then into advanced topics.&lt;/p&gt;
&lt;p&gt;There is a lot of cross-linking of topics within the individual pages for relevance, but you can easily choose to ignore those links since if you follow this index page, you'll get to them in &amp;ldquo;discovery&amp;rdquo; order.&lt;/p&gt;
&lt;h2&gt;Meta Topics&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./audience/"&gt;Target Audience&lt;/a&gt; - Who this plot is aimed for and what skills are useful to take advantage of it&lt;/li&gt;
&lt;li&gt;&lt;a href="./conventions/"&gt;Conventions&lt;/a&gt; describes the notations used&lt;/li&gt;
&lt;li&gt;&lt;a href="./model-view-controller/"&gt;Model-View-Controller&lt;/a&gt; is the high-level concepts of separating the content (model) from its appearance (view)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;File Formats&lt;/h2&gt;
&lt;p&gt;While the goal of this plot is to use a program&amp;mdash;Git in this case&amp;mdash;to manage writing, the first part is talking about &amp;ldquo;what&amp;rdquo; is being managed. In this case, it is the words you want to put into a novel or story. There are other things that you might want tracked, such as character notes, research, various pin boards and themes. These are all going to be &amp;ldquo;tracked&amp;rdquo; as part of the project.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./text-files/"&gt;Text Files&lt;/a&gt; is a discussion of why writing should be done in plain text files&lt;/li&gt;
&lt;li&gt;&lt;a href="./top-matter/"&gt;Top Matter&lt;/a&gt; are the details about a given chapter or story&lt;/li&gt;
&lt;li&gt;&lt;a href="./binary-files/"&gt;Binary Files&lt;/a&gt; talks about why Git doesn't play well with binary files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of limitations in text files for doing extra formatting, I use markup to tell when to make something italic, bold, or write an epigraph.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./markup/"&gt;Markup&lt;/a&gt; is how to decorate a text file with some of the formatting rules that writers need&lt;/li&gt;
&lt;li&gt;&lt;a href="./markdown/"&gt;Markdown&lt;/a&gt; is the most popular form of markup&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Organization&lt;/h2&gt;
&lt;p&gt;Organization also becomes important with bigger projects. While you can throw everything into the project directory in a glorious mess, I have established some conventions that work well for me.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./layout/"&gt;Project Layout&lt;/a&gt; is how I organize my files and directories for writing projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Source Control&lt;/h2&gt;
&lt;p&gt;The entire reason for this plot is to use a source control program to manage writing. These programs keep a historical record as you write, allow to review changes, but also avoid accidentally overwriting changes. Programs like Git also allow you to handle feedback from editors and reviewers even when they come in over a period of time and after you've already made changes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./source-control/"&gt;Source Control&lt;/a&gt; discusses the systems in general and my history with them&lt;/li&gt;
&lt;li&gt;&lt;a href="./git/"&gt;Git&lt;/a&gt; is the source control system I mainly use while writing&lt;/li&gt;
&lt;li&gt;&lt;a href="./repositories/"&gt;Repositories&lt;/a&gt; are copies of the writing project&lt;/li&gt;
&lt;li&gt;&lt;a href="./commits/"&gt;Commits&lt;/a&gt; are how changes are saved to a repository (and a branch)&lt;/li&gt;
&lt;li&gt;&lt;a href="./tags/"&gt;Tags&lt;/a&gt; can be used to identify points in time where changes are made&lt;/li&gt;
&lt;li&gt;&lt;a href="./branches/"&gt;Branches&lt;/a&gt; are used to coordinate with readers and editor, or to redo chapters&lt;/li&gt;
&lt;li&gt;&lt;a href="./merging/"&gt;Merging&lt;/a&gt; talks about how to integrate changes from a branch, editor, or normal writing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Text Editors&lt;/h2&gt;
&lt;p&gt;One advantage of using text files is that almost any editor or IDE can be used to write. They have different capabilities or plugins that help.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./visual-studio-code/"&gt;Visual Studio Code&lt;/a&gt; is a lightweight and flexible text editor&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Transforming&lt;/h2&gt;
&lt;p&gt;A critical component of using text formats is how to transform them into other formats that editors and readers need.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./transforming/"&gt;Transforming&lt;/a&gt; talks about transforming source files into the desired output&lt;/li&gt;
&lt;li&gt;&lt;a href="./mfgames-writing/"&gt;MfGames Writing&lt;/a&gt; is a publishing tool for making EPUB and PDF files&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Source Forges&lt;/h2&gt;
&lt;p&gt;Building on top of source control, Git in specific, are code forges which give a web view of your projects, limit others from seeing them, but also gathering feedback and edits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./forges/"&gt;Code Forges&lt;/a&gt; is about online systems for managing Git repositories, pipelines, and issues&lt;/li&gt;
&lt;li&gt;&lt;a href="./pipelines/"&gt;Pipelines&lt;/a&gt; are automated tasks that run&lt;/li&gt;
&lt;li&gt;&lt;a href="./issues/"&gt;Issues&lt;/a&gt; are a task tracking for a project&lt;/li&gt;
&lt;li&gt;&lt;a href="./pull-requests/"&gt;Pull Requests&lt;/a&gt; are for getting changes from others&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each forge is different from the others in almost every way. That includes setting them up, how to run the pipelines, or even how issues are tracked.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Forgejo&lt;/li&gt;
&lt;li&gt;SourceHut&lt;/li&gt;
&lt;li&gt;GitLab&lt;/li&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Versioning&lt;/h2&gt;
&lt;p&gt;One way of dealing with the &amp;ldquo;final23&amp;rdquo; files&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./versioning/"&gt;Versioning&lt;/a&gt; describes while creating a version for a story is important and how to use semantic versioning&lt;/li&gt;
&lt;li&gt;&lt;a href="./conventional-commits/"&gt;Conventional Commits&lt;/a&gt; describes a common formatting for Git commit messages to automate versioning&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Other Tools&lt;/h2&gt;
&lt;p&gt;Beyond the format of the files, how a project is organized is important to keeping everything separate and also to make some of the tools more effective.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./querying/"&gt;Querying&lt;/a&gt; talks about using tools to work with the source files&lt;/li&gt;
&lt;li&gt;&lt;a href="./markdowny/"&gt;Markdowny&lt;/a&gt; is a tool used for querying the top matter&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up the Environment&lt;/h2&gt;
&lt;p&gt;I've been writing novels and stories for many decades. While most of the time, I just write it and then produce the resulting PDF or EPUB, occasionally I want to go back only to find that the libraries have &amp;ldquo;bit rotted&amp;rdquo; or I based on them on a global package I forgot to migrate to my new machine or that I had modified to handle a newer book that broke the old one.&lt;/p&gt;
&lt;p&gt;Inevitability, this means days or even weeks of bringing the project up to date just to get a PDF out of it.&lt;/p&gt;
&lt;p&gt;Along the way, I got frustrated with that and have come up with various tools or frameworks for producing a consistent environment years or even decades later.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./nixos/"&gt;Using NixOS&lt;/a&gt; describes setting up NixOS to make it easier to write&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;About This&lt;/h2&gt;
&lt;p&gt;Finally, this is a living document. Writing it as a digital plot and versioning it will hopefully allow me to evolve as we go.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://src.mfgames.com/dmoonfire-garden/git-for-authors"&gt;MfGames Forge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="./contributing/"&gt;Contributing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;License&lt;/h3&gt;
&lt;p&gt;This book is distributed under a Creative Commons Attribution-ShareAlike 4.0 International license. More info can be found at &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/"&gt;https://creativecommons.org/licenses/by-sa/4.0/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The preferred attribution for this novel is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Git For Authors&amp;rdquo; by D. Moonfire is licensed under CC BY-SA 4.0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the above attribution, use the following links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git For Authors: &lt;a href="https://d.moonfire.us/garden/git-for-authors/"&gt;https://d.moonfire.us/garden/project-layout/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;D. Moonfire: &lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CC BY-SA 4.0: &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/"&gt;https://creativecommons.org/licenses/by-sa/4.0/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Forges&lt;/h3&gt;
&lt;p&gt;The cannon source of this project can be found on the &lt;a href="https://src.mfgames.com/dmoonfire-garden/git-for-authors"&gt;Moonfire Games forge&lt;/a&gt;. However, due to AI scrapers and automated scammers, the site gets occasionally locked down to avoid overwhelming resources. However, this project is also mirrors to the following locations, which are also monitored for new issues or discussions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://git.sr.ht/%7Edmoonfire/git-for-authors"&gt;SourceHut&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codeberg.org/dmoonfire/git-for-authors"&gt;Codeberg&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You are also free to contact &lt;a href="//d.moonfire.us/contact/"&gt;Dylan&lt;/a&gt; directly.&lt;/p&gt;
&lt;h3&gt;Project&lt;/h3&gt;
&lt;p&gt;This project has a unique identifier to make it easier to distingiush from other queries. All blog posts and pages will hopefully include this if it becomes difficult to find.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;01a09c8d-b936-7318-8763-4b8e64cf39ec&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Front Matter</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/12/using-nitride-front-matter/" />
    <updated>2025-06-12T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/12/using-nitride-front-matter/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="yaml" scheme="https://d.moonfire.us/tags/" label="YAML" />
    <category term="opengraph" scheme="https://d.moonfire.us/tags/" label="OpenGraph" />
    <category term="handlebars" scheme="https://d.moonfire.us/tags/" label="Handlebars" />
    <summary type="html">How to use Nitride to take the front matter from pages and add them as a component into the Entity class.
</summary>
    <content type="html">&lt;p&gt;Being able to associated metadata for a page is a very useful thing. It can be used to group pages into categories, control how it is styled, or to simply provide internal notes. To do that, we use something called a YAML front matter to describe the details.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;---
title: Name of the Page
summary: Summary of page
image: /path/to/image.png
---

This is the front page.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a major functionality of the Markdown + YAML pages that I use in my technical, fantasy, and even blog posts. I use the categories and tags heavily on this page, not to mention giving a summary details for links. It can also be used to provide &lt;a href="/tags/opengraph/"&gt;OpenGraph&lt;/a&gt; schemas.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Defining a Model&lt;/h2&gt;
&lt;p&gt;Because C# is a static language, we want to take advantage of a schema so we can have type-safe. Historically, I've called this &lt;code&gt;PageModel&lt;/code&gt; because I wrap that in a &lt;code&gt;TemplateModel&lt;/code&gt; when I get to the styling.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Models/PageModel.cs
namespace Generator.Models;

public class PageModel
{
    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the optional list of categories associated with the page.
    /// &amp;lt;/summary&amp;gt;
    public List&amp;lt;string&amp;gt;? Categories { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the URL of the image associated with the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Image { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the summary for the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Summary { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the title of the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Title { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Naturally, this can and will get a lot more complicated. Because we are using YAML, we can have nested objects, tags, and references that are appropriate for our page. In the above example, we are defining an optional title, summary, an image, and a list of categories.&lt;/p&gt;
&lt;p&gt;It may come to a surprise to you, but this will eventually become a component in the page &lt;code&gt;Entity&lt;/code&gt; class that is passed through the pipelines.&lt;/p&gt;
&lt;h2&gt;Including the YAML module.&lt;/h2&gt;
&lt;p&gt;Like the others &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; modules, we have to add it as a NuGet reference and tell the system to use it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cd src/dotnet
dotnet add package MfGames.Nitride.Yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding it to the system is just a matter of modifying &lt;code&gt;Program.cs&lt;/code&gt; to include it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Program.cs
var builder = new NitrideBuilder(args)
    .UseIO(rootDirectory)
    .UseMarkdown()
    .UseHtml()
    .UseYaml()
    .UseModule&amp;lt;WebsiteModule&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Parsing Front Matter&lt;/h2&gt;
&lt;p&gt;Parsing the front matter uses an operation, &lt;code&gt;MfGames.Nitride.Yaml.ParseYamlHeader&lt;/code&gt; to parse the text content, pull off the front matter, wrap it in the model call, and then replace &lt;code&gt;ITextContent&lt;/code&gt; with the page without the YAML header.&lt;/p&gt;
&lt;p&gt;In effect, this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;---
title: Name of the Page
summary: Summary of page
image: /path/to/image.png
---

This is the front page.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... becomes &amp;ldquo;This is the front page.&amp;rdquo; in the text content with a &lt;code&gt;PageModel&lt;/code&gt; component.&lt;/p&gt;
&lt;p&gt;Adding the operation is relatively simple but this operation uses a generic parameter to identify the model to parse the YAML as.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesModel.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath,
    MoveToIndexPath moveToIndexPath,
    ParseYamlHeader&amp;lt;PageModel&amp;gt; parseYamlHeader)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;
    _moveToIndexPath = moveToIndexPath;
    _parseYamlHeader = parseYamlHeader;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, like the others, it's just a matter of adding the operation into the pipeline.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In //src/dotnet/Pipelines/Inputs/PagesModel.cs
var list = _readFiles
    .Run(cancellationToken)
    .Run(_identifyMarkdownFromPath)
    .Run(_parseYamlHeader)
    .Run(_moveToIndexPath)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we run it, we get the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[01:44:05 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /contact/index.md, Components [&amp;quot;Zio.UPath&amp;quot;,&amp;quot;Generator.Models.PageModel&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;]
[01:44:05 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;Zio.UPath&amp;quot;,&amp;quot;Generator.Models.PageModel&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we look at the HTML output, you'll notice it has a simplified version.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cat build/typewriter/html/index.html 
&amp;lt;h1&amp;gt;Typewriter Press&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retrieving the component is code just requires the &lt;code&gt;Get&amp;lt;PageModel&amp;gt;()&lt;/code&gt; method or to use the various LINQ-based calls from &lt;a href="/tags/mfgames-gallium/"&gt;MfGames.Gallium&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var page = entity.Get&amp;lt;PageModel&amp;gt;();

var list = entities
    .SelectEntity&amp;lt;PageModel&amp;gt;(OnlyRunOnEntitiesWithPageModel)
    .ToList();

public Entity OnlyRunOnEntitiesWithPageModel(Entity entity, PageModel page)
{
    return entity.Set(somePageSpecificVariable);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Gallium Methods&lt;/h2&gt;
&lt;p&gt;In the above case, the &lt;code&gt;SelectEntity&lt;/code&gt; call will skip calling the lambda for entities that don't have a &lt;code&gt;PageModel&lt;/code&gt;, but will still return it into the list. This is the &amp;ldquo;systems&amp;rdquo; part of the ECS system. The following will strip out the entities that don't have the appropriate models.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;.SelectEntity&amp;lt;PageModel&amp;gt;((entity, page) =&amp;gt; entity, includeEntitiesWithoutComponents: false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So far, Gallium is set up to allow up to four components. I haven't had a need to do more, but those are easy to add. This makes it useful when doing some model processing on files that have a future date.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;.WhereEntity&amp;lt;PageModel, UPage, Instant&amp;gt;(FilterOutFuturePages)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;SelectEntity&lt;/code&gt; is rather powerful because it takes an &lt;code&gt;Entity&lt;/code&gt; and returns one. It can be the same entity, or it can be one that has zero or more components added or changed inside it. I found this really useful when I'm parsing out categories or I'm trying to build up a list of some specialized functionality that the styling will need.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public Entity OnSelectEntity(
    Entity entity,
    PageModel page,
    UPath path,
    ITextContent textContent)
{
    return entity
        .Set(nextPreviousModel)
        .SetAll(parentPage, parentUrl)
        .Set(modifiedPageModel);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;We have the minimum number of components and systems needed to setup styling. Next time, I'll use &lt;a href="/tags/handlebars/"&gt;Handlebars&lt;/a&gt; to style the page and put a little chrome around the output.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Markdown</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/" />
    <updated>2025-06-10T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="markdig" scheme="https://d.moonfire.us/tags/" label="MarkDig" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <summary type="html">Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.
</summary>
    <content type="html">&lt;p&gt;Yesterday, I created an over-engineered program to copy a single file from one directory to another. Now, time to make it less overkill by transforming that Markdown file into simple HTML.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MarkDig&lt;/h2&gt;
&lt;p&gt;I don't like reinventing the wheel. I mean, I seem to keep doing it but I don't enjoy it. That was one reason why I tried another static site generators before working on &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;. However, when it comes to redoing a Markdown parser, even I'm not that foolish when there is already &lt;a href="/tags/markdig/"&gt;MarkDig&lt;/a&gt;, an excellent library for turning Markdown into HTML and extendable enough that I could also turn Markdown into &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; (a later post).&lt;/p&gt;
&lt;p&gt;In these cases, we need to tell Nitride how to do anything with Markdown since I didn't make it part of the core library. To do that, we need to pull in the NuGet package. While we're at it, we're also going to add the HTML processing library.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cd src/dotnet
$ dotnet add package MfGames.Nitride.Markdown
$ dotnet add package MfGames.Nitride.Html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the packages installed, we need to add those modules into the system. This is where &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; came in helpful since I just have to add a module for the package it will handle the registration of any operations, components, and systems that we need to use.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Program.cs
var builder = new NitrideBuilder(args)
    .UseIO(rootDirectory)
    .UseMarkdown()
    .UseHtml()
    .UseModule&amp;lt;WebsiteModule&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, I'm trying to follow the generic host pattern for the setup.&lt;/p&gt;
&lt;h2&gt;Identifying Markdown&lt;/h2&gt;
&lt;p&gt;While it may be obvious to convert any entity class that ends in &lt;code&gt;.md&lt;/code&gt; into &lt;code&gt;.html&lt;/code&gt;, we break this apart into separate steps. First, is that we identify a file as a Markdown file. This does two things, it adds the &lt;code&gt;MfGames.Nitride.Markdown.IsMarkdown&lt;/code&gt; as a component, and then treats the contents as text instead of binary.&lt;/p&gt;
&lt;p&gt;If you remember previously, we had this output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[00:41:57 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we're going to use a new operation, &lt;code&gt;MfGames.Nitride.Markdown.IdentifyMarkdownFromPath&lt;/code&gt;. This could be put in a central place, such as &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, but I found it is better to do this earlier than later so I usually put the identify process in the input methods. In this case, &lt;code&gt;PagesPipeline&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This operation doesn't take any parameters because it attempts to &amp;ldquo;do the right thing&amp;rdquo; with a minimal amount of effort. Running the code now produces this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:46:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The big things is that we have a new component, &lt;code&gt;IsMarkdown&lt;/code&gt;, and the &lt;code&gt;IBinaryContent&lt;/code&gt; changed to &lt;code&gt;ITextComponent&lt;/code&gt; which gives us some additional extension methods but also indicates that the file is a text file instead of treating it as a simple binary. It still hasn't loaded the file into memory, it just switched how it is handled.&lt;/p&gt;
&lt;p&gt;This is a separate step is because sometimes I want to keep a specific file as Markdown and not convert it to HTML. Also, there are times when I construct an &lt;code&gt;Entity&lt;/code&gt; directly without having a valid path and I just have to add the &lt;code&gt;IsHtml&lt;/code&gt; and &lt;code&gt;ITextContent&lt;/code&gt; and it then acts like every other file without having to have any special rules.&lt;/p&gt;
&lt;h2&gt;Content&lt;/h2&gt;
&lt;p&gt;Entities with &lt;code&gt;ITextComponent&lt;/code&gt; have a number of useful extension methods associated with them. (Technically, you can use these methods against any &lt;code&gt;Entity&lt;/code&gt; class and it will try to convert a binary to text if there is a &lt;code&gt;IBinaryContent&lt;/code&gt; and we want a &lt;code&gt;ITextContent&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;bool hasText = entity.HasTextContent();
string text = entity.GetTextContent();

entity.SetTextContent(stringValue);
entity.SetTextContent(stringBufferValue);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are also stored as a &lt;code&gt;ITextContent&lt;/code&gt; instead of &lt;code&gt;string&lt;/code&gt; or &lt;code&gt;StringBuffer&lt;/code&gt;. This is because the default interface is to leave the content on the disk and use &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; to retrieve it. However, as soon as &lt;code&gt;SetTextContent&lt;/code&gt; is used, then it keeps that value in memory for the rest of the execution. This is the point where memory pressure begins to increase.&lt;/p&gt;
&lt;p&gt;In the future, we could easily create a &lt;code&gt;ITextContent&lt;/code&gt; implementation that writes large text files to the disk to get them out of memory. However, even my largest chapter of twenty-five thousand words doesn't create too much of a problem so I haven't bothered trying to implement that at this point (but if I did, it would go into &lt;code&gt;//.cache&lt;/code&gt; in some manner).&lt;/p&gt;
&lt;h2&gt;Converting Markdown to HTML&lt;/h2&gt;
&lt;p&gt;Just identifying a file as Markdown doesn't do anything in itself. To convert it, we use another operation, &lt;code&gt;MfGames.Nitride.Markdown.ConvertMarkdownToHtml&lt;/code&gt;. This easily goes into the &lt;code&gt;StyleHtmlPipeline&lt;/code&gt; to handle the conversion and styling. It allows any MarkDig extension or plugin to be called as part of the setup, allow one to customize exactly how the output is generated.&lt;/p&gt;
&lt;p&gt;We also want to change the extension from &lt;code&gt;.md&lt;/code&gt; to &lt;code&gt;.html&lt;/code&gt;. I realize I should have baked that logic into the &lt;code&gt;ConvertMarkdownToHtml&lt;/code&gt; since it is a common operation, which I will do, but for now, we also need to use the &lt;code&gt;MfGames.Nitride.IO.Paths.ChangePathExtension&lt;/code&gt; to do that.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public StyleHtmlPipeline(
    ILogger&amp;lt;StyleHtmlPipeline&amp;gt; logger,
    SimplifiedMarkdownPipeline simplifiedMarkdownPipeline,
    ConvertMarkdownToHtml convertMarkdownToHtml,
    ChangePathExtension changePathExtension)
{
    _logger = logger;
    _changePathExtension = changePathExtension.WithExtension(&amp;quot;.html&amp;quot;);

    _convertMarkdownToHtml = convertMarkdownToHtml
        .WithConfigureMarkdown(builder =&amp;gt;
        {
            SmartyPantOptions smartyPantOptions = new();
            SmartyPantsExtension smartyPants = new(smartyPantOptions);

            builder
                .Use&amp;lt;GenericAttributesExtension&amp;gt;()
                .Use(smartyPants);
        });

    AddDependency(simplifiedMarkdownPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_convertMarkdownToHtml, cancellationToken)
        .Run(_changePathExtension, cancellationToken)
        .ToList();

    _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

    return list.ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this gets us:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
[01:00:43 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
$ find build -type f
build/typewriter/html/index.html
$ cat src/pages/typewriter/index.md 
# Typewriter Press
$ cat build/typewriter/html/index.html
cat build/typewriter/html/index.html
&amp;lt;h1&amp;gt;Typewriter Press&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now our over-engineered copy method has duplicated &lt;code&gt;markdown2html&lt;/code&gt; for a single file.&lt;/p&gt;
&lt;h2&gt;Components&lt;/h2&gt;
&lt;p&gt;You may notice that &lt;code&gt;IsMarkdown&lt;/code&gt; has been removed and &lt;code&gt;IsHtml&lt;/code&gt; was added. This is part of where I struggled with &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt; and lead me down the path of using components. I don't have to pre-define the different data types, purposes, or even formats of a file. Enums are great, but they don't allow easy extension but with an ECS, it's just a matter of adding and removing components based on the use.&lt;/p&gt;
&lt;p&gt;I've used components for a lot of things including identifying pages that should be in the blog archives, special notices, or pages that I want to ignore because they are aliases. I also embed indexes and lists into the pages to allow things like the &amp;ldquo;next&amp;rdquo; or &amp;ldquo;previous&amp;rdquo; links. If I was doing a web comic, I could have a per-character next/previous system easily implemented via those components.&lt;/p&gt;
&lt;p&gt;My other static site generators didn't even have the content type tagging Statiq did, which was a novel concept for me and one that I'm glad I had a chance to use. It simplified a lot of my logic and lead nicely into where I am today.&lt;/p&gt;
&lt;h2&gt;Planning Ahead&lt;/h2&gt;
&lt;p&gt;As I'm planning ahead, I'm going to do the following change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rename &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt; to &lt;code&gt;ContentPipeline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Move the logic I just added into a new pipeline called &lt;code&gt;BareHtmlPipeline&lt;/code&gt; and insert it into between &lt;code&gt;ContentPipeline&lt;/code&gt; and &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason for this is because RSS/Atom feeds use bare HTML to generate their content, so it makes sense to have that bare pipeline feed both of them while having the &lt;code&gt;ContentPipeline&lt;/code&gt; handle a lot of the linking and references that we'll need.&lt;/p&gt;
&lt;h2&gt;Directory Paths&lt;/h2&gt;
&lt;p&gt;One last thing for this post: I prefer paths that end in directory slashes instead of files. So, if create a contact page at &lt;code&gt;//src/pages/content.md&lt;/code&gt;, we want the HTML to be at &lt;code&gt;https://typewriter.press/content/&lt;/code&gt;. This is, creatively enough, another operation: &lt;code&gt;MfGames.Nitride.IO.Paths.MoveToIndexPath&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath,
    MoveToIndexPath moveToIndexPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;
    _moveToIndexPath = moveToIndexPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .Run(_moveToIndexPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we add a &lt;code&gt;contact.md&lt;/code&gt; page which a run gives us this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /contact/index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;ContentPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;BareHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 2 files
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/contact/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
$ cat src/pages/typewriter/contact.md 
# Contact Us

Our emails is [contact@typewriter.press](mailto:contact@typewriter.press).
$ cat build/typewriter/html/contact/index.html 
&amp;lt;h1&amp;gt;Contact Us&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Our emails is &amp;lt;a href=&amp;quot;mailto:contact@typewriter.press&amp;quot;&amp;gt;contact@typewriter.press&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the input is &lt;code&gt;//src/pages/contact.md&lt;/code&gt;, but through the pipeline, it is written out as &lt;code&gt;//build/typewriter/html/contact/index.html&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;Next up, handling front matter. We need that for many reasons, but one of the biggest reason is to provide metadata to generate styled HTML output.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Entities</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/" />
    <updated>2025-06-09T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="gatsbyjs" scheme="https://d.moonfire.us/tags/" label="GatsbyJS" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="serilog" scheme="https://d.moonfire.us/tags/" label="Serilog" />
    <summary type="html">The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.
</summary>
    <content type="html">&lt;p&gt;From yesterday's post, there was an dangling topic, the &lt;code&gt;Entity&lt;/code&gt; class. Unlike most of the static site generators I've worked with, &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; uses an ECS (&lt;a href="https://en.wikipedia.org/wiki/Entity_component_system"&gt;Entity Component System&lt;/a&gt;) to generate its files.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;History&lt;/h2&gt;
&lt;p&gt;I've worked on a number of static site generators over the years, including more than a few adhoc ones until I finally got something stable with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt; which carried me for almost a decade of heavy use. A lot of the patterns that Nitride was based on were on how I found I had to build my pages in Cobblestone, including having a separate &amp;ldquo;gather&amp;rdquo; step which wrote out temporary files and then the &amp;ldquo;process&amp;rdquo; step which formatted them and made everything pretty. I had to do that because I wanted to have cross-linking and references work across multiple Git projects.&lt;/p&gt;
&lt;p&gt;A good example is the &lt;a href="//fedran.com/"&gt;Fedran&lt;/a&gt; website. At the bottom of a &lt;a href="//fedran.com/sand-and-blood/chapter-001/"&gt;chapter&lt;/a&gt;, I have a list of characters and locations in the chapter. The links are built from data from the wiki project but the chapter information comes from the story Git repository. The gather step let me combine both of those together to generate those links.&lt;/p&gt;
&lt;p&gt;That also meant that I needed to have some supplementary information stored in the gather step (which was written to a &lt;code&gt;//build&lt;/code&gt; folder) to help with those linking.&lt;/p&gt;
&lt;p&gt;Near the end, once Cobblestone began to break under its abstraction and complexity, I wanted to get away from writing my own and switched to &lt;a href="/tags/gatsbyjs/"&gt;GatsbyJS&lt;/a&gt; which made a very pretty website, but it was painful to debug and develop. It only lasted a year or so before I wanted to move (though the Gatsby version of Fedran was probably the prettiest I had ever made). If anything, Gatsby ended up being an ecosystem that needed me to pay attention to update things and my cycle of coming around to update a project was too far for it so I spent days trying to get it working again to build it.&lt;/p&gt;
&lt;p&gt;So I tried out &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt;. I never successfully made a website out of it, but it introduced me to some really interesting concepts including pulling in data from other sources (Gatsby also did this, just never really used it). But with the C#, it had some really interesting problems. However, I quickly found that I was trying to do something that was outside of the planned usage, which involved me trying to extend an enumeration which involved basically rewriting large hunks of the system just to handle it. Plus, I really like &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; and I could never get it to play well with that.&lt;/p&gt;
&lt;p&gt;Which lead me into creating Nitride.&lt;/p&gt;
&lt;p&gt;Now, I want to say, both Gatsby and Statiq are both good systems. They just aren't good systems &lt;em&gt;for me&lt;/em&gt;. These don't do what I want them to do and they are designed around patterns that I'm not comfortable with. Even with the desire not to write my own, it wasn't enough.&lt;/p&gt;
&lt;h2&gt;Entity Component Systems&lt;/h2&gt;
&lt;p&gt;Nitride is build on &lt;a href="/tags/mfgames-gallium/"&gt;MfGames.Gallium&lt;/a&gt; which basically means the formal name of the system &lt;a href="https://en.wikipedia.org/wiki/Gallium_nitride"&gt;Gallium Nitride&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gallium nitride (GaN) is a binary III/V direct bandgap semiconductor commonly used in blue light-emitting diodes since the 1990s. The compound is a very hard material that has a Wurtzite crystal structure. Its wide band gap of 3.4 eV affords it special properties for applications in optoelectronics, high-power and high-frequency devices. For example, GaN is the substrate that makes violet (405 nm) laser diodes possible, without requiring nonlinear optical frequency doubling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gallium is a small ECS library. It doesn't intend to be fast or efficient, but it was written to match the idiomatic patterns of &lt;code&gt;System.Linq&lt;/code&gt;. I wanted something that wasn't going to reinvent the wheel, which also meant not making custom collection classes or lookup tables. Instead, I wanted to work with BCL primitives which is why much of the system is based on &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is some high-level documentation for Gallium on &lt;a href="//mfgames.com/mfgames-cil/docs/gallium/"&gt;its project page&lt;/a&gt; and its &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests"&gt;test&lt;/a&gt;, but I'll give the basics here.&lt;/p&gt;
&lt;p&gt;The core object is &lt;code&gt;MfGames.Gallium.Entity&lt;/code&gt;. This is a &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/src/MfGames.Gallium/Entity.cs"&gt;small class&lt;/a&gt; that has an integer identifier and basically a &lt;code&gt;Dictionary&amp;lt;Type, object&amp;gt;&lt;/code&gt; called &lt;code&gt;Components&lt;/code&gt;. It has some methods to make it easier, but basically you can set or get components from it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// This creates an empty entity.
Entity entity1 = new();

// This creates an entity with four components:
//   &amp;quot;System.Int32&amp;quot;: 123
//   &amp;quot;System.String&amp;quot;: &amp;quot;bob&amp;quot;
//   &amp;quot;System.IO.FileInfo&amp;quot;: ...
//   &amp;quot;System.IO.DirectoryInfo&amp;quot;: ...
Entity entity2 = new()
    .Set(123)
    .Set(&amp;quot;bob&amp;quot;)
    .SetAll(
        new FileInfo(&amp;quot;/tmp/bob.txt&amp;quot;),
        new DirectoryInfo(&amp;quot;/tmp&amp;quot;)
    );

Assert.True(entity2.HasComponent&amp;lt;FileInfo&amp;gt;);
Assert.False(entity2.HasComponent&amp;lt;decimal&amp;gt;);

Assert.True(123, entity2.Get&amp;lt;int&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In effect, the class of the component is the key. There are a number of other functions, the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EntityTests.cs"&gt;test class&lt;/a&gt; is probably the best documentation at this point.&lt;/p&gt;
&lt;p&gt;The systems part of the system is the LINQ-inspired collection classes. Most of them work off &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; with the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EnumerableEntityTests.cs"&gt;test class&lt;/a&gt; being the bulk of the documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Set up a list of entities.
List&amp;lt;Entity&amp;gt; list = ...;

// This gets all the entities that have a FileInfo.
var fileOnlyList = list.WhereEntityHas&amp;lt;FileInfo&amp;gt;().ToList();

// This gets all entities that have both a FileInfo and an int.
var fileAndIntList = list.WhereEntityHasAll&amp;lt;FileInfo, int&amp;gt;().ToList();

// Get a modified list based on components. This looks for all
// entities that have a FileInfo and then adds a marker component,
// IsHtml, into it.
IsHtml isHtml = IsHtml.Instance;

var modifiedList = list
    .SelectEntity&amp;lt;FileInfo&amp;gt;((entity, file) =&amp;gt; entity.Set(isHtml))
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a note, &lt;code&gt;Entity&lt;/code&gt; is &amp;ldquo;mostly&amp;rdquo; immutable. The &lt;code&gt;Set&lt;/code&gt; command will return a new &lt;code&gt;Entity&lt;/code&gt; instance with the same ID but with a different set of components. The &amp;ldquo;mostly&amp;rdquo; bit comes that the objects that it points to can be mutated.&lt;/p&gt;
&lt;p&gt;🛈 The lack of formal documentation for Gallium and Nitride is one of the reasons this is still alpha or beta level code.&lt;/p&gt;
&lt;h2&gt;Operations&lt;/h2&gt;
&lt;p&gt;The next concept needed for today's post is an operation which extends &lt;code&gt;MfGames.Nitride.IOperation&lt;/code&gt;. These are just easy ways of creating some functionality that works on an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; that can be combined together.&lt;/p&gt;
&lt;p&gt;Naturally, there are some extension methods that make it easier to work with operations with an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;IEnumerable&amp;lt;Entity&amp;gt; entityList = ...;

IEnumerable&amp;lt;Entity&amp;gt; otherList = someOperation1.Run(entityList);

IEnumerable&amp;lt;Entity&amp;gt; resultList = otherList
    .Run(someOperation2)
    .Run(someOperation3)
    .Run(someOperation4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it relatively easy to create easy processing through composition of one or more operations. Alternatively, we could easily just work directly off the enumerable.&lt;/p&gt;
&lt;h2&gt;Zio&lt;/h2&gt;
&lt;p&gt;There is no question, I'm fond of the &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; library. In this case, Nitride uses two big features with it: to create an abstract view of the project that is independent of the working directory and the &lt;code&gt;UPath&lt;/code&gt; class which normalizes paths.&lt;/p&gt;
&lt;p&gt;If you remember from the previous posts, we set a root directory in the &lt;code&gt;Program.cs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static async Task&amp;lt;int&amp;gt; Main(string[] args)
{
    var rootDirectory = new DirectoryInfo(Environment.CurrentDirectory);

    var builder = new NitrideBuilder(args)
        .UseIO(rootDirectory)
        .UseModule&amp;lt;WebsiteModule&amp;gt;();

    return await builder.RunAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This caused an &lt;code&gt;Zio.IFileSystem&lt;/code&gt; to be injected in the system which treats that directory as the &lt;code&gt;/&lt;/code&gt;. So, in our case where we had our index file in &lt;code&gt;//src/pages/index.md&lt;/code&gt;, it has a &lt;code&gt;UPath&lt;/code&gt; of &lt;code&gt;/src/pages/index.md&lt;/code&gt;. It doesn't seem like a lot, but it means that all paths in the system are always relative to the root of the project and there is no need to pass a root directory throughout the system which means path operations are considerably simplified.&lt;/p&gt;
&lt;p&gt;And the &lt;code&gt;UPath&lt;/code&gt; ensures a consistent pattern for accessing so this works on Windows, Mac, and Linux without a problem. Not to mention, any issues with &lt;code&gt;..&lt;/code&gt; are handled automatically.&lt;/p&gt;
&lt;p&gt;It also simplifies writing tests.&lt;/p&gt;
&lt;h2&gt;Copy File in Six Files&lt;/h2&gt;
&lt;p&gt;Put all that together, we can use two operations to create a massively over-engineered copy file operation using Nitride. Admittedly, the complexity will make sense later, but at the moment, we're going to read the file in the &lt;code&gt;PagesPipeline&lt;/code&gt;, through &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, through &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;, and finally write it out in &lt;code&gt;OutputHtmlPipeline&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Making Some Noise&lt;/h3&gt;
&lt;p&gt;To start with, let's put some logging to show how everything flows from pipeline to pipeline. To do that, we're going to inject an &lt;code&gt;ILogger&amp;lt;&amp;gt;&lt;/code&gt; into the constructor into all four pipelines and then put a logging message into the last three (the &lt;code&gt;PagesPipeline&lt;/code&gt; is going to do something different).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Content/SimplifiedMarkdownPipeline.cs
public class SimplifiedMarkdownPipeline : PipelineBase
{
    private readonly ILogger _logger;

    public SimplifiedMarkdownPipeline(
        ILogger&amp;lt;SimplifiedMarkdownPipeline&amp;gt; logger,
        PagesPipeline pagesPipeline)
    {
        _logger = logger;

        AddDependency(pagesPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = entities.ToList();

        _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, Nitride uses &lt;a href="/tags/serilog/"&gt;Serilog&lt;/a&gt;, so using &lt;code&gt;{Count:N0}&lt;/code&gt; does the right thing even thought we are using a logging abstraction for the actual call. When we run this, we get the following output (I'm not going to show the &lt;code&gt;just build&lt;/code&gt; call for these today):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:00:46 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:00:46 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:00:46 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.1079413
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.1097142
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🛈 I moved &lt;code&gt;NuGet.config&lt;/code&gt; to the root level because there is where the &lt;code&gt;Website.sln&lt;/code&gt; file was located and it felt right.&lt;/p&gt;
&lt;h3&gt;Reading Files&lt;/h3&gt;
&lt;p&gt;Now, to read a file, we are going to use the &lt;code&gt;MfGames.Nitride.IO.ReadFiles&lt;/code&gt; operation which reads file from the Zio file system and passes them through. We've already included that NuGet package in a previous step, so nothing to do there.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.IO.Contents;
using MfGames.Nitride.Pipelines;
using Microsoft.Extensions.Logging;

namespace Generator.Pipelines.Inputs;

public class PagesPipeline : PipelineBase
{
    private readonly ILogger _logger;
    private readonly ReadFiles _readFiles;

    public PagesPipeline(
        ILogger&amp;lt;PagesPipeline&amp;gt; logger,
        ReadFiles readFiles)
    {
        _logger = logger;
        _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = _readFiles
            .Run(cancellationToken)
            .ToList();

        _logger.LogInformation(
            &amp;quot;Read in {Count:N0} files from /src/pages&amp;quot;,
            list.Count);

        foreach (var entity in list)
        {
            _logger.LogInformation(&amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
                entity.Get&amp;lt;UPath&amp;gt;(),
                entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
        }

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires a bit of explanation. We are using dependency injection to insert the &lt;code&gt;ReadFiles&lt;/code&gt; operation into the constructor. This is not a singleton, so you can have multiple &lt;code&gt;ReadFiles&lt;/code&gt; if you need to read from multiple locations (or use a globbing operator to do it in one).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;_readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the constructor, I configure it. I could do this in the &lt;code&gt;RunAsync&lt;/code&gt; function but I found it made it easier to parse the files when I configure in the constructor. Like most things in Nitride, this uses a fluent coding style where each object sets the properties and returns itself to make a nice chained list of calls.&lt;/p&gt;
&lt;p&gt;There are two properties being set, &lt;code&gt;Pattern&lt;/code&gt; and &lt;code&gt;RemovePathPrefix&lt;/code&gt;. I use a source generator to automatically create the &lt;code&gt;With...&lt;/code&gt; pattern for fluent coding. The first one tells which files to read, relative to the root directory. This happen to use a globbing that allows me to grab all Markdown files in the source directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = _readFiles
    .Run(cancellationToken)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This run the _readFiles operation and gets a list of the files it read in. (I'm also going to put this in the &lt;code&gt;OutputHtmlPipeline&lt;/code&gt; to show how everything writes out.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;foreach (var entity in list)
{
    _logger.LogInformation(
        &amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
        entity.Get&amp;lt;UPath&amp;gt;(),
        entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is just some debugging to show how things are working and how Gallium works. In this case, the read files sets two components: the &lt;code&gt;UPath&lt;/code&gt; for the path of the file and a &lt;code&gt;IBinaryContent&lt;/code&gt; which is an abstraction to the binary content. Since we haven't done anything to the file, this just points to the file system itself without loading it into memory. That way, we can easily deal with gigabyte-sized files without overloading the system.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;return list.ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, we return the list of files that we just read in. With this change, running the build gets us this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:20:03 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:20:03 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:20:03 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.9543639
[14:20:04 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.9559424
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, that single file read in is then passed through the other pipelines because each of them return their input list and then the final pipeline gets those same entities and paths.&lt;/p&gt;
&lt;h3&gt;Changing Paths&lt;/h3&gt;
&lt;p&gt;Now the problem with the above example is that we are writing our output to the same location as we are reading. That isn't optional. What we want to do is strip off the &lt;code&gt;/src/pages/typewriter&lt;/code&gt; from the beginning of the read in path and add &lt;code&gt;/build/typewriter/html&lt;/code&gt; in front of it. This is easily done with two additional operators, &lt;code&gt;RemovePathPrefix&lt;/code&gt; and &lt;code&gt;AddPathPrefix&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To start with, let's remove the prefix from the read methods to normalize the paths.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    RemovePathPrefix removePathPrefix)
{
    _logger = logger;
    _removePathPrefix = removePathPrefix.WithPathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
    _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then we chain the operation using an extension method on operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_removePathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since each operation also takes an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; and removes the same type, they can flow from one operation to another. When we run the build, the output line looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding the output prefix is done on the output step:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we already have a list of entities, we can just use the extension method to pipe it through the operation and produce the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:28:35 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason &lt;code&gt;PagesPipeline&lt;/code&gt; doesn't use &lt;code&gt;entities.Run(...)&lt;/code&gt; is because that is the first pipeline and the &lt;code&gt;entities&lt;/code&gt; is going to be an empty list. Instead, we need to make entities, which is why we call &lt;code&gt;_readFiles.Run()&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3&gt;Writing Files&lt;/h3&gt;
&lt;p&gt;And the last bit for today's post is to write out files. This, creatively enough, uses the &lt;code&gt;WriteFiles&lt;/code&gt; operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .Run(_writeFiles)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we've already done all the fancy operations to adjust the path so it writes out the files in the proper location:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ find build -type f
build/typewriter/html/index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convenience Methods&lt;/h3&gt;
&lt;p&gt;Now, adding and removing path prefixes is pretty common, so both the &lt;code&gt;ReadFiles&lt;/code&gt; and &lt;code&gt;WriteFiles&lt;/code&gt; have methods to do those in a single call.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles)
{
    _logger = logger;
    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles.WithAddPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;And there you go, an over-engineered system for copying a single file from one directory to another. In the next post of this series, we'll turn that Markdown file into an HTML page.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Pipelines</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/08/using-nitride-pipelines/" />
    <updated>2025-06-08T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/08/using-nitride-pipelines/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="just" scheme="https://d.moonfire.us/tags/" label="Just" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <summary type="html">The first major concept of Nitride is the idea of a "pipeline" which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.
</summary>
    <content type="html">&lt;p&gt;I found a good way of working on a project is being able to see the end product. That means forging a minimal path from the start to end, and then going back to flesh it out. Interestingly, this is the opposite of how I write novels because, in those, the journey is the important part. Not so much with development projects.&lt;/p&gt;
&lt;p&gt;In this case, the goal is to get a website generated and the ability to see it locally.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;Since this mainly a .NET project, we start by creating a new project and solution. This is a fairly simple generator and I don't plan on needing more than one assembly, so I'm going to throw everything into &lt;code&gt;//src/dotnet/&lt;/code&gt; for the website.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ dotnet new console --name Generator --output src/dotnet
$ dotnet new solution --name Website
$ dotnet sln add src/dotnet/Generator.csproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since we want to have the project run, we also update the &lt;code&gt;Justfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-just"&gt;# //Justfile

# Builds the typewriter.press website
build-typewriter:
    dotnet run --project src/dotnet/Generator.csproj --
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that gives us an easy command to run that works anywhere underneath the Git repository (thanks to &lt;a href="/tags/just/"&gt;Just&lt;/a&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj --
Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up NuGet&lt;/h2&gt;
&lt;p&gt;At the moment, the &lt;code&gt;MfGames.*&lt;/code&gt; assemblies are not on nuget.org. There are a couple of reasons, mostly self-doubt that anyone would find this useful and the dread of setting up a SSL certificate which appeared to be required. My current intent is to do it once I have a number of sites using Nitride or I get a request to do so. Until then, I use &lt;code&gt;NuGet.config&lt;/code&gt; file to hook up my libraries to the website.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- //src/dotnet/NuGet.config &amp;gt;
&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;packageSources&amp;gt;
    &amp;lt;clear /&amp;gt;
    &amp;lt;add key=&amp;quot;nuget.org&amp;quot; value=&amp;quot;https://api.nuget.org/v3/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
    &amp;lt;add key=&amp;quot;mfgames.com&amp;quot; value=&amp;quot;https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
  &amp;lt;/packageSources&amp;gt;
  &amp;lt;packageSourceMapping&amp;gt;
    &amp;lt;packageSource key=&amp;quot;nuget.org&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
    &amp;lt;packageSource key=&amp;quot;mfgames.com&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;MfGames.*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
  &amp;lt;/packageSourceMapping&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up the Generator&lt;/h2&gt;
&lt;p&gt;While &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; could use configuration files to do the bulk of the setup, I went with a configuration-as-code approach to it since I almost always end up writing custom code. That way, everything is done in the same manner. This also uses a model roughly based on the generic host for command line.&lt;/p&gt;
&lt;p&gt;🛈 I'm in the process of reworking the initial setup to use the generic host more completely. The current implementation was my best approach to write a dependency-injected console application that is configured via the CLI instead of JSON files.&lt;/p&gt;
&lt;p&gt;The first step is to include the required package. Nitride is organized in a lot of small, focused packages. This is it avoid pulling in too much for a given project but also to help keep everything organized. For the initial work, we can add the first two required assemblies.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cd src/dotnet
$ dotnet add package MfGames.Nitride
$ dotnet add package MfGames.Nitride.IO
$ dotnet add package Autofac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nitride using the excellent &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; library for abstracting file system access, plus making it easier to write test to make sure everything work. We also use &lt;a href="https://autofac.org/"&gt;Autofac&lt;/a&gt; for a lot of the module registration and processing.&lt;/p&gt;
&lt;p&gt;🛈 I'm working on removing the Autofac dependency and using Microsoft.Extensions.DependencyInjection instead but it will take a while. There are a lot of useful features in Autofac that the extensions don't have so it will take some time.&lt;/p&gt;
&lt;p&gt;First, we create the website module with a very generic, naive implementation (unless otherwise mentioned, all of these files are in &lt;code&gt;//src/dotnet/&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// WebsiteModule.cs
using Autofac;

namespace Generator;

public class WebsiteModule : Module
{
    /// &amp;lt;inheritdoc /&amp;gt;
    protected override void Load(ContainerBuilder builder)
    {
        builder
            .RegisterAssemblyTypes(GetType().Assembly)
            .AsSelf()
            .AsImplementedInterfaces()
            .SingleInstance();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that, we change the initial program to use Nitride:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Program.cs
using MfGames.Nitride;
using MfGames.Nitride.IO.Setup;

namespace Generator;

public static class Program
{
    public static async Task&amp;lt;int&amp;gt; Main(string[] args)
    {
        var rootDirectory = new DirectoryInfo(Environment.CurrentDirectory);

        var builder = new NitrideBuilder(args)
            .UseIO(rootDirectory)
            .UseModule&amp;lt;WebsiteModule&amp;gt;();

        return await builder.RunAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run this, we'll get an error:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj --
Required command was not provided.

Description:

Usage:
  MfGames.Nitride [command] [options]

Options:
  --log-level &amp;lt;log-level&amp;gt;                    Controls the verbosity of the output, not case-sensitive and prefixes allowed: 
                                             Verbose, Debug, Information, Warning, Error, Fatal [default: Warning]
  --log-context-format &amp;lt;log-context-format&amp;gt;  Controls the format of the source context for log items, not case-sensitive and 
                                             prefixes allowed: None, Class, Full [default: Class]
  --log-time-format &amp;lt;log-time-format&amp;gt;        Controls the format of the time in the log messages, such as 'HH:mm:ss' (default) or 
                                             'yyyy-MM-ddTHH:mm:ss'. Blank means exclude entirely. [default: HH:mm:ss]
  --log-level-override &amp;lt;log-level-override&amp;gt;  Overrides log levels for certain contexts in the format of either 'Context' or 
                                             'Context=Level' (repeat for multiple) [default: Microsoft=Warning]
  --version                                  Show version information
  -?, -h, --help                             Show help and usage information

Commands:
  build  Generate the website
  watch  Generates the site and then watches for changes

error: Recipe `build-typewriter` failed on line 15 with exit code 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;watch&lt;/code&gt; command doesn't work very well right now, but we want &lt;code&gt;build&lt;/code&gt;, so we'll update our Justfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# //Justfile
build-typewriter:
    dotnet run --project src/dotnet/Generator.csproj -- build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then try again:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter
dotnet run --project src/dotnet/Generator.csproj -- build
[12:27:17 ERR] &amp;lt;PipelineManager&amp;gt; There are no registered pipelines run, use ConfigureContainer to include IPipeline instances
error: Recipe `build-typewriter` failed on line 15 with exit code 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pipelines&lt;/h2&gt;
&lt;p&gt;Now we get to the first big concept of Nitride: pipelines. Some of the ideas are built up from &lt;a href="/tags/statiq/"&gt; Statiq&lt;/a&gt; and also the patterns I've worked out with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt;. They basically are larger &amp;ldquo;units&amp;rdquo; of processing for the site that are intended to gather a bunch of files and then feed them into another pipeline which then splits out into different ones for output.&lt;/p&gt;
&lt;p&gt;It also allows a group of inputs to be handled differently, such as linking the &amp;ldquo;next&amp;rdquo; and &amp;ldquo;previous&amp;rdquo; to posts which doesn't make for pages. Another case where I've used this heavily is to copy binary assets directly to the styled output, generate CSS through &lt;code&gt;webpack&lt;/code&gt; or &lt;code&gt;esbuild&lt;/code&gt;, or generate project-specific pages from the input.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-goat"&gt;        +-------+ +-------+
        | Pages | | Posts |
        +-------+ +-------+
            |         |
            +----+----+
                 | 
                 v
          +-------------+
          |  Simplified |
          |   Markdown  |
          +-------------+
                 |
     +-----------+-----------+
     |           |           |
     v           v           v
+---------+ +---------+ +---------+
|  Style  | |  Atom   | | Style   |
|  HTML   | |  Feeds  | | Gemtext |
+---------+ +---------+ +---------+
     |                       |
     v                       v
+---------+             +---------+
|  Output |             | Output  |
|  HTML   |             | Gemtext |
+---------+             +---------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pipelines attempt to run in parallel to reduce the amount of time on the developer's machine. They are also asynchronous at their core since a pipeline could be used to query another system, require long-running processes, or perform some other action that would require using that functionality.&lt;/p&gt;
&lt;p&gt;Obviously, you could use a single pipeline. I use to start that way, but the pattern of gathering the information, combining them together to generate tags and categories, and then styling fits better as separate steps for me. Since this is my website, we're going to create a minimum from the pages, generate simplified Markdown, style it, and then write it out.&lt;/p&gt;
&lt;p&gt;Since we use dependency injection throughout Nitride, we pass in the dependencies into constructor of a pipeline and then add it to that project. Internally, Nitride runs every pipeline in a separate task when it is building the site.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Inputs/PagesPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Inputs;

public class PagesPipeline : PipelineBase
{
    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Content/SimplifiedMarkdownPipeline.cs
using Generator.Pipelines.Inputs;
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Content;

public class SimplifiedMarkdownPipeline : PipelineBase
{
    public SimplifiedMarkdownPipeline(
        PagesPipeline pagesPipeline)
    {
        AddDependency(pagesPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Html/StyleHtmlPipeline.cs
using Generator.Pipelines.Content;
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Html;

public class StyleHtmlPipeline : PipelineBase
{
    public StyleHtmlPipeline(
        SimplifiedMarkdownPipeline simplifiedMarkdownPipeline)
    {
        AddDependency(simplifiedMarkdownPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Html/OutputHtmlPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Html;

public class OutputHtmlPipeline : PipelineBase
{
    public OutputHtmlPipeline(
        StyleHtmlPipeline styleHtmlPipeline)
    {
        AddDependency(styleHtmlPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since we want to avoid exceptions, we'll ignore the entire &lt;code&gt;Entity&lt;/code&gt; bit and do a couple of changes that will be explained in the next post.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;PagesPipeline.cs&lt;/code&gt;, we want to return an empty list:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    return Array.Empty&amp;lt;Entity&amp;gt;().ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For all the others, we're just going to pass whatever we got in back out:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    return entities.ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'll admit, I stumbled into the usage of &lt;code&gt;IAsyncEnumerable&lt;/code&gt;. It lets me use &lt;code&gt;await&lt;/code&gt; calls inside a function without needing to load an entire list into memory just to copy it out again into another list to make the pipelines to work. Given that one of my baseline websites was to process my entire &lt;a href="//fedran.com/"&gt;fiction website&lt;/a&gt; without killing my machine, I want to reduce the number of copies in memory and reduce the pressure there.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;IAsyncEnumerable&lt;/code&gt; is cumbersome though, so might I might create a convenience method in &lt;code&gt;PipelineBase&lt;/code&gt; that allows for a &lt;code&gt;Task&amp;lt;IEnumerable&amp;lt;Entity&amp;gt;&amp;gt;&lt;/code&gt; and change it an &lt;code&gt;IAsyncEnumerable&lt;/code&gt; but that is a future idea to work out.&lt;/p&gt;
&lt;p&gt;When we run this, we get this (after adding ``&amp;ndash;log-level debug&lt;code&gt;into the&lt;/code&gt;//Justfile`):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj -- build --log-level debug
[13:22:48 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[13:22:48 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[13:22:48 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.1051617
[13:22:48 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.1075694
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It doesn't really do anything in the 0.1 seconds that it takes to run, but it basically gets the pipeline chain working so things will be generating with the next in this series.&lt;/p&gt;
&lt;h2&gt;Observations&lt;/h2&gt;
&lt;p&gt;I'm also aware that this appears to be overkill for most websites. If I was doing an HTML-only website, I would have started with only a single pipeline but I already know that I'm going to generate Atom feeds out of that and they work better with simple HTML instead of fancy chrome. Being able to style the Atom's output separately gives me some flexibility to do different things there. Also there are going to be multiple inputs eventually so experience tells me that this is the &amp;ldquo;best&amp;rdquo; approach for generating output given the circumstances of this specific website.&lt;/p&gt;
&lt;p&gt;You'll also notice that I'm not doing a site-specific pipelines. I've tried a couple of approached and my current one is to run the entire generation process once per website using some configuration settings, which I'll get after I finish the main website.&lt;/p&gt;
&lt;h2&gt;Future Plans&lt;/h2&gt;
&lt;p&gt;The pipelines also are the units that are triggered on the &lt;code&gt;watch&lt;/code&gt; command (which doesn't really work). If a pipeline identifies that it needs to be run, it will run but then trigger a re-run of every pipeline that depends on that.&lt;/p&gt;
&lt;p&gt;The advantage of that if I'm working on styling CSS, I can have a pipeline feeding into the &lt;code&gt;StyleHtmlPipeline&lt;/code&gt; that generates that. When the hypothetical &lt;code&gt;CssHtmlPipeline&lt;/code&gt; triggers a rebuild, it runs itself, &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;, and &lt;code&gt;OutputHtmlPipeline&lt;/code&gt; only because of those dependencies. But changing a page would trigger everything &lt;em&gt;but&lt;/em&gt; the &lt;code&gt;CssHtmlPipeline&lt;/code&gt; which should keep watches fairly performant.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;In the next post, I'll explain what &lt;code&gt;Entity&lt;/code&gt; is and start make this actually generate something useful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Introduction</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/07/using-nitride-introduction/" />
    <updated>2025-06-07T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/07/using-nitride-introduction/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="bitrot" scheme="https://d.moonfire.us/tags/" label="Bitrot" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="direnv" scheme="https://d.moonfire.us/tags/" label="direnv" />
    <category term="lefthook" scheme="https://d.moonfire.us/tags/" label="Lefthook" />
    <category term="editorconfig" scheme="https://d.moonfire.us/tags/" label="EditorConfig" />
    <category term="conventional-commits" scheme="https://d.moonfire.us/tags/" label="Conventional Commits" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <summary type="html">I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.
</summary>
    <content type="html">&lt;p&gt;One drawback of having a lot of irons in the fire is occasionally projects that should not have been forgotten are and they &lt;a href="/tags/bitrot/"&gt;bitrow&lt;/a&gt;. A good example is the &lt;a href="https://typewriter.press"&gt;Typewriter Press&lt;/a&gt; and it's associated reflected websites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sassy.typewriter.press"&gt;Sassy&lt;/a&gt; - Romance novels&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dusty.typewriter.press"&gt;Dusty&lt;/a&gt; - Historical fiction&lt;/li&gt;
&lt;li&gt;&lt;a href="https://broken.typewriter.press"&gt;Broken&lt;/a&gt; - Fantasy including contemporary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, when I start publishing my sci-fi stories, they would go under &amp;ldquo;Electric&amp;rdquo;. I call them &amp;ldquo;reflected&amp;rdquo; sites because they are just filtered versions of the main Typewriter site with some slightly different branding. That way, if I wrote more romance, I could put them there and folks could go to the romance-only site or jump over to the main one to see that I have no single genre (much like I don't have a single genre for my writing).&lt;/p&gt;
&lt;p&gt;However, it's been years since I've updated those sites and they are beginning to creak underneath their age. Not to mention, I haven't included the last two books I published on them and that is humiliating for me.&lt;/p&gt;
&lt;p&gt;So, I decided to switch the sites over to &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; and document the process.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be broken into multiple posts, mostly because I know I'm going to get distracted around the fifteenth of the month when I switch to writing, but also to keep these topic-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conventions&lt;/h2&gt;
&lt;p&gt;My usual conventions when giving file names is to use &lt;code&gt;//&lt;/code&gt; for the Git repository root. So, &lt;code&gt;//flake.nix&lt;/code&gt; means the &lt;code&gt;flake.nix&lt;/code&gt; in the root level of the project. Also, I'll add a trailing &lt;code&gt;/&lt;/code&gt; when I'm talking specifically about a directory, such as &lt;code&gt;//node_modules/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;NixOS Flakes&lt;/h2&gt;
&lt;p&gt;Since I'm going to be creating a new site instead of converting the existing, I can easily start from the beginning. That usually means starting with a NixOS flake which is my preferred way of writing.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# //flake.nix
{
  inputs = {
    nixpkgs.url = &amp;quot;nixpkgs/nixos-25.05&amp;quot;;
    flake-utils.url = &amp;quot;github:numtide/flake-utils&amp;quot;;
    mfgames-project-setup.url = &amp;quot;git+https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git&amp;quot;;
  };

  outputs = inputs @ { self, nixpkgs, flake-utils, mfgames-project-setup }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        project-config = mfgames-project-setup.lib.mkConfig {
          inherit system;
          pkgs = nixpkgs.legacyPackages.${system};
          dotnet.enable = true;
          prettier.proseWrap = &amp;quot;never&amp;quot;;
        };
      in
      {
        devShell = pkgs.mkShell {
          packages = [
            # Development
            pkgs.dotnet-sdk
            pkgs.nodejs_20

            # Local Serving
            pkgs.miniserve
            pkgs.agate
          ]
          ++ project-config.packages;

          shellHook = project-config.shellHook;
        };
      });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There really isn't much to this other than my use of &lt;a href="https://mfgames.com/mfgames-project-setup-flake/"&gt;mfgames-project-setup-flake&lt;/a&gt;. This is a flake that sets up a lot of the common elements I use: a consistent &lt;code&gt;.editorconfig&lt;/code&gt;, &lt;code&gt;treefmt&lt;/code&gt; for formatting various parts of the system, &lt;code&gt;lefthook&lt;/code&gt; for setting up &lt;a href="/tags/conventional-commits/"&gt;conventional commits&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And naturally, I have to set up &lt;a href="/tags/direnv/"&gt;direnv&lt;/a&gt; because I want to set up my environment as soon as I change into the directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# //.envrc
use flake || use nix

dotenv_if_exists .env

PATH_add $PWD/node_modules/.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm adding the &lt;code&gt;node_modules&lt;/code&gt; path because I know I'm going to be using &lt;code&gt;webpack&lt;/code&gt; as part of this site.&lt;/p&gt;
&lt;p&gt;Once those two files are in, then I can get my basic setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ git add flake.nix .envrc
$ direnv allow
direnv: nix-direnv: Renewed cache
nixago: updating repository files
nixago: '.conform.yaml' link updated
nixago: '/.conform.yaml' added to .gitignore
nixago: '.editorconfig' copy created
nixago: 'lefthook.yml' link updated
Error: unknown shorthand flag: 'a' in -a
Error: unknown shorthand flag: 'a' in -a
nixago: '/lefthook.yml' added to .gitignore
nixago: '.prettierrc.json' link updated
nixago: '/.prettierrc.json' added to .gitignore
nixago: 'treefmt.toml' link updated
nixago: '/treefmt.toml' added to .gitignore
mfgames-project-setup: '/.direnv/' added to .gitignore
sync hooks: ✔️ (commit-msg, pre-commit)
direnv: export +AR +AS +AWS_ACCESS_KEY_ID +AWS_BUCKET +AWS_ENDPOINT +AWS_REGION +AWS_SECRET_ACCESS_KEY +CC +CONFIG_SHELL +CXX +DOTNET_CLI_TELEMETRY_OPTOUT +DOTNET_NOLOGO +DOTNET_SKIP_FIRST_TIME_EXPERIENCE +DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK +HOST_PATH +IN_NIX_SHELL +LD +MSBUILDALWAYSOVERWRITEREADONLYFILES +MSBUILDTERMINALLOGGER +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_LDFLAGS +NIX_STORE +NM +NODE_PATH +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS
$ l
total 64K
drwxr-xr-x  5 dmoonfire users 4.0K Jun  7 11:10 .
drwxrwxr-x 18 dmoonfire users 4.0K Dec 26 08:08 ..
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 .conform.yaml -&amp;gt; /nix/store/pckycri07q2ah90swk6hf21ilsi59a4s-conform.yaml
drwxr-xr-x  4 dmoonfire users 4.0K Jun  7 11:10 .direnv
-rw-r--r--  1 dmoonfire users 5.2K Jun  7 11:10 .editorconfig
-rw-------  1 dmoonfire users  259 Jun  7 10:40 .env
-rw-r--r--  1 dmoonfire users  13K Jun  7 11:10 flake.lock
-rw-r--r--  1 dmoonfire users  983 Jun  7 11:03 flake.nix
drwxrwxr-x  8 dmoonfire users 4.0K Jun  7 11:10 .git
-rw-r--r--  1 dmoonfire users  139 Jun  7 11:10 .gitignore
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 lefthook.yml -&amp;gt; /nix/store/ihdb9vnc6jp535m43ndas79s4p9viyjn-lefthook.yml
lrwxrwxrwx  1 dmoonfire users   59 Jun  7 11:10 .prettierrc.json -&amp;gt; /nix/store/mjdm21r27r4n43aq92qkvmdcvy0l2qrc-prettierrc.json
-rw-r--r--  1 dmoonfire users 1.2K Jun  7 10:40 README.md
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 treefmt.toml -&amp;gt; /nix/store/4lk6rmb2khs5l3yn91rah9y4c6zmxybf-treefmt.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Probably one of my favorite things is that it also sets up the initial &lt;code&gt;.gitignore&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cat .gitignore
# nixago: ignore-linked-files
/treefmt.toml
/.prettierrc.json
/lefthook.yml
/.conform.yaml
# mfgames-project-setup: ignore-files
/.direnv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Overall, using that just cuts out that first hour of starting up a new project. And it lets me evolve my changes to style and different tools by simply running &lt;code&gt;nix flake update&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Directory Layout&lt;/h2&gt;
&lt;p&gt;Related to using a flake to setup a lot of the boilerplate, I have &lt;a href="//d.moonfire.us/garden/project-layout/"&gt;written my thoughts&lt;/a&gt; on setting up polyglot projects such as this in a consistent manner. I think it is a relatively short digital garden plot, but there are a couple applicable things.&lt;/p&gt;
&lt;p&gt;Source code goes into &lt;code&gt;//src/&lt;/code&gt; and the output is going into &lt;code&gt;//build&lt;/code&gt; (which has an entry in &lt;code&gt;.gitignore&lt;/code&gt;). One of the artifacts of my &lt;a href="/tags/wordpress/"&gt;WordPress&lt;/a&gt; days is that I have static pages, which I put into &lt;code&gt;//src/pages/&lt;/code&gt;, and blog posts which go into &lt;code&gt;//src/posts/&lt;/code&gt;. There is some magic that goes on with posts, but that's a different post.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ find src
src
src/pages
src/pages/typewriter
src/pages/typewriter/index.md
src/posts
src/posts/2025-07-01-website-redesign.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I put an extra layer of directories under &lt;code&gt;//src/pages/&lt;/code&gt; for a site &amp;ldquo;slug&amp;rdquo; to identify which site the page will go on. In this case, I'm going to have &lt;code&gt;typewriter&lt;/code&gt; for the main site, &lt;code&gt;sassy&lt;/code&gt;, &lt;code&gt;dusty&lt;/code&gt;, &lt;code&gt;broken&lt;/code&gt;, and &lt;code&gt;electric&lt;/code&gt;. There will also be a &lt;code&gt;common&lt;/code&gt; for pages that are shared across all pages such as the contact page.&lt;/p&gt;
&lt;p&gt;For output, those same slugs are going to be used. I'm intending to have this output both HTML and Gemtext pages for HTTPS and &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; respectively. I know not a lot of people use Gemini these days, but I still think it is useful and there is a certain minimalism to it that appeals to me.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;find build
build
build/typewriter
build/typewriter/html
build/typewriter/gemtext
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Just&lt;/h2&gt;
&lt;p&gt;Another part of my standard project layout is using &lt;a href="/tags/just/"&gt;Just&lt;/a&gt; to handle the build system. There are a number of reasons, but I'm going to be dealing with Node programs, such as &lt;code&gt;webpack&lt;/code&gt;, and also .NET projects for the build. There is also CLI executables for hosting locally (&lt;code&gt;miniserv&lt;/code&gt; and &lt;code&gt;agate&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I made a conscious decision not to write wrappers for Node and the other CLIs into Nitride. There is the ability to execute, but I don't want to tightly tie the site generator to a specific tool. Not to mention, most of the time, I want to set them up in different tabs to watch them so I want a more &amp;ldquo;generic&amp;rdquo; task runner than the ones built into Node or .NET which have some biases I've struggled with.&lt;/p&gt;
&lt;p&gt;Just is something that is self-contained, doesn't make a lot of assumptions, and doesn't drag an entire ecosystem in with it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-just"&gt;# //Justfile
set dotenv-load

_default:
    just --choose

# Cleans up the project without removing the local .env
clean:
    git clean -xfd --exclude .env

# Builds all the websites
build: build-typewriter

# Builds the typewriter.press website
build-typewriter:
    echo I did something

# Serves the Typewriter website
serve-typewriter-html:
    miniserve --index index.html build/typewriter/html

# Serves the Typewriter Gemini pod
serve-typewriter-gemtext:
    mkdir -p .cache/agate
    agate --content build/typewriter/gemtext --certs .cache/agate --hostname localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Naturally, &lt;code&gt;.cache/&lt;/code&gt; has to be added to the &lt;code&gt;.gitignore&lt;/code&gt; file. I use &lt;code&gt;.cache&lt;/code&gt; from the project layout plot.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;Now that we have the basic boilerplate for a new project, time to start adding .NET into the mix.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Why SeaweedFS?</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/11/08/why-seaweedfs/" />
    <updated>2024-11-08T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/11/08/why-seaweedfs/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="ceph" scheme="https://d.moonfire.us/tags/" label="Ceph" />
    <category term="seaweedfs" scheme="https://d.moonfire.us/tags/" label="SeaweedFS" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="sand-and-bone" scheme="https://d.moonfire.us/tags/" label="Sand and Bone" />
    <summary type="html">Thoughts on why I take the effort to use a distributed network drive for my home.
</summary>
    <content type="html">&lt;p&gt;I got an email last week asking if I could explain why I set up a Seaweed server. It's been a while since I talked about it, mostly in hints in &lt;a href="/blog/2022/12/10/ceph-and-nixos/"&gt;this post from 2022&lt;/a&gt; and &lt;a href="/blog/2024/03/21/switching-ceph-to-seaweedfs/"&gt;this post from 2024&lt;/a&gt;. The request gives me an opportunity to expand on it, and to remind myself why I do it.&lt;/p&gt;
&lt;h2&gt;Stories&lt;/h2&gt;
&lt;p&gt;In some regards, there is a bit of trauma in me when it comes to losing out on things. This includes hearing stories that my father told me and realizing that when he died, they were gone forever. These same thoughts came up in my novel &lt;a href="/tags/sand-and-bone/"&gt;Sand and Bone&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The little girl's confessions echoed in Rutejìmo's head in a quiet symphony as he wrote in his book. The hand-bound collections of pages creaked under his hand, the leather thong strained to hold the almost fifty pages of tightly-spaced writing. Over the years, he had added a dozen pages to the collection. It wouldn't be too long before the binding couldn't handle the additional pages, but he thought he had a few more years left before that happened.&lt;/p&gt;
&lt;p&gt;Even with his additional pages, he didn't have room to write down all of the stories he had heard over the years. He wanted to detail the joys of the little girl's death, such as the choked story about how she had stolen her brother's toy when he wasn't looking. He had also wanted to write the horrors, like one man's confession for killing his sister. Each one was precious and important. Time would erase their stories and a part of Rutejìmo died every time he forgot one.&lt;/p&gt;
&lt;p&gt;&amp;mdash; &lt;a href="//fedran.com/sand-and-bone/chapter-008/"&gt;Sand and Bone 8: Alone&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This fear is more than just one person, losing stories of things that happened decades ago as told by an old man. This includes my own stories, which will also be lost when I die. There is little chance I will be &amp;ldquo;done&amp;rdquo; with &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt;. There is little chance I will get all of the stories out of my head and onto paper. There isn't enough time, I don't have the self-esteem to do it, and I don't have the drive. When I'm gone, so are those stories. But, even going beyond that, there are so many stories of horror and joy that I feel we are losing with the passage of time. Happy stories of people falling in love, horror stories of &lt;a href="https://www.auschwitz.org/en/"&gt;Auschwitz&lt;/a&gt;, and everything in between. It is that breadth of stories, lessons, and happiness that we lose.&lt;/p&gt;
&lt;p&gt;But the big question is, will anyone care? Does anyone want to know the first time I got caught shoplifting. Or the first time I finally said &amp;ldquo;I love you&amp;rdquo; to Partner?&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;But, you never know.&lt;/p&gt;
&lt;p&gt;I'm never going to record them, but when I was given my father's artwork and decades of effort, I don't want to lose them yet. I don't want to just chuck it aside. So, I need to keep them.&lt;/p&gt;
&lt;h2&gt;Hoarding&lt;/h2&gt;
&lt;p&gt;I come from a long line of hoarders. I realized that as I look at my DVD collection or the boxes of books I have. My LEGO collection isn't huge, but it does fill five or six boxes. I once got rid of it, and I felt guilty for years for doing that; which is why I built it up again. Sometimes, I just play.&lt;/p&gt;
&lt;p&gt;My mother did it. She had a massive display case of dragons and cats. She has dozens of sets of china in the basement. Entire room with the bones of a bankrupt lumber mill. A massive library (that I inherited).&lt;/p&gt;
&lt;p&gt;My father did it. I spent days going through pieces of paper where he kept every calendar, shred of note, and personal letter he sent to his children. I saw research when I asked for help to get through college, mainly to prove that I was going the wrong path and I really should just drive two hours one way to go the college he thought was a better choice for me. I ended up saying &amp;ldquo;screw you&amp;rdquo; and went into debt for a couple decades instead.&lt;/p&gt;
&lt;p&gt;My father has terabytes of images that he drew. Decades of him struggling to be a &amp;ldquo;good&amp;rdquo; artist, self-doubt and pain. But I'm so happy to watch him to do it. Also, he did the artwork for the nuclear reactor project he was on, and the particle accelerator. And birthday cards for all of his children and grandchildren.&lt;/p&gt;
&lt;p&gt;For me, there is a good size of digital hoarding going on. I mean, a couple thousand ebooks is one things, but I have PDFs, for game systems that go back years. The original PDFs of HERO System 5 and 6. Every scan of the &lt;em&gt;Dragon&lt;/em&gt; magazine. GURPS. Legal documents, funny stories, and the like. And then there are my DVDs. Back when I didn't have children, I was buying 3-5 a week. When Suncoast Video went out of business, I had a huge refund from the IRS. Walked into the store and said &amp;ldquo;I'm gunna buy everything I can.&amp;rdquo; Now, there are DVDs I can't buy anymore, I can't find. But they are slowly rotting in my file cabinet (did you know they only have a 20-30 year shelf life?). I don't want to lose them, even if I don't use them every day.&lt;/p&gt;
&lt;p&gt;And then there is Partner's photography business. They do large photo shoots but they also need to keep them around for years &amp;ldquo;just in case&amp;rdquo; someone loses their wedding photos. She doesn't &amp;ldquo;have to&amp;rdquo;, but I don't want to ever have to say &amp;ldquo;sorry, I don't have them&amp;rdquo; for a senior photos or someone's puppies. Or family members are lost.&lt;/p&gt;
&lt;p&gt;So I want to keep them.&lt;/p&gt;
&lt;h2&gt;Storage&lt;/h2&gt;
&lt;p&gt;All this takes space. The nice thing about digital space is that I can make backups. I can take them with me with a few kilograms of hard drives or upload them into the cloud. I can make copies to make it harder to lose (like when I was hit with a ransomware that took out my media server). The goal is &lt;a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/"&gt;3-2-1 Backup Strategy&lt;/a&gt;: three copies, two locations, and at least one offline copy.&lt;/p&gt;
&lt;p&gt;Now, dealing with storage is something I've done for quite a long time. I remember being pulled out of sixth grade school for a day to help my mother recover her RAID 5 box. At the time, drives were in the low 100s of megabytes, but I had to learn a lot about how RAID worked to figure it out. Then watching the slow recovery that took an entire day&amp;hellip; then having another drive fail about a day after we recovered the first time and doing it all again. I used and watched hardware and software RAID controllers fail.&lt;/p&gt;
&lt;p&gt;At the time, the data wasn't static. We were processing millions of records to do analysis. The drives would get corrupted by poor power, heavy usage, and the eventual failure of mechanical drives. We had customers lose data, watched our data centers blow a disk. Like Partner's photos, we had to keep our analysis for years after because of re-evaluation or the occasional lawsuit.&lt;/p&gt;
&lt;p&gt;Over time, it became apparent that there was a maximum size where RAID was useful. After a while, it wasn't a matter &amp;ldquo;if&amp;rdquo; a drive goes back but &amp;ldquo;when&amp;rdquo; a drive goes bad. Backblaze (where I keep my backups, I recommend them) had a &lt;a href="https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2024/"&gt;quarterly blog post&lt;/a&gt; about drive stats. They track with my own experiences. Yeah, 1.7% failure rate doesn't seem like much but when I'm dealing with stuff that needs to be kept for years, it hits a lot faster than you'd think it would. And offline backups fail too.&lt;/p&gt;
&lt;p&gt;When I left my mother's company, I went back a little and just threw some drives into a big machine. Didn't bother with RAID because I knew it would fail since I was looking at a decade of hard drive use and started with a manual mirroring between a couple Windows partitions. But as the DVDs got ripped, music got purchased from Amazon, and photos kept being taken, I started to run out. Soon, it wasn't just &lt;code&gt;Q:\&lt;/code&gt;, &lt;code&gt;R:\&lt;/code&gt;, and &lt;code&gt;S:\&lt;/code&gt; being copies but each one having a portion of everything.&lt;/p&gt;
&lt;p&gt;When I got hit with the ransomware attack, I lost my DVD collection (years of ripping) but not most of Partner's photos and the other things. I started to recover them but one of the drives didn't make it.&lt;/p&gt;
&lt;p&gt;Then I lost another drive a few months later.&lt;/p&gt;
&lt;p&gt;Then we didn't have money to handle the failure indicators, so I was just watching the SMART notices popping up knowing that there was nothing to do but watch the slow-moving train accident.&lt;/p&gt;
&lt;p&gt;Then the media server decided to go down for a week.&lt;/p&gt;
&lt;p&gt;Not having access to anything was a stark reminder that I had to do &amp;ldquo;something&amp;rdquo; if I didn't want to lose everything. And it wasn't just tossing in another drive every once in a while. I had hit the threshold where I needed to spread it out across more than just drives, I needed to spread it across servers.&lt;/p&gt;
&lt;h3&gt;Ceph&lt;/h3&gt;
&lt;p&gt;Enter &lt;a href="/tags/ceph/"&gt;Ceph&lt;/a&gt;. I've read about Ceph for many years before that point and it always appealed to me. It seemed to solve a lot of the problems I've experienced over the years. And it didn't have to require same-sized disks to pull of a RAID. And I could add machines if I needed to add more storage.&lt;/p&gt;
&lt;p&gt;The biggest thing with Ceph is that it balanced across multiple servers to reduce failure, but also let you mark files as needing one, two, or more copies. Partner's photo library? Two copies. Videos? One is probably good enough. The servers didn't have to be that powerful either, so I could use my older machines to keep the disk when I had to upgrade to a newer one. In theory, I could use a Raspberry Pi to act as a cluster.&lt;/p&gt;
&lt;p&gt;The incident when this happened was when the media server died again. I had saved some money from my commissions (earmarked to get a book edited) and used it to buy fresh hard drives. If anything, replacing the 10+ year old drives with something new and bigger would give me some. I also took the opportunity to switch to &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; instead of Windows, which I dislike anyways. I mean, NixOS had options for Ceph, how hard could it be?&lt;/p&gt;
&lt;p&gt;Apparently, it really wasn't ready for prime time. Eventually the wiki popped up to say that.&lt;/p&gt;
&lt;p&gt;Took me a week to figure it out. There was a lot missing (and still is) from the core, but I managed to write up some notes for myself on how to make it work. But, after days of trial and errors, of &amp;ldquo;almost got it&amp;rdquo; joys only to watch it crash, I finally got something working. And it was glorious. At least until I realized was swapping like mad and Ceph really needs 1 GB per TB of storage. A few frantic purchases later, and I had a couple more cheap Dell machines (one of the ones that actually died this week) and I ended up with a fairly balanced Ceph cluster.&lt;/p&gt;
&lt;h3&gt;SeaweedFS&lt;/h3&gt;
&lt;p&gt;I liked Ceph, but I'm not on it anymore. Partially it was selfish reasons, I want to fix the struggles I had bringing on a new drive into the cluster but the community decided to go a different way. But there was also no one who was able to to do that different way while I was down. To test a fix myself, I had to build for twelve hours just to see if something worked (I also work with 10+ year old computers a lot). I was on unstable, so I went weeks being unable to build because I couldn't downgrade to stable. I also learned that &lt;code&gt;nixpkgs&lt;/code&gt; had some patterns that were really hard to get into, like redefining &lt;code&gt;lua&lt;/code&gt; at the package level to mean a specific version of &lt;code&gt;lua&lt;/code&gt; and no one on the Matrix or Discourse forum was able to tell me that until I happened to find one person who explained it and said I should have just &amp;ldquo;known&amp;rdquo; about that mapping.&lt;/p&gt;
&lt;p&gt;And then, the same day I realized my efforts to get Ceph working were linked on the &lt;a href="https://nixos.wiki/wiki/Ceph"&gt;NixOS wiki for Ceph&lt;/a&gt;, I also saw a little line:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another distributed filesystem alternative you may evaluate is SeaweedFS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I vaguely remembered looking at SeaweedFS before, but I was focused on getting Ceph working. And in that moment, when Ceph was not building and I didn't have the resources to fix the problem myself, I decided to try it out.&lt;/p&gt;
&lt;p&gt;SeaweedFS does 80% of Ceph. It didn't have all the fancy features, but it had the features I wanted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distributed across multiple machines&lt;/li&gt;
&lt;li&gt;Ability to add or remove storage on the fly&lt;/li&gt;
&lt;li&gt;Variable replication copies&lt;/li&gt;
&lt;li&gt;Currently built and could be run&lt;/li&gt;
&lt;li&gt;Can be mounted on Linux&lt;/li&gt;
&lt;li&gt;It uses 30 GB volumes on standard &lt;code&gt;ext4&lt;/code&gt; partitions instead of a custom one I cannot debug&lt;/li&gt;
&lt;li&gt;Single Go executable (don't code Go but it only takes twenty minutes build, not twelve hours)&lt;/li&gt;
&lt;li&gt;Could create S3/cloud tier backup (Ceph couldn't do that)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yeah, it didn't have NixOS options but Google and GitHub gave me a starting point for me to puzzle it out on my own. I learned a new library and started with a little partition to see if it worked. Like Ceph, I had a lots of fits and starts, trials and puzzling through it, but eventually got it working.&lt;/p&gt;
&lt;p&gt;And it was more scattered in terms of information but it worked.&lt;/p&gt;
&lt;p&gt;It didn't do Ceph's &amp;ldquo;deep cleaning&amp;rdquo; to detect bit rot on failing drives.&lt;/p&gt;
&lt;p&gt;It blew up when you tried to create too many encoding shards without the space.&lt;/p&gt;
&lt;p&gt;It blew up when you tried to replicate without having enough nodes.&lt;/p&gt;
&lt;p&gt;Around that time, Ceph started building on NixOS again but I was already enamored by SeaweedFS. It was &amp;ldquo;good enough&amp;rdquo; for me. It took me about a week to migrate hunks of the Ceph data to the Seaweed, decommission a Ceph drive and make it a SeaweedFS drive. Then repeat until everything was moved over.&lt;/p&gt;
&lt;p&gt;A few weeks ago, I tried to consolidate my dad's hard drives into the cluster and ran out of space. I ended up buying a new minicomputer and throwing 11 TB worth of drives into it to give me room (two copies of everything, even the media files). This week, I lost one of those old Dells so I had to shuffle around the volumes with a few commands and it &amp;ldquo;just worked&amp;rdquo;. I'm going to replace the dead computer and I'm confident that it will &amp;ldquo;just work&amp;rdquo; then too. And, more importantly, I don't have to spend a week to figure out the commands to make it happen since I can just copy/paste a bunch of Nix code and redeploy my servers.&lt;/p&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;I could hoard less. It takes time and energy to keep the computers running but it makes sure Partner has their &lt;em&gt;Golden Girls&lt;/em&gt;, the kids have their videos, and I have my dad's artwork. Also, there is something peaceful about looking at a 29.9 TB partition and having it working smoothly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---------------------------------------------------------------------------------------------------------------+
| 1 fuse device                                                                                                 |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
| MOUNTED ON  |  SIZE |  USED | AVAIL |              USE%             | TYPE           | FILESYSTEM             |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
| /mnt/home   | 29.9T | 20.9T |  9.0T | [#############.......]  69.8% | fuse.seaweedfs | fs.home:8888:/         |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually this is all going to go away. When I die, my family isn't going to be able to keep it going. Like Rutejìmo's stories and my dad's artwork, all this will fade. But I'm going to keep it going as long as I can. And try to find more pages for my book.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Recovering SeaweedFS</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/10/25/recovering-seaweedfs/" />
    <updated>2024-10-25T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/10/25/recovering-seaweedfs/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="ceph" scheme="https://d.moonfire.us/tags/" label="Ceph" />
    <category term="seaweedfs" scheme="https://d.moonfire.us/tags/" label="SeaweedFS" />
    <category term="plex" scheme="https://d.moonfire.us/tags/" label="Plex" />
    <category term="tailscale" scheme="https://d.moonfire.us/tags/" label="Tailscale" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="backblaze" scheme="https://d.moonfire.us/tags/" label="Backblaze" />
    <summary type="html">I accidentally overfilled my SeaweedFS, here is how I recovered my cluster.
</summary>
    <content type="html">&lt;p&gt;Lately, I've been quite fond of &lt;a href="/tags/seaweedfs/"&gt;SeaweedFS&lt;/a&gt;. It isn't as powerful as &lt;a href="/tags/ceph/"&gt;Ceph&lt;/a&gt; but it considerably easier to maintain and manage. There are some tradeoffs, such as finding bit rotting (when the disks start to fail), but I find it not quite as &amp;ldquo;fragile&amp;rdquo; when it comes to using a random collection of Linux machines.&lt;/p&gt;
&lt;p&gt;One of the features I want to play with SeaweedFS is the ability to upload a directory transparently to a S3 bucket (not AWS though, they are too big). I'm thinking about that for later, when I want to make an extra, off-site back up critical files including Partner's photo shoots.&lt;/p&gt;
&lt;h2&gt;Overfilling&lt;/h2&gt;
&lt;p&gt;Last week, I worked on one of the tasks I've been stalling on: archiving my dad's artwork. He had a lot of copies of nearly identical files and I didn't have the working storage on my laptop. I figured I had this huge (22 TB, though mostly full) cluster, I could use that.&lt;/p&gt;
&lt;p&gt;Yeah&amp;hellip; not the best of ideas.&lt;/p&gt;
&lt;p&gt;I didn't realize I had made a mistake until everything started to fail because all of the nodes were at 98% or more full and the system couldn't replicate even the replication logs. I didn't even realize that until Partner said &lt;a href="/tags/plex/"&gt;Plex&lt;/a&gt; was down.&lt;/p&gt;
&lt;p&gt;Well, with replication down, I couldn't even use the weed shell to remove a file. When I did that, it just hung for hours.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ weed-shell
&amp;gt; rm -rf in/dad-pictures
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nix Shell Scripts&lt;/h2&gt;
&lt;p&gt;Above, I use &lt;code&gt;weed-shell&lt;/code&gt;. This is a custom script I generate with &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; that is installed in any server that can talk to my SeaweedFS.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs:
let
  shellScript = (
    pkgs.writeShellScriptBin &amp;quot;weed-shell&amp;quot; ''
      weed shell -filer fs.local:8888 -master fs.local:9333 &amp;quot;$@&amp;quot;
    ''
  );
in
{
  environment.systemPackages = [ shellScript ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lets me handle common functions I use when maintaining things. In this case, I don't have to enter the common parameters needed to talk to my SeaweedFS cluster.&lt;/p&gt;
&lt;h2&gt;Cleaning Up&lt;/h2&gt;
&lt;p&gt;I tried a bunch of things, such as trying to force a more extreme of vacuuming (cleaning deleted files):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;&amp;gt; volume.vacuum --help
Usage of volume.vacuum:
  -collection string
    	vacuum this collection
  -garbageThreshold float
    	vacuum when garbage is more than this limit (default 0.3)
  -volumeId uint
    	the volume id
&amp;gt; volume.vacuum -garbageThreshold 0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This didn't help as much as I hoped, but it did allow some replication and some commands to go through. I needed to clear up a lot more space so I could remove files properly and do a wholesale &lt;code&gt;rm -rf&lt;/code&gt; to blow away father's files and try again later once I get some more space.&lt;/p&gt;
&lt;h2&gt;Replication&lt;/h2&gt;
&lt;p&gt;I have my volumes set to &lt;code&gt;010&lt;/code&gt; replication. These are three numbers as data center, rack, and host.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data Center: Make X copies to replicate across multiple data centers. I don't have multiple data centers, so this is always &lt;code&gt;0&lt;/code&gt; for me.&lt;/li&gt;
&lt;li&gt;Rack: This is to replicate across multiple racks. My setup is that each computer is a &amp;ldquo;rack&amp;rdquo; so my &lt;code&gt;1&lt;/code&gt; replication means make an extra copy on a different machine. I treat this as a rack because I also have a DeskPi Super6C which is 6 Raspberry Pi CM4 (compute modules) in a single case, so I treat all six as a &amp;ldquo;rack&amp;rdquo; but with separate hosts.&lt;/li&gt;
&lt;li&gt;Host: The same machine. I don't have much use for two copies on the same machine, so I always set this to &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I ever got a friend where I could set up a local server, I would consider setting up a second &amp;ldquo;data center&amp;rdquo; to have an off-site backup. That probably would require &lt;a href="/tags/tailscale/"&gt;Tailscale&lt;/a&gt; but that's beyond my current scope.&lt;/p&gt;
&lt;h2&gt;Volumes&lt;/h2&gt;
&lt;p&gt;SeaweedFS basically creates multiple 30 GB files which act as a blob with multiple files inside it. That way, the problems with thousands of small files aren't an issue since everything is done on the 30 GB files called &amp;ldquo;volumes&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Replication is done at the volume level, which means I was able to turn off replication for a series of volumes.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;&amp;gt; lock
&amp;gt; volume.configure.replication -replication 000 -volumeId 1
&amp;gt; volume.configure.replication -replication 000 -volumeId 2
&amp;gt; volume.fix.replication
&amp;gt; volume.balance -force
&amp;gt; unlock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;lock&lt;/code&gt; and &lt;code&gt;unlock&lt;/code&gt; are important when making changes like this, they prevent some critical operations from corrupting the cluster. The commands will tell you when it is needed.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;volume.configure.replication&lt;/code&gt; basically changed those volumes to no replication (risky). Once that is done, &lt;code&gt;volume.fix.replication&lt;/code&gt; and &lt;code&gt;volume.balance -force&lt;/code&gt; deletes the excessive copies and shuffle things around, giving me some breathing room to get replication running again so I can mass delete files.&lt;/p&gt;
&lt;p&gt;When I'm done, I just go and change all the nodes back to &lt;code&gt;-replication 010&lt;/code&gt; to give me the second backup.&lt;/p&gt;
&lt;h2&gt;Data Hoarding&lt;/h2&gt;
&lt;p&gt;The problem ultimate is data hoarding. Both my father and I both have multiple copies of files running around. It isn't great, but when you don't have time to clean out a copy of a laptop dying, it is sometimes easier to &lt;code&gt;rsync&lt;/code&gt; the entire laptop into a directory of the new machine and then move on.&lt;/p&gt;
&lt;p&gt;In this case, I needed to do some trimming of the duplicates from his files. The script is based on the one from a &lt;a href="https://stackoverflow.com/a/19552048"&gt;StackOverflow answer&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;find . -not -empty -type f -printf &amp;quot;%s\n&amp;quot; \
    | sort -rn \
    | uniq -d \
    | xargs -I{} -n1 find . -type f -size {}c -print0 \
    | xargs -0 sha256sum \
    | sort \
    | uniq -w32 --all-repeated=separate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is pretty simple because it only lists duplicates and the paths to find them.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ echo one &amp;gt; a.txt
$ echo one &amp;gt; b.txt
$ echo two &amp;gt; c.txt
$ echo two &amp;gt; d.txt
$ echo three &amp;gt; e.txt
$ find . -not -empty -type f -printf &amp;quot;%s\n&amp;quot; \
    | sort -rn \
    | uniq -d \
    | xargs -I{} -n1 find . -type f -size {}c -print0 \
    | xargs -0 sha256sum \
    | sort \
    | uniq -w32 --all-repeated=separate
27dd8ed44a83ff94d557f9fd0412ed5a8cbca69ea04922d88c01184a07300a5a  ./c.txt
27dd8ed44a83ff94d557f9fd0412ed5a8cbca69ea04922d88c01184a07300a5a  ./d.txt

2c8b08da5ce60398e1f19af0e5dccc744df274b826abe585eaba68c525434806  ./a.txt
2c8b08da5ce60398e1f19af0e5dccc744df274b826abe585eaba68c525434806  ./b.txt
$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, I can find the duplicates in my own system and delete them to clear out a few terabytes worth of data. It takes time, but I haven't done it before so I pointed it at &lt;code&gt;/mnt/seaweed&lt;/code&gt; and let it run.&lt;/p&gt;
&lt;p&gt;Once that is done, I can turn replication back on, fix replication, rebalance, and I should be good to go.&lt;/p&gt;
&lt;h2&gt;Forward Steps&lt;/h2&gt;
&lt;p&gt;I knew I was running out of storage for a while now, so I blew my monthly budget and got the fourth server ordered. This one has three 4 TB NVMe sticks (about 11 TB which will be added to the cluster) and should give me enough room to get my dad's files collected, deduplicated, and then look into uploading them to a cheap S3 storage (&lt;a href="/tags/backblaze/"&gt;Backblaze&lt;/a&gt;) for later.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Bakfu and OCI Containers</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/09/27/bakfu-and-oci-containers/" />
    <updated>2024-09-27T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/09/27/bakfu-and-oci-containers/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <category term="mfgames-culture" scheme="https://d.moonfire.us/tags/" label="MfGames.Culture" />
    <category term="author-intrusion" scheme="https://d.moonfire.us/tags/" label="Author Intrusion" />
    <category term="semantic-version" scheme="https://d.moonfire.us/tags/" label="Semantic Version" />
    <category term="second-hand-dresses" scheme="https://d.moonfire.us/tags/" label="Second-Hand Dresses" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="wasm" scheme="https://d.moonfire.us/tags/" label="WASM" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="csharp-mode" scheme="https://d.moonfire.us/tags/" label="csharp-mode" />
    <category term="emacs" scheme="https://d.moonfire.us/tags/" label="Emacs" />
    <category term="mintest" scheme="https://d.moonfire.us/tags/" label="Mintest" />
    <summary type="html">Thoughts on scope creep, OCI containers, and why I still need Bakfu for package management.
</summary>
    <content type="html">&lt;p&gt;I'm giving myself a week to focus on something that I've been trying to do for a &lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;while&lt;/a&gt;, come up with a generic package management library. This of course brings me to the first thought: don't. Actually &lt;a href="https://medium.com/@sdboyer/so-you-want-to-write-a-package-manager-4ae9c17d9527"&gt;someone else&lt;/a&gt; said it better and with a lot more detail why this is a terrible idea. There are many other blog posts and warnings out there.&lt;/p&gt;
&lt;p&gt;And yet I'm still going forward with writing a package management library: &lt;a href="/tags/bakfu/"&gt;Bakfu&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Purpose&lt;/h2&gt;
&lt;p&gt;The main reason is that I see a need to have package management in a number of projects that are on my plate or I would like to be on my plate. While I could write a dedicated one for each one, I'm loathe to spend that much effort when I can spent far more effort coming up with something generic to separate the various concerns. Ultimate, this is the &lt;a href="https://xkcd.com/974/"&gt;XKCD's General Problem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The projects I have in mind for needing a plugin infrastructure are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="/tags/mfgames-culture/"&gt;MfGames.Culture&lt;/a&gt;: My library for handling arbitrary calendars&lt;/li&gt;
&lt;li&gt;&lt;a href="/tags/author-intrusion/"&gt;Author Intrusion&lt;/a&gt;: My CLI-based tools for working with writing&lt;/li&gt;
&lt;li&gt;Some vague game ideas in my head for mod support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be honest, Author Intrusion has stalled out years ago, but I keep wanting to write it. The culture library, on the other hand, is on my mind because it is the largest identifiable &amp;ldquo;rock&amp;rdquo; causing my writer's block. I haven't been able to edit &lt;a href="/tags/second-hand-dresses/"&gt;Second-Hand Dresses&lt;/a&gt; because I got stuck on the calendar of events. It's a minor thing, but I really liked it when &lt;a href="//fedran.com"&gt;Fedran&lt;/a&gt; had the master timeline and per-character timelines also.&lt;/p&gt;
&lt;h2&gt;MfGames Culture&lt;/h2&gt;
&lt;p&gt;My current thoughts for the culture library is to implement the calendars as &lt;a href="/tags/wasm/"&gt;WASM&lt;/a&gt; plugins that let me take the calendar components (e.g., Gregorian) and merge it with the cultural elements (English, French, etc) and allow the conversion between them. If I can, I'd implement each of my &lt;a href="//fedran.com/calendars/"&gt;calendars&lt;/a&gt; as a separate plugin and let me evolve them. (There are a number of more, plus I use the Japanese &amp;ldquo;small seasons&amp;rdquo; in my world and a rough time of day also.)&lt;/p&gt;
&lt;p&gt;Many of my chapters have a date associated with them. This was to help me identify the dates when certain events happened, but also to make sure a character has enough time to get from one place to the other (given that I don't have a map of the world yet).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;---
when:
  start: 1471/3/28 MTR 4::22
  duration: 0::30
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My original use was to parse the date (&lt;code&gt;1471/3/28 MTR 4::22&lt;/code&gt;) into a common format (Julian Days or JD), then use that value to calculate the date in all the calendars I use. This worked out nicely since I could create a tooltip that showed the date on the website.&lt;/p&gt;
&lt;p&gt;The original culture library was written in Typescript, which worked fine when I was generating my website with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt;, but now that I'm using &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;, which is in C#, that is less useful, hence the rewrite since I dislike opening a pipeline to a CLI to generate websites (so slow&amp;hellip;).&lt;/p&gt;
&lt;p&gt;But the main part is that I don't want to package all the calendars and cultures in a single file. I want to be able to make one just for Fedran, one that covers US information, or allow someone to pick the minimum set they need for a given project. That leans toward a plugin infrastructure where I can have a file like &lt;code&gt;package.json&lt;/code&gt; that lists the calendars needed for &lt;em&gt;that&lt;/em&gt; project.&lt;/p&gt;
&lt;h2&gt;Reevaluation&lt;/h2&gt;
&lt;p&gt;The eighteen months I've been working on the ideas, mostly trying out a few things and failing a lot. I started a Rust implementation, switch to C# to prototype faster (as a more comfortable language for me). But I want to finish this and move on, so that is why I'm spending this time to hopefully get a working package manager library.&lt;/p&gt;
&lt;p&gt;I hadn't reviewed my goals in a while and I was suffering with scope creep.&lt;/p&gt;
&lt;p&gt;I realized that this morning when I read the article &lt;a href="https://opensource.microsoft.com/blog/2024/09/25/distributing-webassembly-components-using-oci-registries/"&gt;Distributing WebAssembly components using OCI registries&lt;/a&gt;. In specific, I was reading it hoping that the &lt;span class="missing-link" data-path="/tags/oci-containers/"&gt;OCI Containers&lt;/span&gt; would handle the problem and I could abandon my project entirely (the best goal, to be honest). To be honest, WASM OCI Containers are perfect for my plans for MfGames.Culture, but there is something missing.&lt;/p&gt;
&lt;p&gt;That is when I realized I was trying to figure out a container format and also figure out an abstracted dependency system.&lt;/p&gt;
&lt;h2&gt;Containers&lt;/h2&gt;
&lt;p&gt;The container format isn't really the important part even though I thought itw as. There are already good formats out there: OCI Containers, zip archives, NuGet packages.&lt;/p&gt;
&lt;p&gt;With OCI Containers (specifically Docker images), a developer would curate a set of containers and compose something, but they don't worry too much about updating them or having them move around. Yeah, they could update their tag to &lt;code&gt;:latest&lt;/code&gt;, but that is fraught with troubles if my experience updating with the &lt;a href="https://hub.docker.com/_/nextcloud"&gt;Docker version of Nextcloud&lt;/a&gt; was any indication. Too many times having to walk through the updates, one major version at a time with occasionally manually updating.&lt;/p&gt;
&lt;p&gt;The composition is also part of the problem. Composing services is awesome for packages, but I would consider a rather technical task. As opposed to someone going into a list of game mods, checking a bunch, and then expecting it to run. Getting that list of mods (which could be OCI containers) isn't provided in a generic manner, including the base libraries (such as UI injection library or a parsing framework) as needed, and then making it work isn't in the scope of OCI Containers. As far as I can tell, it isn't in the scope of anything at a generic level. Everyone seems to reinvent the wheel, or retrofits an existing wheel like JetBrains did with their module system (from my understanding).&lt;/p&gt;
&lt;h2&gt;Abstraction&lt;/h2&gt;
&lt;p&gt;Abstraction is something important to me. Libraries evolve, not just from refactoring code or being built on a different pattern, but how they are managed. Maintainers come and go. Sites come and go, though the development world would be in chaos if GitHub ever went away.&lt;/p&gt;
&lt;p&gt;But, at one point, SourceForge was thought to be the same. And now, it is barely there as nothing more than a download repository with excessive advertising.&lt;/p&gt;
&lt;p&gt;Also, the project owners change. Sometimes they just throw up their hands and delete everything. Sometimes, they just retire. A good example is me. A long time ago, I used to maintain &lt;a href="/tags/csharp-mode/"&gt;csharp-mode&lt;/a&gt;, the C# mode for &lt;a href="/tags/emacs/"&gt;Emacs&lt;/a&gt;. After a while, I got burned out trying to implement generics and wandered off; someone else offered to take the reins and I happily gave it to them.&lt;/p&gt;
&lt;p&gt;Another example is the friction that comes when one part of the organization is moving slower than the other. I think &lt;a href="https://www.bouncycastle.org/"&gt;BouncyCastle&lt;/a&gt; is an example of that. There are three libraries that do roughly the same thing, but each one is different based on their platform (.NET Framework verses .NET Core), package name (BouncyCastle verses Portable.BouncyCastle), and evolution (BouncyCastle.Cryptography is the latest). But in this case, I can usually manually change all references from &lt;code&gt;BouncyCastle&lt;/code&gt; or &lt;code&gt;Portable.BouncyCastle&lt;/code&gt; to &lt;code&gt;BouncyCastle.Cryptography&lt;/code&gt; and have it work.&lt;/p&gt;
&lt;p&gt;In the above case, it would be a lot easier if I could say all references to &lt;code&gt;BouncyCastle&lt;/code&gt; or &lt;code&gt;Portable.BouncyCastle&lt;/code&gt; could be addressed to &lt;code&gt;BouncyCastle.Cryptography&lt;/code&gt; and not have to manually edit older packages that don't know about the new library.&lt;/p&gt;
&lt;p&gt;There is also remixes and forks. In &lt;span class="missing-link" data-path="/tags/minetest/"&gt;Minetest&lt;/span&gt;, there are numerous &amp;ldquo;beds&amp;rdquo; plugins that don't play with each other but are dependencies of others. In many cases, I just have to pick one of them.&lt;/p&gt;
&lt;p&gt;It isn't only just the package names that need to be replaced. Many modern languages allow you to pull in a package from a Git repository (if I'm lucky, it doesn't have to be GitHub). NPM allows it, Rust does, I'm pretty sure Go also does. This is great because a developer can pin a specific hash to the version.&lt;/p&gt;
&lt;p&gt;But that is also fragile. What happens if the developer deletes the repository? Or changes their name. Suddendly &lt;code&gt;dmoonfire/some-project&lt;/code&gt; doesn't work but the person using that library isn't maintaining their project either, so I have to figure out how to get a second- or three-degree reference to point to somewhere new like my local copy. This is basically the same problem as the BouncyCastle example above.&lt;/p&gt;
&lt;p&gt;There is no abstraction, which is what I think is important. I want something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PackageA depends on PackageB&lt;/li&gt;
&lt;li&gt;PackageC provides/substitutes as PackageB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That way, if &amp;ldquo;PackageB&amp;rdquo; in the example goes away, then &amp;ldquo;PackageC&amp;rdquo; can takes it place without having to rebuild everything. In a more concrete example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyProject depends on BouncyCastle&lt;/li&gt;
&lt;li&gt;Portable.BouncyCastle substitutes BouncyCastle&lt;/li&gt;
&lt;li&gt;BouncyCastle.Cryptography substitutes as BouncyCastle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For locations, the same thing, I want to be able to say that a specific package can be found &amp;ldquo;here&amp;rdquo; or &amp;ldquo;here&amp;rdquo; or &amp;ldquo;here&amp;rdquo;.&lt;/p&gt;
&lt;h2&gt;Annotation&lt;/h2&gt;
&lt;p&gt;Security vulnerabilities are a still a big thing for me. I noticed NuGet is starting to download a list of vulnerabilities and report them in Visual Studio.&lt;/p&gt;
&lt;p&gt;I want to expand that concept into something more generic, like Bluesky has with their &lt;a href="https://docs.bsky.app/blog/blueskys-moderation-architecture"&gt;distributed moderation&lt;/a&gt;. This is also something I'd love to see with Fediverse servers.&lt;/p&gt;
&lt;p&gt;Being able to subscribe to a service that validates libraries and looks for hacks or does code auditing (and gets paid for it) is something I think this world needs. I could also see it as a local project that prevents certain packages from being viewed (like our problem being unable to move past &lt;code&gt;netstandard2.0&lt;/code&gt; but a minor version jumped to &lt;code&gt;netstandard2.1&lt;/code&gt;). Or a curated list of assemblies for a project that is checked at the project root.&lt;/p&gt;
&lt;p&gt;I use &lt;code&gt;EasyQuery&lt;/code&gt; as an example. I love the library and what it does for my day job, but they use the minor version for breaking changes but there is nothing in NPM that tells me that. So I have a wiki page to developers to tell them, but an annotation service could say &amp;ldquo;breaking changes on minor versions&amp;rdquo; (they aren't the only one who doesn't honor &lt;a href="/tags/semantic-version/"&gt;sematic versioning&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Or also &amp;ldquo;here is a local cache of the package.&amp;rdquo;&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This is just a rehash of what I've said previously, but for my needs. I need avoid scope creep and focus on what I need if I have any hope of finishing.&lt;/p&gt;
&lt;p&gt;Obviously, finding that someone else has solved these problems would be awesome. Then I could abandon this project. Until then, I'm going to plug at it and see if I can get something minimal that works for getting me my calendars back.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Generating and Using Identifiers (Part 2)</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/06/29/identifiers-update/" />
    <updated>2024-06-29T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/06/29/identifiers-update/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="uuidv7" scheme="https://d.moonfire.us/tags/" label="UUIDv7" />
    <category term="identifiers" scheme="https://d.moonfire.us/tags/" label="Identifiers" />
    <summary type="html">A few expanded ideas about identifiers after discussing it with others.
</summary>
    <content type="html">&lt;p&gt;Last month, I did a post about &lt;a href="/blog/2024/05/30/identifiers/"&gt;snowflake and UUIDv7 identifiers&lt;/a&gt;. I was pretty happy with it, but then I was playing around with &lt;a href="https://clew.se/"&gt;Clew&lt;/a&gt; which is a recent, smaller web search engine and decided to look up my identifers post just to check it out.&lt;/p&gt;
&lt;p&gt;Right underneath mine was a post by &lt;a href="https://www.unkey.com/blog/uuid-ux"&gt;Unkey&lt;/a&gt; on the same topic. It has some good observations that I didn't think about when I wrote mine, so decided to expand on mine to include the ideas I like.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/05/30/identifiers/"&gt;2024-05-30 Generating and Using Identifiers&lt;/a&gt; - A little discussion on unique identifiers, including formatting and presentation. A small database lesson on page splitting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/06/29/identifiers-update/"&gt;2024-06-29 Generating and Using Identifiers (Part 2)&lt;/a&gt; - A few expanded ideas about identifiers after discussing it with others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2026/01/04/other-smarter-people/"&gt;2026-01-04 Other Smarter People&lt;/a&gt; - I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&amp;ldquo;Copying UUIDs is annoying&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Yes, but I didn't think about it. Both UUIDs (including my current crush on v7) and my preference for the Crockford encoding both have the same problem with double-clicking to copy, because of the dashes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;01-hz61-3s22-k8nr-6w6g-rv1y-5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I still feel that removing the separators turns it into a long series of numbers that is difficult to parse and elide in a consistent manner, so just removing the separators isn't an option. However, underscores don't have the same behavior (and the Unkey uses them later for the prefix discussion).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;01_hz61_3s22_k8nr_6w6g_rv1y_5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don't normally think about underscores since they conflict with underlines for links, but in this case, the identifier should be treated as a whole, so favoring the double-click and single word behavior is a major benefit.&lt;/p&gt;
&lt;h2&gt;Groups of Four&lt;/h2&gt;
&lt;p&gt;This isn't from the Unkey post, but with underscores, I think the groups of four is excessive. I don't know why, but the lower bar somehow makes it more obvious. Plus, how often do you need to hand transcribe or read out loud the numbers.&lt;/p&gt;
&lt;p&gt;I think splitting the identifiers into groups of eight would be sufficient, as long as they are consistently eight instead of UUIDs default formatting of 8-4-4-4-12 which is difficult for me.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I still think groups of eight (from the right) is still workable, has a natural break point for eliding (&amp;ldquo;rv1y5c52&amp;rdquo; in the above example) and doesn't need quite as much space.&lt;/p&gt;
&lt;h2&gt;&amp;ldquo;Prefixing&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;I can't believe I forgot about prefixes. When Github and Gitlab both started adding prefixes to their API keys, I thought it was a great idea. Then, when I went to Gitea and later to Foregjo, seeing just a bunch of hex characters for an API key felt subtly &amp;ldquo;wrong&amp;rdquo; to me.&lt;/p&gt;
&lt;p&gt;The Unkey post pointed out, purpose. A bunch of numbers is one thing, but knowing the purpose of the key helps identify it as a secret (e.g., something not to check into code) and also possibly give a way of preventing usage.&lt;/p&gt;
&lt;p&gt;Naturally, since we want to treat an identifier, it should be separated by an underscore from the rest of the key. Also it solves a problem when the first character of the number is a number, it is an invalid code identifier. Not that someone is going to say:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-typescript"&gt;const 01_hz613s22_k8nr6w6g_rv1y5c52 = getIdentifier();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, it fits better into the &amp;ldquo;idea&amp;rdquo; of code to treat it as one. So, starting the prefix with a non-number would make this more useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;player_01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Separation&lt;/h3&gt;
&lt;p&gt;While thinking about it, I considered making a distinction between the identifier and the code with a double underscore.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bedor_player__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Originally, I didn't think it made sense because we don't really want to parse the identifier to pull out the prefix from the code itself. That said, there is one situation where we need to be able to parse: eliding.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Just breaking off the last group would result in stripping off the contextual prefix. We want to keep that information even when shortened, which means we have to have a mechanical way of identifying the prefix to determine how to break it apart.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bedor_player__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;li&gt;bedor_player__rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The double underscore also means that it remains a single word for purposes of selection.&lt;/p&gt;
&lt;h3&gt;Global Prefixes&lt;/h3&gt;
&lt;p&gt;Having multiple words does make sense to me, so &amp;ldquo;bedor_player&amp;rdquo; seems reasonable to have since it can be arbitrarily long. That said, the identifiers already produce a &amp;ldquo;globally unique&amp;rdquo; value, so the prefix also doesn't have to be global. That means we just need enough scope information for the producer of the identifier, but not a full scope like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com_mfgames_bedor_player__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good examples would be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;forgejo__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;li&gt;f__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;li&gt;api_prod__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;li&gt;ap__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case, shorter is better.&lt;/p&gt;
&lt;h3&gt;Suffixes&lt;/h3&gt;
&lt;p&gt;That led into the question about suffixes. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;api_prod__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/li&gt;
&lt;li&gt;api__01_hz613s22_k8nr6w6g_rv1y5c52__prod&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main reason I don't think suffixes make sense is that I believe most people look at the beginning and the end of variables in most cases. And, having a textual prefix and suffix adds more complexity since both have to be looked at to understand the scope. Having only a prefix means the contextual information is always on the same side of the code.&lt;/p&gt;
&lt;h2&gt;Eliding&lt;/h2&gt;
&lt;p&gt;Eliding (abbreviating) is important when dealing with large identifiers. Git allows you to use an arbitrary number for reducing the hashes to identify a Git, but I remember trying to figure out if six or eight was the best for some purpose. With the grouping above, there is a natural break at eight and sixteen.&lt;/p&gt;
&lt;p&gt;I realized this really is only a concern if there are smaller numbers were most of the identifier is zero such as &lt;code&gt;00000000-0000-0000-0000-000000000000&lt;/code&gt; which has a Crockford encoding of &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;api__0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, for most UUIDv7 identifiers, this isn't possible because the first 48-bits are a timestamp and won't ever be zero (again) after that epoch millisecond.&lt;/p&gt;
&lt;p&gt;But, there is a known problem with eliding: there is a higher level of collisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;01_hz613s22_k8nr6w6g_rv1y5c52 is &amp;ldquo;rv1y5c52&amp;rdquo;&lt;/li&gt;
&lt;li&gt;01_k8nr6w6g_hz613s22_rv1y5c52 is &amp;ldquo;rv1y5c52&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In these cases, there needs to be a check for duplicates as part of the code. That is the cost of being able to elide, additional complexity when selecting.&lt;/p&gt;
&lt;p&gt;I will have to mention, eliding is a &amp;ldquo;human&amp;rdquo; thing, not a protocol thing. We wouldn't be sending up an elided identifier as part of a HTTP header or an authorization key. We need eliding for showing a table of all known identifiers so someone can see additional information or delete the key. That said, in a grid, you probably don't need the prefix, but the double underscores makes it possible to have it just in case you mix your keys in the same grid.&lt;/p&gt;
&lt;h2&gt;&amp;ldquo;Specification&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;There isn't a formal specification, but a current summary of my wandering thoughts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate a 128-bit UUIDv7&lt;/li&gt;
&lt;li&gt;Encode the 128-bit number into lowercase Crockford32&lt;/li&gt;
&lt;li&gt;Put underscores every eight characters starting at the right&lt;/li&gt;
&lt;li&gt;If eliding, remove the left-most groups until the desired length&lt;/li&gt;
&lt;li&gt;Add an optional prefix that does not start with a number with a &amp;ldquo;__&amp;rdquo; separator&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I'm still happy having a framework for this, though I could see extending some of my favorite C# strong typing library: &lt;a href="https://github.com/andrewlock/StronglyTypedId"&gt;StronglyTypedId&lt;/a&gt; to support the format and prefixes. I mean, it would be nice to be able to say:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System;
using StronglyTypedIds;

[StronglyTypedId(Template.Crockford, Prefix=&amp;quot;a&amp;quot;)]
public partial struct ApiId { }

var id = new ApiId(Guid7.NewGuid());

Console.WriteLine(id);
// a__01_hz613s22_k8nr6w6g_rv1y5c52

Console.WriteLine(id.Elide(1));
// a__rv1y5c52
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully, I've worked out most of the kinks I've found. I might come back, or I might formalize this into libraries. Either way, I think it is a workable pattern for the difficulties I've experience in the past.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Generating and Using Identifiers</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/05/30/identifiers/" />
    <updated>2024-05-30T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/05/30/identifiers/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="snowflake-id" scheme="https://d.moonfire.us/tags/" label="Snowflake ID" />
    <category term="uuidv7" scheme="https://d.moonfire.us/tags/" label="UUIDv7" />
    <category term="identifiers" scheme="https://d.moonfire.us/tags/" label="Identifiers" />
    <summary type="html">A little discussion on unique identifiers, including formatting and presentation. A small database lesson on page splitting.
</summary>
    <content type="html">&lt;p&gt;Over the last few months, I've had a number of conversations about unique identifiers. For starters, I get pulled into a periodic meeting to justify decisions we made about six years ago for my day job. Then there was a conversation on &lt;a href="https://octodon.social/@dmoonfire"&gt;fedi&lt;/a&gt; (not a link to the conversation, it was too long ago and I auto-delete my posts).&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/05/30/identifiers/"&gt;2024-05-30 Generating and Using Identifiers&lt;/a&gt; - A little discussion on unique identifiers, including formatting and presentation. A small database lesson on page splitting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/06/29/identifiers-update/"&gt;2024-06-29 Generating and Using Identifiers (Part 2)&lt;/a&gt; - A few expanded ideas about identifiers after discussing it with others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2026/01/04/other-smarter-people/"&gt;2026-01-04 Other Smarter People&lt;/a&gt; - I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;So, identifiers. Almost everything in the database needs a way of unique identifying a record. Composite keys are nice and precise but a PITA to manage, which suggests a single field is much better.&lt;/p&gt;
&lt;p&gt;For a long time, an &lt;code&gt;int&lt;/code&gt; (32-bit) was &amp;ldquo;good enough&amp;rdquo; because it easily handled ~2e9 (2,147,483,647) records (most database I've worked with don't give you unsigned integers and negatives are horrible to work with). Slap your favorite database's way of creating a unique number with every record (sometimes called &lt;code&gt;IDENTITY&lt;/code&gt;) and then hope everything works until you leave the project.&lt;/p&gt;
&lt;p&gt;That doesn't always happen.&lt;/p&gt;
&lt;p&gt;At many of my jobs, I've had to deal with millions (and occasionally billions) of rows of data. My current one, which is a multi-tenant database, we have customers with 10-20 million records each for the primary tables, easily a 3x or 10x that many on the secondary tables, and adding 100k records every day. That makes hitting the 2e9 limit uncomfortably probable since we're expected to be functional for at least three decades (imagine every gift card that is created and how each one has to be tracked for 3-5 years before it is filed with the state).&lt;/p&gt;
&lt;p&gt;To handle that, we took the effort to bump up the key fields from &lt;code&gt;int&lt;/code&gt; to &lt;code&gt;bigint&lt;/code&gt; (64-bit, &lt;code&gt;long&lt;/code&gt; for C# folks). This gave us plenty of breathing room since now we can handle more records than we will ever have inserted into the database.&lt;/p&gt;
&lt;h2&gt;Horizontal Scaling&lt;/h2&gt;
&lt;p&gt;Automatically generating identifiers works great with a single database, but what happens when you have two, three, or even a hundred? Some multi-tenant applications get around this by having one database per customer, but that increases the maintenance load to manage a thousand or more databases. That increases update times and requires our DBA to watch everything for failures.&lt;/p&gt;
&lt;p&gt;When the identifiers have to be shared, they need to be unique otherwise they aren't very good identifiers. In our case, we have a number of servers that are all capable of inserting rows and we don't want to funnel every row creation through a single database server to make sure they are unique (this called &amp;ldquo;single point of failure&amp;rdquo;). Instead, we want to make sure each one can create a unique identifier with some authority and be confident that another server isn't going to steal the same identifier.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I'm glossing over a lot of the specifics because this blog is focusing on a specific methodology of creating unique identifiers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There are &lt;em&gt;many&lt;/em&gt; techniques for having database servers create non-overlapping identifiers. We have sequences, identities in certain ranges, and lookup tables. The problem I find with most of these is that they require a certain amount of manual configuration and tweaking. Adding a new server causes everything to rebalance the possible generated IDs, or smaller &amp;ldquo;blocks&amp;rdquo; of identifiers are assigned and there is a risk of starving out one active server because it generates identifiers at eighteen times the rate of the others (this happened to me, the east coast server was answering 80% of the the load).&lt;/p&gt;
&lt;p&gt;Ideally, we want a setup that lets us toss together a new server, start it up, and then have it insert itself into the process smoothly without disrupting the others.&lt;/p&gt;
&lt;h2&gt;Conventions&lt;/h2&gt;
&lt;p&gt;For the rest of this post, I'm going to use the following conventions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; means a random number, ideally cryptographically strong random&lt;/li&gt;
&lt;li&gt;&lt;code&gt;t&lt;/code&gt; for bits related to a timestamp&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt; for device or worker&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; serial number, starting at 0 and incrementing by one&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt; the negative bit&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4&lt;/code&gt; the number 4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Enter UUIDs v4 (related GUIDs)&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier"&gt;UUIDs&lt;/a&gt; are awesome and horrible in the same way. On one hand, they are fantastic for creating non-overlapping identity keys that are highly unlikely to have a collision. They are also 128-bit numbers which gives them a &amp;ldquo;huge&amp;rdquo; amount of space to work with.&lt;/p&gt;
&lt;p&gt;The most common UUID I've deal with is the v4 which is entirely random except for a &amp;ldquo;4&amp;rdquo; jammed into the beginning of the third block.&lt;/p&gt;
&lt;p&gt;In hex:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rrrrrrrr-rrrr-4rrr-rrrr-rrrrrrrrrrrr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at all those randoms. For a v4, there are 122 bits of random which means 5.3e36 possible numbers which makes it difficult to get a collision. I have never seen one in the wild, but that doesn't mean it is impossible. Just highly unlikely.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A GUID is the same as UUID, but with slightly different byte order.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Up until this point, most of the code I've dealt with has been creating v4 UUIDs. Microsoft recommended them, there is almost no chance to have a conflict, and they ensure a nice random coverage of identifiers.&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;Database hate them. The problem is that they are &amp;ldquo;random&amp;rdquo; which means when the database builds up an index of the rows, it wants to put everything in a nice neat order so it is easier to find things. When the database can find something quickly, then the user doesn't complain that it takes two seconds to open up a page.&lt;/p&gt;
&lt;p&gt;The reason is that most relational databases (Postgresql, MariaDB, SQL Server) arranges data into &amp;ldquo;pages&amp;rdquo;, which are blocks of data. There are data pages (where the row data is located) and index pages (where the index is located). When the DB server inserts a row, it will arrange the data pages in primary key order (you can change with a &amp;ldquo;clustered&amp;rdquo; index).&lt;/p&gt;
&lt;p&gt;Let's use this as an example, assuming that we can get four rows on a single page.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right;"&gt;Page&lt;/th&gt;
&lt;th&gt;ProjectId&lt;/th&gt;
&lt;th&gt;ProjectTitle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;0d5a76af-8a9f-460b-8e00-bcb23d8e4d57&lt;/td&gt;
&lt;td&gt;Flight of the Scions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;148dc206-d9bd-464a-a5b9-8ba49856fd46&lt;/td&gt;
&lt;td&gt;Sand and Bone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;4aaab767-4571-42f3-9b2f-4d1e69c73e23&lt;/td&gt;
&lt;td&gt;Sand and Blood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;69b46634-46dc-452b-a4d8-468403310009&lt;/td&gt;
&lt;td&gt;Sand and Ash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;a2fbaa43-52da-484c-9dc2-3352b4b0aa8f&lt;/td&gt;
&lt;td&gt;Second-Hand Dresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;ce0db656-97a5-4b06-85e1-173074b96950&lt;/td&gt;
&lt;td&gt;Allegro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;e8af805b-bb8c-4adb-98a6-7fa9c4a86bb8&lt;/td&gt;
&lt;td&gt;Nor Curse Be Found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;f991f021-8e83-4f23-8c75-ed4ba0953915&lt;/td&gt;
&lt;td&gt;Pack Daughter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If we insert a new row, such as &amp;ldquo;5e187e66-25bb-4e5b-ae4c-07e986cb92fa&amp;rdquo; for &lt;em&gt;Son of Vo&lt;/em&gt;, we have to insert it between &lt;em&gt;Sand and Blood&lt;/em&gt; and &lt;em&gt;Sand and Ash&lt;/em&gt;, but since we already have two filled pages, we have to split the first page by creating a new page, inserting it between pages one and two, and then linking them together (basically turn the old page two into three, pages are a linked list). Then, then new row is added to one of them (in this case, the least full one).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right;"&gt;Page&lt;/th&gt;
&lt;th&gt;ProjectId&lt;/th&gt;
&lt;th&gt;ProjectTitle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;0d5a76af-8a9f-460b-8e00-bcb23d8e4d57&lt;/td&gt;
&lt;td&gt;Flight of the Scions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;148dc206-d9bd-464a-a5b9-8ba49856fd46&lt;/td&gt;
&lt;td&gt;Sand and Bone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;1&lt;/td&gt;
&lt;td&gt;4aaab767-4571-42f3-9b2f-4d1e69c73e23&lt;/td&gt;
&lt;td&gt;Sand and Blood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;5e187e66-25bb-4e5b-ae4c-07e986cb92fa&lt;/td&gt;
&lt;td&gt;Son of Vo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;2&lt;/td&gt;
&lt;td&gt;69b46634-46dc-452b-a4d8-468403310009&lt;/td&gt;
&lt;td&gt;Sand and Ash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;3&lt;/td&gt;
&lt;td&gt;a2fbaa43-52da-484c-9dc2-3352b4b0aa8f&lt;/td&gt;
&lt;td&gt;Second-Hand Dresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;3&lt;/td&gt;
&lt;td&gt;ce0db656-97a5-4b06-85e1-173074b96950&lt;/td&gt;
&lt;td&gt;Allegro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;3&lt;/td&gt;
&lt;td&gt;e8af805b-bb8c-4adb-98a6-7fa9c4a86bb8&lt;/td&gt;
&lt;td&gt;Nor Curse Be Found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right;"&gt;3&lt;/td&gt;
&lt;td&gt;f991f021-8e83-4f23-8c75-ed4ba0953915&lt;/td&gt;
&lt;td&gt;Pack Daughter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Now imagine doing this a million times. Millions of jamming rows into gaps and splitting when there isn't room. By the end, you'll have all these empty spaces because re-indexing (rebuilding the data pages so they are nice and packed) is an expensive operation so it isn't done automatically. Deleting rows will also create spaces but if there is a space while creating a row, then the server will use it and fill it back up.&lt;/p&gt;
&lt;p&gt;All of these empty spaces are called fragmentation. When it gets back, the database server spends a lot of time jumping over the empty spaces. Every time there is a page split, the server has to stop and do the split. Rebuilding the index requires it to stop longer to pack everything in.&lt;/p&gt;
&lt;p&gt;When setting up the table, one could set the &amp;ldquo;fill factor&amp;rdquo; which is how much space to reserve but insert enough GUIDs into the table and the page splitting starts to happen.&lt;/p&gt;
&lt;p&gt;Also, UUIDs are 128-bits which means they aren't a &amp;ldquo;native&amp;rdquo; data type for most database servers and servers (64-bit). This means they are usually stored as an array of bytes or strings.&lt;/p&gt;
&lt;h2&gt;Snowflakes&lt;/h2&gt;
&lt;p&gt;This where I was about seven or eight years ago. We knew that we needed to move toward horizontal scaling, UUIDs were not an option if we wanted performance, and we needed something that wouldn't require a lot of work.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://en.wikipedia.org/wiki/Snowflake_ID"&gt;snowflake IDs&lt;/a&gt;. Originally from Twitter, the 64-bit snowflake has four parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 bit to always make it positive&lt;/li&gt;
&lt;li&gt;41 bits of timestamp&lt;/li&gt;
&lt;li&gt;10 bits device identifier&lt;/li&gt;
&lt;li&gt;12 bits of serial&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a bit layout because for some reason, they didn't make these numbers fit on a hex boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntttttttttttttttttttttttttttttttttttttttttddddddddddssssssssssss
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The biggest part was that the timestamp part was in the front. Since time was always increasing, this meant that the generated identifiers were &amp;ldquo;mostly&amp;rdquo; increasing. This meant that there was significantly less random inserts in the middle of the index and everything was appended at the end (or nearly at the end depending on frequency).&lt;/p&gt;
&lt;p&gt;It also had the device ID. This meant that a server could &amp;ldquo;reserve&amp;rdquo; a number (0-1023) and there would be &lt;em&gt;no&lt;/em&gt; chance of a collision. Of course, that also meant that the devices (which could be a server or process) had to coordinate the device assignment.&lt;/p&gt;
&lt;p&gt;The last part was a serial number that started with 0 and went up to 4096. Since the timestamp was frequently at millisecond scale, that meant we could create 4096 rows per millisecond with no chance of collision.&lt;/p&gt;
&lt;p&gt;For my day job, this is what we went with since it was the smallest data type that fit our expected key spaces (billions but not quadrillions), and we were able to coordinate the device identifiers to ensure no chance of collisions.&lt;/p&gt;
&lt;p&gt;Also, snowflakes are 64-bit which means most languages can handle them natively (not Javascript though, which chokes on them). The need to coordinate the device, however, makes it not a great option for truly distributed services (such as Fediverse servers or Matrix rooms).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;There are also 128-bit versions of snowflake IDs in some libraries.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;UUID v7&lt;/h2&gt;
&lt;p&gt;Recently, I learned about UUIDv7. Like the v4, this is a long list of hash with a &amp;ldquo;7&amp;rdquo; in the third group. The primary difference is that the first part is a millisecond-based timestamp instead of random number but the latter half is still random.&lt;/p&gt;
&lt;p&gt;In hex:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tttttttt-tttt-7rrr-rrrr-rrrrrrrrrrrr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This solves the page split problem nicely by taking that leading 48-bits of timestamp so the value are always increasing and uses a smaller random space (74 bits or only 1.89e22 possible numbers). This means there is a greater chance of having a collision but only if the numbers were generated in the same millisecond (that's the big part).&lt;/p&gt;
&lt;p&gt;UUIDv7 also doesn't need the coordination of snowflakes, which lowers the complexity of generating a new identifier. At the moment, languages like C# doesn't have a built-in version for v7 and databases still struggle with the larger data type (compared to a 64-bit number which is native to most processors).&lt;/p&gt;
&lt;p&gt;For most of what I do, those negatives are relatively trivial with v7, so any new project would be based on UUIDv7 and I'd feel pretty good about it.&lt;/p&gt;
&lt;h2&gt;ULIDs&lt;/h2&gt;
&lt;p&gt;Notable, there are also &lt;a href="https://ulid.page/"&gt;ULID&lt;/a&gt; (&lt;a href="https://github.com/ulid/spec"&gt;specification&lt;/a&gt;) which also try to solve the problem. These are a lot like UUIDv7 except:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They don't have the version in the third group&lt;/li&gt;
&lt;li&gt;They have the same 48 bits of timestamp&lt;/li&gt;
&lt;li&gt;They have 80 bits of random instead of 74 for an increase of randomness (1.21e24 instead of 1.89e22)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They are also formatted with capital &lt;a href="https://en.wikipedia.org/wiki/Base32#Crockford%27s_Base32"&gt;Crockford Base32 encoding&lt;/a&gt; instead of hex numbers. This creates a considerably more compact format such as &amp;ldquo;01HZ613S22K8NR6W6GRV1Y5C52&amp;rdquo; instead of &amp;ldquo;022f4166-c274-43b1-a69d-a9c3c2153ca2&amp;rdquo; (26 characters verses 36).&lt;/p&gt;
&lt;p&gt;While the ULID page says it is compatible with UUIDs, they aren't because of that missing version field. There are &lt;a href="https://buildkite.com/blog/goodbye-integers-hello-uuids"&gt;systems&lt;/a&gt; that validate those entries so I find that claim to be not valid when dealing with very strict people (like my DBA and customers). Still, they are &amp;ldquo;close&amp;rdquo; to UUID and fit in much the same space.&lt;/p&gt;
&lt;h2&gt;Formatting&lt;/h2&gt;
&lt;p&gt;In all of these cases, there is one other issue to address: formatting.&lt;/p&gt;
&lt;p&gt;UUIDs and GUIDs are terrible in terms of how much space they use. Hex is great as a case-insensitive format, but the grouping of 8-4-4-4-12 makes it difficult to read over a phone or to find differences in the numbers. I struggle with more than 3-4 numbers at a time, so trying to enter in the twelve at the end can be a trial while being distracted.&lt;/p&gt;
&lt;p&gt;The Crockford encoding of ULIDs is much better because it uses a wider range of characters than hex which allows the actual identifier to be fewer characters. It also has some ability to handle difficult letters because there is no &amp;ldquo;I&amp;rdquo; or &amp;ldquo;L&amp;rdquo; in the encoding which means they can be treated as a &amp;ldquo;1&amp;rdquo; and still have the same results.&lt;/p&gt;
&lt;p&gt;But, for ULIDs, the resulting ID should be in all capitals and I consider this to be a problem because I have a degree of dyslexia and it is easy to get lost in a 26-character series of letters and numbers all at the same height. It's basically a &amp;ldquo;bar&amp;rdquo; of numbers.&lt;/p&gt;
&lt;p&gt;In the end, what I found that was easier with customers and end users was to make the identifier lowercase to help distinguish the numbers and then break it into groups of four with dashes (favoring the right side). Using the above example of &amp;ldquo;01HZ613S22K8NR6W6GRV1Y5C52&amp;rdquo;, that would result in &amp;ldquo;01-hz61-3s22-k8nr-6w6g-rv1y-5c52&amp;rdquo; which is slightly longer (32 characters) but makes it less overwhelming to read it over a phone or type it into a computer balanced on a box with a keyboard.&lt;/p&gt;
&lt;p&gt;These values can also be elided nicely much like you only have to enter the first 6-8 characters of a Git hash to reference it, there is a &amp;ldquo;reasonable&amp;rdquo; chance of avoiding overlaps if you only go with the last two blocks. I like to put &amp;ldquo;rv1y-5c52&amp;rdquo; in a web or GUI column but then put the full value in a copy button or as a tooltip.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;At this point, any new project I create would be based on UUIDv7. While they have a tiny smaller random space than ULID and UUIDv4, they play well with databases and have good support in most languages (C# and others have native UUID/GUID support, though maybe not v7 generation).&lt;/p&gt;
&lt;p&gt;The readability is an important aspect of mine, so I see myself formalizing on the lowercase with dashes with eliding to smaller numbers when the detail isn't needed.&lt;/p&gt;
&lt;p&gt;Overall, I like exploring these situations. I'll admit, I wish I knew about ULIDs or UUIDv7 eight years ago, but it is what it is.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Reflections on Nitride</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/04/21/nitride/" />
    <updated>2024-04-21T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/04/21/nitride/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="gatsbyjs" scheme="https://d.moonfire.us/tags/" label="GatsbyJS" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="bevy" scheme="https://d.moonfire.us/tags/" label="Bevy" />
    <summary type="html">I just got mfgames.com migrated over to Nitride, so a few thoughts about the previous three years.
</summary>
    <content type="html">&lt;p&gt;This week's goal was to get &lt;a href="https://mfgames.com"&gt;mfgames.com&lt;/a&gt; migrated over to &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;. It was a lofty goal, mainly because I've been making small incremental changes over the last year but never got something pushed over the finish line.&lt;/p&gt;
&lt;p&gt;I succeeded.&lt;/p&gt;
&lt;h2&gt;Migration&lt;/h2&gt;
&lt;p&gt;This is my third &amp;ldquo;big&amp;rdquo; site for Nitride, one that follows the common pattern of pulling in multiple Git repositories from my &lt;a href="https://src.mfgames.com"&gt;forge&lt;/a&gt;, integrating the data, and then creating a website out of it. In other words, one of the main reasons I created Nitride.&lt;/p&gt;
&lt;p&gt;It isn't exactly a fancy site, but I find that I'm not really creating flashy sites anymore. Part of this is because I still enjoy &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt;, which is a text-only format, but also because the chrome is distracting to me. I want something simple but not the black on gray of Netscape Navigator.&lt;/p&gt;
&lt;p&gt;Overall, I'm pretty happy with the results. It creates landing pages for projects, pulls in the &lt;code&gt;./docs/&lt;/code&gt; folder for each project and formats them (for example, &lt;a href="//mfgames.com/mfgames-cil/docs/gallium/"&gt;MfGames.Gallium&lt;/a&gt;), and merges news posts from the projects (while decorating them) and creates a single unified feed.&lt;/p&gt;
&lt;p&gt;There are more things I want to do, such as links to issue tracking, licenses, and other stuff. I also ended up losing some features like the culture sandbox (which I needed to rewrite anyways), the slippery maps, and a few other minor things.&lt;/p&gt;
&lt;p&gt;Still happy with it.&lt;/p&gt;
&lt;h2&gt;News&lt;/h2&gt;
&lt;p&gt;I was working on debugging creating news posts in individual project repositories anews posts from individual projects into mfgames.com as my &amp;ldquo;little thing&amp;rdquo; for today and I came up with the exact period of time I decided to write my latest static site generator (SSG):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git log --pretty=&amp;quot;%ai %s&amp;quot;
...
2021-05-04 02:27:28 -0500 feat: implementing my own
2021-04-07 22:13:20 -0500 chore: updating Statiq libraries
...
2014-05-07 14:41:27 -0700 Initial commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It all started a year before, when I migrated from CobblestoneJS (my SSG from the early 2010s) to &lt;a href="https://www.gatsbyjs.com/"&gt;GatsbyJS&lt;/a&gt;. Which was pretty and gorgeous and so difficult to debug. It was also fragile for me because I don't update my libraries to follow the curve and every time I tried to add a new feature to the site, I spent a few days just trying to figure out the node packages to get everything building again first.&lt;/p&gt;
&lt;p&gt;When I got into Gemini for the first time, I also realized that GatsbyJS wasn't going to work there because that shiny site was not text friendly. It was also tied into my frustrating that going to a page the first time downloaded the content in one manner, but navigation was done via SPA and pulled it from another; and the cache was killing me there.&lt;/p&gt;
&lt;p&gt;(But it looked so pretty.)&lt;/p&gt;
&lt;p&gt;So I decided to look for a new SSG. I didn't want to write my own because&amp;hellip; I really don't like writing utility programs. But I wanted to do something Gatsby didn't do, which is generate a site that didn't require Javascript.&lt;/p&gt;
&lt;p&gt;I figured I was good at C# (this was before Rust, but I'm still best at C#), so I wanted to find a C# static site generator in hopes that the NuGet package management wasn't as excruciating as Node's.&lt;/p&gt;
&lt;p&gt;So I found &lt;a href="https://www.statiq.dev/"&gt;Statiq&lt;/a&gt; which seemed like a perfect match. But going from GatsbyJS to Statiq quickly hit a point where I realized I was, once again, forcing a program to do things it wasn't supposed to be doing. The patterns in Statiq were nice but they were too rigid for the complexity of my way of creating sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The fragility of pulling in 80+ Git repositories into a CI pipeline&lt;/li&gt;
&lt;li&gt;The use of multiple dynamic lists on a single page&lt;/li&gt;
&lt;li&gt;The desire to make a Gemini-friendly site&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that month of thinking, I started to write my own. This came when I was looking at Statiq's document type realizing that my desire to create a new type basically meant I had to rebuild the entire framework to support a slightly different enumeration.&lt;/p&gt;
&lt;h2&gt;Gallium&lt;/h2&gt;
&lt;p&gt;Enter Entity Component Systems (ECS) which I learned about while pondering writing a game using &lt;a href="https://bevyengine.org/"&gt;Bevy&lt;/a&gt;. I had never heard of an ECS before that, but the idea was great. Why not write a static site generator that didn't need a single enum to drive processing, where I could add arbitrary components to pages such as nested lists or metadata, and then easily be able to use them.&lt;/p&gt;
&lt;p&gt;Of course, then I had to look at existing ECS systems in C#, but I eventually decided to be more idiomatic to C# (another frustration of Statiq) and focus on something that let me use LINQ-like method calls (such as &lt;code&gt;.Select()&lt;/code&gt; and &lt;code&gt;.Where()&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;List&amp;lt;Entity&amp;gt; newsList = fileList
    .WhereEntity&amp;lt;UPath&amp;gt;(this.OnlyNewsFiles)
    .Run(this.setInstantFromPath, cancellationToken)
    .SelectEntity&amp;lt;UPath, Instant&amp;gt;(this.pathService.SetPathFromInstant)
    .Select(entity =&amp;gt; entity.Set(IsPost.Instance))
    .Select(entity =&amp;gt; entity.Set(IncludeInFeed.Instance))
    .Where(entity =&amp;gt; entity.Has&amp;lt;IsMarkdown&amp;gt;())
    .OrderBy(entity =&amp;gt; entity.Get&amp;lt;PageModel&amp;gt;().Title)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm specifically not saying &lt;a href="/tags/gallium/"&gt;Gallium&lt;/a&gt; is a &amp;ldquo;high performance&amp;rdquo; because it isn't. It is an ECS for relatively small systems that builds on existing libraries for patterns. I don't need high performance for sites with only a few thousand pages, I want something easy to understand.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There are quite a few steps going forward. I still want to make Nitride easier to use, Gallium to track LINQ patterns, and mfgames.com to present more data. But this is a big step and I'm really happy that I'm proud of (I'm sure there will be tweaks). More importantly, I have something that hasn't given me any major trouble updating since I started three years ago.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Switching Ceph to SeaweedFS on NixOS</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/03/21/switching-ceph-to-seaweedfs/" />
    <updated>2024-03-21T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/03/21/switching-ceph-to-seaweedfs/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="ceph" scheme="https://d.moonfire.us/tags/" label="Ceph" />
    <category term="seaweedfs" scheme="https://d.moonfire.us/tags/" label="SeaweedFS" />
    <category term="raspberry-pi" scheme="https://d.moonfire.us/tags/" label="Raspberry Pi" />
    <category term="colmena" scheme="https://d.moonfire.us/tags/" label="Colmena" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="restic" scheme="https://d.moonfire.us/tags/" label="Restic" />
    <summary type="html">Over the new year, I decided to get SeaweedFS working on my home lab and eventually took down my Ceph cluster to move everything over.
</summary>
    <content type="html">&lt;p&gt;At the end of 2023, I realized that I was running out of space on my home &lt;a href="/tags/ceph/"&gt;Ceph&lt;/a&gt; cluster and it was time to add another node to it. While I had space for one more 3.5&amp;quot; drive in one of my servers, I was feeling a little adventurous and decided to get a &lt;a href="https://deskpi.com/collections/deskpi-super6c"&gt;DeskPi Super6C&lt;/a&gt;, a &lt;a href="/tags/raspberry-pi/"&gt;Raspberry Pi&lt;/a&gt; CM4, a large NVMe drive, and try to create a new node that way.&lt;/p&gt;
&lt;p&gt;Well, over the following few months, a lot of mistakes were made that are worthy of a dedicated post. But, when most of those problems were resolve, I encountered another series of &amp;ldquo;adventures&amp;rdquo; which led me to switching out my home's Ceph cluster for a &lt;a href="/tags/seaweedfs/"&gt;SeaweedFS&lt;/a&gt; one.&lt;/p&gt;
&lt;h2&gt;Unable to Build Ceph&lt;/h2&gt;
&lt;p&gt;Around the time I was working on the Pi setup, my &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; flake was unable to build the &lt;code&gt;ceph&lt;/code&gt; packages. Part of this is because I was working off unstable and so the few weeks of being unable to build meant I couldn't get Ceph working on the new hardware. I tried even compiling it myself, which takes about six hours on my laptop and longe because I had to remote build on the Pi itself since I have yet to figure out how to get &lt;a href="/tags/colmena/"&gt;Colmena&lt;/a&gt; to build &lt;code&gt;aarch64&lt;/code&gt; on my laptop.&lt;/p&gt;
&lt;p&gt;Also, I was dreading setting up Ceph since I remember how many manual steps I had to do to get the OSDs working on my machines. While researching it, I was surprised to see my &lt;a href="/blog/2022/12/10/ceph-and-nixos/"&gt;blog post on it&lt;/a&gt; was on the &lt;a href="https://nixos.wiki/wiki/Ceph"&gt;wiki page&lt;/a&gt;, which is kind of cool and a nice egoboo.&lt;/p&gt;
&lt;p&gt;There was &lt;a href="https://github.com/NixOS/nixpkgs/pull/281924"&gt;a PR&lt;/a&gt; on Github for using the Ceph-provided OSD setup that would have hopefully alleviated it. That looked promising, so I was watching that PR with interest because I was right at the point of needing it.&lt;/p&gt;
&lt;p&gt;Sadly, that PR ended up being abandoned for a &amp;ldquo;better&amp;rdquo; approach. Given that it takes me six hours to build Ceph, I couldn't really help with that approach which meant I was stuck waiting unless I was willing to dedicate a month or so trying to figure it all out. Given that the last time I tried to do that, my PR was abandoned for a different reason, I was preparing to keeping my Ceph pinned until the next release and just having my Raspberry Pi setup sit there idle.&lt;/p&gt;
&lt;p&gt;I was also being impatient and there was something new to try out.&lt;/p&gt;
&lt;h2&gt;SeaweedFS&lt;/h2&gt;
&lt;p&gt;Then I noticed a little thing on top of the NixOS wiki for Ceph:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another distributed filesystem alternative you may evaluate is SeaweedFS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I vaguely remember looking at it when I first set up my 22 TB Ceph cluster, but I'd been dreaming about having a Ceph cluster for so long that it was dismissed because I really wanted to do Ceph.&lt;/p&gt;
&lt;p&gt;Now, the need was less there so I thought I would give it a try. If anything, I still had a running Ceph cluster and I could run them side-by-side.&lt;/p&gt;
&lt;p&gt;A big difference I noticed is that SeaweedFS has a single executable that provides everything. You can run it as a all-in-one process, but the three big services can also be run independently. That includes the master (coordinates everything), the volumes (where things are stored), and filer (make it look like a filesystem).&lt;/p&gt;
&lt;p&gt;Also, Ceph likes to work at the block level whereas SeaweedFS wants to be pointed to plain directories. So the plan was to take the 1 TB drive for my Raspberry Pi and turn it into a little cluster to try it out.&lt;/p&gt;
&lt;h2&gt;SeaweedFS and NixOS&lt;/h2&gt;
&lt;p&gt;The first thing was that SeaweedFS doesn't have any NixOS options. I couldn't find any flakes for it either. My attempt to create one took me three days with little success. Instead, I ended up cheating and just grabbed the &lt;a href="https://hg.sr.ht/%7Edermetfan/seaweedfs-nixos/browse/seaweedfs.nix?rev=tip"&gt;best-looking one I could find&lt;/a&gt; and dump it directly into my flake. It isn't even an override.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Yeah I would love to have a flake for this but I'm not skilled enough to create it myself.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Masters&lt;/h2&gt;
&lt;p&gt;With that, a little fumbling got a master† server up and running. You only need one of these, so pick a stable server and set it up.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/nodes/node-0.nix
inputs @ { config
, pkgs
, flakes
, ...
}: {
  imports = [
    ../../services/seaweedfs.nix # the file from dermetfan
  ];

  services.seaweedfs.clusters.default = {
    package = pkgs.seaweedfs;

    masters.main = {
      openFirewall = true;
      ip = &amp;quot;fs.home&amp;quot;; # This is what shows up in the links
      mdir = &amp;quot;/var/lib/seaweedfs/master/main&amp;quot;;
      volumePreallocate = true;

      defaultReplication = {
        dataCenter = 0;
        rack = 0;
        server = 0;
      };
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a really basic setup that doesn't really do anything. The master server is pretty much a coordinator. But, what is nice is that that it shows something by starting up a web server at &lt;code&gt;fs.home:9333&lt;/code&gt; that lets you see that it is up (sadly, no dark mode) and running. This site will also let you get to all the other servers through web links.&lt;/p&gt;
&lt;p&gt;Another important part is the &lt;code&gt;defaultReplication&lt;/code&gt;. I made it explicit, but when messing around, setting all three to &lt;code&gt;0&lt;/code&gt; means that you don't get hung up the first time you try to write a file and it tries to replicate to a second node that isn't set up. All zeros is basically &amp;ldquo;treat the cluster as a single large disk.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Later on, you can change that easily. I ended up setting &lt;code&gt;rack = 1;&lt;/code&gt; in the above example because I treat each node as a &amp;ldquo;rack&amp;rdquo; since I don't really have a server rack.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;† I don't like using &amp;ldquo;master&amp;rdquo; and prefer main, but that is the terminology that SeaweedFS uses.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Volumes&lt;/h2&gt;
&lt;p&gt;Next up was configuring a volume server. I ended up doing one per server (I have four nodes in the cluster now) even though three of them had multiple partitions/directories on different physical drives. In all of these cases, I named the directory &lt;code&gt;/mnt/fs-001&lt;/code&gt; and created an &lt;code&gt;ext4&lt;/code&gt; partition on it. I could have used ZFS, but I know and trust &lt;code&gt;ext4&lt;/code&gt; and had trouble with ZFS years ago. But it doesn't matter, just make a drive.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/nodes/node-0.nix
inputs @ { config
, pkgs
, flakes
, ...
}: {
  imports = [
    ../../services/seaweedfs.nix # the file from dermetfan
  ];

  services.seaweedfs.clusters.default = {
    package = pkgs.seaweedfs;

    volumes.${config.networking.hostName} = {
      openFirewall = true;
      dataCenter = &amp;quot;home&amp;quot;;
      rack = config.networking.hostName;
      ip = &amp;quot;${config.networking.hostName}.home&amp;quot;;
      dir = [ &amp;quot;/mnt/fs-001&amp;quot; ];
      disk = [ &amp;quot;hdd&amp;quot; ]; # Replication gets screwy if these don't match
      max = [ 0 ];
      port = 9334;

      mserver = [
        {
          ip = &amp;quot;fs.home&amp;quot;;
          port = 9333;
        }
      ];
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once started up, this starts a service on &lt;code&gt;http://node-001.home:9333&lt;/code&gt;, connects to the master which will then show a link on that page, and basically say there is plenty of space.&lt;/p&gt;
&lt;p&gt;The key parts I found are the &lt;code&gt;disk&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Replication is based on dataCenter, rack, and server but also only if the disk types agree. So, &lt;code&gt;hdd&lt;/code&gt; will only sync to other &lt;code&gt;hdd&lt;/code&gt; even if half of them are &lt;code&gt;ssd&lt;/code&gt; or &lt;code&gt;nvme&lt;/code&gt;. Because I have a mix of NVMe and HDD, I made them all &lt;code&gt;hdd&lt;/code&gt; because it works and I don't really care.&lt;/p&gt;
&lt;p&gt;The value of &lt;code&gt;0&lt;/code&gt; for &lt;code&gt;max&lt;/code&gt; means use all the available space. Otherwise, it only grabs a small number of 30 GB blocks and stops. Since I was dedicating the entire drive over to the cluster, I wanted to use everything.&lt;/p&gt;
&lt;h2&gt;Filers&lt;/h2&gt;
&lt;p&gt;The final service needed is a filer. This is basically the POSIX layer that lets you mount the drive in Linux and start to do fun things with it. Like the others, it just gets put on the server. I only set up one filer and it seems to work, but others set up multiples. I just don't really understand why.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/nodes/node-0.nix
inputs @ { config
, pkgs
, flakes
, ...
}: {
  imports = [
    ../../services/seaweedfs.nix # the file from dermetfan
  ];

  services.seaweedfs.clusters.default = {
    package = pkgs.seaweedfs;

    filers.main = {
      openFirewall = true;
      dataCenter = &amp;quot;home&amp;quot;;
      encryptVolumeData = false;
      ip = &amp;quot;fs.home&amp;quot;;
      peers = [ ];

      master = [ # this is actually in cluster.masters that I import in the real file
        {
          ip = &amp;quot;fs.home&amp;quot;;
          port = 9333;
        }
      ];
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like the others, this starts up a web service at &lt;code&gt;fs.home:8888&lt;/code&gt; that lets you browse the file system, upload files, and do fun things. Once this is all deployed (by your system of choice, mine is Colmena), then it should be up and running. Which means you should be able to upload a folder with the port 8888 site.&lt;/p&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;p&gt;I found the error messages are a little confusing a time, but weren't too much of a trouble to find. I just had to tail &lt;code&gt;journalctl&lt;/code&gt; and then try to figure it out.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;journalctl -f | grep seaweed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have multiple servers, debugging requires doing this to all of them.&lt;/p&gt;
&lt;h2&gt;Secondary Volumes&lt;/h2&gt;
&lt;p&gt;Adding more volumes is pretty easy. I just add a Nix expression to each node to include drives.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;  services.seaweedfs.clusters.default = {
    package = pkgs.seaweedfs;

    volumes.main = {
      openFirewall = true;
      dataCenter = &amp;quot;main&amp;quot;;
      rack = config.networking.hostName;
      mserver = cluster.masters; # I have this expanded out above
      ip = &amp;quot;${config.networking.hostName}.home&amp;quot;;
      dir = [ &amp;quot;/mnt/fs-002&amp;quot; &amp;quot;/mnt/fs-007&amp;quot; ]; # These are two 6 TB red drives
      disk = [ &amp;quot;hdd&amp;quot; &amp;quot;hdd&amp;quot; ]; # Replication gets screwy if these don't match
      max = [ 0 0 ];
      port = 9334;
    };
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As soon as they deploy, they hook up automatically and increase the size of the cluster.&lt;/p&gt;
&lt;h2&gt;Mounting&lt;/h2&gt;
&lt;p&gt;Mounting&amp;hellip; this gave me a lot of trouble. Nix does not play well with the auto-mount and SeaweedFS, so I had to jump through a few hoops. In the end, I created a &lt;code&gt;mount.nix&lt;/code&gt; file that I include on any node that I have to mount the cluster on, which always goes into &lt;code&gt;/mnt/cluster&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs @ { config
, pkgs
, ...
}:
let
  mountDir = &amp;quot;/mnt/cluster&amp;quot;;

  # A script to go directly to the shell.
  shellScript = (pkgs.writeShellScriptBin
    &amp;quot;weed-shell&amp;quot;
    ''
      weed shell -filer fs.home:8888 -master fs.home:9333 &amp;quot;$@&amp;quot;
    '');

  # A script to list the volumes.
  volumeListScript = (pkgs.writeShellScriptBin
    &amp;quot;weed-volume-list&amp;quot;
    ''
      echo &amp;quot;volume.list&amp;quot; | weed-shell
    '');

  # A script to allow the file system to be mounted using Nix services.
  mountScript = (pkgs.writeShellScriptBin
    &amp;quot;mount.seaweedfs&amp;quot;
    ''
      if ${pkgs.gnugrep}/bin/grep -q ${mountDir} /proc/self/mountinfo
      then
        echo &amp;quot;already mounted, unmounting&amp;quot;
        exit 0
      fi

      echo &amp;quot;mounting weed: ${pkgs.seaweedfs}/bin/weed&amp;quot; &amp;quot;$@&amp;quot;
      ${pkgs.seaweedfs}/bin/weed &amp;quot;$@&amp;quot;
      status=$?

      for i in 1 1 2 3 4 8 16
      do
        echo &amp;quot;checking if mounted yet: $i&amp;quot;
        if ${pkgs.gnugrep}/bin/grep -q ${mountDir} /proc/self/mountinfo
        then
          echo &amp;quot;mounted&amp;quot;
          exit 0
        fi

        ${pkgs.coreutils-full}/bin/sleep $i
      done

      echo &amp;quot;gave up: status=$status&amp;quot;
      exit $status
    '');
in
{
  imports = [
    ../../seaweedfs.nix
  ];

  # The `weed fuse` returns too fast and systemd doesn't think it has succeeded
  # so we have a little delay put in here to give the file system a chance to
  # finish mounting and populate /proc/self/mountinfo before returning.
  environment.systemPackages = [
    pkgs.seaweedfs
    shellScript
    volumeListScript
    mountScript
  ];

  systemd.mounts = [
    {
      type = &amp;quot;seaweedfs&amp;quot;;
      what = &amp;quot;fuse&amp;quot;;
      where = &amp;quot;${mountDir}&amp;quot;;
      mountConfig = {
        Options = &amp;quot;filer=fs.home:8888&amp;quot;;
      };
    }
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, let me break this into the parts. SeaweedFS has a nice little interactive shell where you can query status, change replication, and do lots of little things. However, it requires a few parameters, so the first thing I do is create a shell script called &lt;code&gt;weed-shell&lt;/code&gt; that provides those parameters so I don't have to type them.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ weed-shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second thing while doing this is that I wanted to see a list of all the volumes. SeaweedFS creates 30 GB blobs for storage instead of thousands of little files. This makes things more efficient in a lot of way (replication is done on volume blocks).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ weed-volume-list | head
.&amp;gt; Topology volumeSizeLimit:30000 MB hdd(volume:810/1046 active:808 free:236 remote:0)
  DataCenter main hdd(volume:810/1046 active:808 free:236 remote:0)
    Rack node-0 hdd(volume:276/371 active:275 free:95 remote:0)
      DataNode node-0.home:9334 hdd(volume:276/371 active:275 free:95 remote:0)
        Disk hdd(volume:276/371 active:275 free:95 remote:0)
          volume id:77618  size:31474091232  file_count:16345  replica_placement:10  version:3  modified_at_second:1708137673 
          volume id:77620  size:31501725624  file_count:16342  delete_count:4  deleted_byte_count:7990733  replica_placement:10  version:3  modified_at_second:1708268248 
          volume id:77591  size:31470805832  file_count:15095  replica_placement:10  version:3  modified_at_second:1708104961 
          volume id:77439  size:31489572176  file_count:15067  replica_placement:10  version:3  modified_at_second:1708027468 
          volume id:77480  size:31528095736  file_count:15118  delete_count:1  deleted_byte_count:1133  replica_placement:10  version:3  modified_at_second:1708093312 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When doing things manually, that is all I needed to see things working and get the warm and fuzzy feeling that it worked.&lt;/p&gt;
&lt;p&gt;Getting it to automatically mount (or even &lt;code&gt;systemctl start mnt-cluser.mount&lt;/code&gt;) is that the command to do so is &lt;code&gt;weed fuse /mnt/cluster -o &amp;quot;filer=fs.home:8888&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;NixOS doesn't like that.&lt;/p&gt;
&lt;p&gt;So my answer was to write a shell script that fakes a &lt;code&gt;mount.seaweedfs&lt;/code&gt; and calls the right thing. Unfortunately, it rarely worked and it took me a few days to figure out why. While &lt;code&gt;weed fuse&lt;/code&gt; returns right away, I'm guessing network latency means that &lt;code&gt;/proc/self/mountinfo&lt;/code&gt; doesn't update for a few seconds later. But &lt;code&gt;systemd&lt;/code&gt; had already queried the &lt;code&gt;mountinfo&lt;/code&gt; file, saw that it wasn't mounted, and then declared the mount failed.&lt;/p&gt;
&lt;p&gt;But, by the time I (as a slow human) looked at it, the &lt;code&gt;mountinfo&lt;/code&gt; showed success.&lt;/p&gt;
&lt;p&gt;The answer was to delay returning from &lt;code&gt;mount.seaweedfs&lt;/code&gt; until we give SeaweedFS a chance to finish so &lt;code&gt;systemd&lt;/code&gt; could see it was mounted and didn't fail the unit. Hence the loop, grep, and sleeping inside &lt;code&gt;mount.seaweedfs&lt;/code&gt;. Figuring that out required a lot of reading code and puzzling through things to figure out, so hopefully that will help someone else.&lt;/p&gt;
&lt;p&gt;After I did, though, it was working pretty smoothly since, including recovering on reboot.&lt;/p&gt;
&lt;h2&gt;Changing Replication&lt;/h2&gt;
&lt;p&gt;As I mentioned above, once I was able to migrate the Ceph cluster, I changed replication to &lt;code&gt;rack = 1;&lt;/code&gt; to create one extra copy across all four nodes. However, SeaweedFS doesn't automatically rebalance like Ceph does. Instead, you have to go into the shell and run some commands.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ weed-shell
lock
volume.deleteEmpty -quietFor=24h -force
volume.balance -force
volume.fix.replication
unlock
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also set it up to do it automatically, I'm not entirely sure I've done that so I'm not going to show my attempt.&lt;/p&gt;
&lt;h2&gt;Observations&lt;/h2&gt;
&lt;p&gt;One of the biggest thing I noticed is that Ceph does proactive maintenance on drives. It doesn't sound like much, but I feel more comfortable that Ceph would detect errors. It also means that the hard drives are always running in my basement; just the slow grind of physical hardware as Ceph scrubs and shuffles things around.&lt;/p&gt;
&lt;p&gt;SeaweedFS is more passive in that regard. I don't trust that it won't catch a failed hard drive a fast, but it still doesn't have the failures of RAID, lets me spread out data across multiple servers and locations. There is also a feature for uploading to a S3 server if I wanted. I use a &lt;a href="/tags/restic/"&gt;Restic&lt;/a&gt; service for my S3 uploads.&lt;/p&gt;
&lt;p&gt;That passivity also means it hasn't been grinding my drives as much and I don't have to worry about the SSDs burning out too quickly.&lt;/p&gt;
&lt;p&gt;Another minor thing is that while there are a lot less options with SeaweedFS, it took me about a third of the time to get the cluster up and running. There were a few error messages that threw me, but for the most part, I understood the errors and what SeaweedFS was looking for. Not always the case with Ceph and I had a few year-long warnings that I never figured out how to fix that I was content to leave as-is.&lt;/p&gt;
&lt;p&gt;I do not like the lack of dark mode on SeaweedFS's websites.&lt;/p&gt;
&lt;h2&gt;Opinions&lt;/h2&gt;
&lt;p&gt;I continue to like Ceph, but I also like SeaweedFS. I would use either, depending on the expected load. If I was running Docker images or doing coding on the cluster, I would use a Ceph cluster. But, in my case, I'm using it for long-term storage, video files, assets, and photo shoots. Not to mention my dad's backups. So I don't need the interactive of Ceph along its higher level of maintenance.&lt;/p&gt;
&lt;p&gt;Also, it is a relatively simple Go project, doesn't take six hours to build, and uses more concepts that I understand (&lt;code&gt;mkfs.ext4&lt;/code&gt;) that I'm more comfortable with it.&lt;/p&gt;
&lt;p&gt;It was also available at the point I wanted to play (though Ceph is building on NixOS unstable again, so that is moot problem. I was just being impatient and wanted to learn something new.)&lt;/p&gt;
&lt;p&gt;At the moment, SeaweedFS works out nice for my use case and decided to switch my entire Ceph cluster over. I don't feel as safe with SeaweedFS, but I feel Safe Enough™.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>NixOS and NextCloud</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/03/03/nixos-and-nextcloud/" />
    <updated>2024-03-03T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/03/03/nixos-and-nextcloud/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="nextcloud" scheme="https://d.moonfire.us/tags/" label="NextCloud" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <summary type="html">Just a few tips on getting NextCloud 28 working with NixOS, plus a copy of my `.nix` file that I got working.
</summary>
    <content type="html">&lt;p&gt;I spent a few hours upgrading my &lt;a href="/tags/nextcloud/"&gt;NextCloud&lt;/a&gt; server from
version 26 to 28. Sadly, it was not a pleasant process, mainly because there
were some index changes that were conflicting and the migration code kept
having a problem with creating indexes with one name for &lt;code&gt;oc_table_name&lt;/code&gt;
because it conflicted with &lt;code&gt;table_name&lt;/code&gt;. This is probably because I have a
relatively old NextCloud setup that has been migrated from OwnCloud over the
years and didn't use table prefixes.&lt;/p&gt;
&lt;p&gt;Unfortunately, that meant I ended up having to do a lot of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DROP INDEX index_name ON table_name;&lt;/code&gt; in &lt;code&gt;mysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl restart nextcloud-setup.service || systemctl status nextcloud-setup.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Look at the error, go back to the beginning with the new name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That took about an hour to get through and pretty much points out that I also
aren't using the proper indexes because half of those tables are empty and
the other have isn't. However, &lt;code&gt;table_name&lt;/code&gt; in the above example was sometimes
the prefixes (&lt;code&gt;oc_table_name&lt;/code&gt;) and other times it was the non-prefixed (&lt;code&gt;table_name&lt;/code&gt;)
version so that is just something I'm going to live with.&lt;/p&gt;
&lt;p&gt;Once I finished, it complained about missing indexes, so I had to run this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;nextcloud-occ db:add-missing-indices
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which probably put all the indexes I dropped and the migrations put on the wrong
table back to the table that is actually used.&lt;/p&gt;
&lt;p&gt;Finally, there were still a few other errors about &amp;ldquo;interned_strings_buffer&amp;rdquo;
and memcached not working. I figured some of these were related to me losing my
database instance on my hosting provider, which meant I moved the database from
a dedicated server to the same one as the NextCloud server.&lt;/p&gt;
&lt;p&gt;Below the Nix file I use to set up NextCloud so it doesn't give me any warnings
and has backups properly set up.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# nextcloud.nix
{ config
, pkgs
, ...
}:
let
  host = &amp;quot;nextcloud.example.com&amp;quot;;
  backup-name = &amp;quot;restic&amp;quot;;
in
{
  # Set up the user in case you need consistent UIDs and GIDs. And also to make
  # sure we can write out the secrets file with the proper permissions.
  users.groups.nextcloud = { };
  users.users.nextcloud = {
    isSystemUser = true;
    group = &amp;quot;nextcloud&amp;quot;;
  };

  # Set up backing up the database automatically. The path will be in
  # `/var/backups/mysql/nextcloud.gz`.
  services.mysqlBackup.databases = [ &amp;quot;nextcloud&amp;quot; ];

  # Restic is already set up to back up the mysql directory but
  # we also set it up to backup the data.
  services.restic.backups.${backup-name}.paths = [&amp;quot;/var/lib/nextcloud/data&amp;quot;];

  # Set up secrets. This is a sops-nix file checked in at the same folder as
  # this file.
  sops.secrets = {
    nextcloud-admin-password = {
      sopsFile = ./secrets.yaml;
      mode = &amp;quot;0600&amp;quot;;
      owner = &amp;quot;nextcloud&amp;quot;;
      group = &amp;quot;nextcloud&amp;quot;;
    };

    nextcloud-db-password = {
      sopsFile = ./secrets.yaml;
      mode = &amp;quot;0600&amp;quot;;
      owner = &amp;quot;nextcloud&amp;quot;;
      group = &amp;quot;nextcloud&amp;quot;;
    };

    nextcloud-secrets = {
      sopsFile = ./secrets.yaml;
      mode = &amp;quot;0600&amp;quot;;
      owner = &amp;quot;nextcloud&amp;quot;;
      group = &amp;quot;nextcloud&amp;quot;;
    };
  };

  # Set up Nextcloud.
  services.nextcloud = {
    enable = true;
    package = pkgs.nextcloud28;
    https = true;
    hostName = host;
    secretFile = &amp;quot;/var/run/secrets/nextcloud-secrets&amp;quot;;

    phpOptions.&amp;quot;opcache.interned_strings_buffer&amp;quot; = &amp;quot;13&amp;quot;;

    config = {
      dbtype = &amp;quot;mysql&amp;quot;;
      dbname = &amp;quot;nextcloud&amp;quot;;
      dbhost = &amp;quot;localhost&amp;quot;;
      dbpassFile = &amp;quot;/var/run/secrets/nextcloud-db-password&amp;quot;;

      adminuser = &amp;quot;admin&amp;quot;;
      adminpassFile = &amp;quot;/var/run/secrets/nextcloud-admin-password&amp;quot;;
    };

    settings = {
      maintenance_window_start = 2; # 02:00
      default_phone_region = &amp;quot;en&amp;quot;;
      filelocking.enabled = true;

      redis = {
        host = config.services.redis.servers.nextcloud.bind;
        port = config.services.redis.servers.nextcloud.port;
        dbindex = 0;
        timeout = 1.5;
      };
    };

    caching = {
      redis = true;
      memcached = true;
    };
  };

  # Set up Redis because the admin page was complaining about it.
  # https://discourse.nixos.org/t/nextlcoud-with-redis-distributed-cashing-and-file-locking/25321/3
  services.redis.servers.nextcloud = {
    enable = true;
    bind = &amp;quot;::1&amp;quot;;
    port = 6379;
  };

  # Setup Nginx because we have multiple services on this server.
  services.nginx = {
    enable = true;
    virtualHosts.&amp;quot;${host}&amp;quot; = {
      forceSSL = true;
      enableACME = true;
    };
  };

  networking.firewall.allowedTCPPorts = [ 80 443 ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully, this will help others if they encounter the same problems as me
since I couldn't find these answers with web searching.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>New Site Colors</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/02/24/new-site-colors/" />
    <updated>2024-02-24T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/02/24/new-site-colors/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="priduck" scheme="https://d.moonfire.us/tags/" label="Priduck" />
    <category term="css" scheme="https://d.moonfire.us/tags/" label="CSS" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <summary type="html">I decided to rework the colors of my websites, both for d.moonfire.us and future work on fedran.com. Along the way, I made a library to package the theme work and got to play with CSS variables.
</summary>
    <content type="html">&lt;p&gt;I should have been working on something else today, but a series of events lead me down a path of reworking how I implemented my themes here on &lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt; and &lt;a href="https://fedran.com/"&gt;https://fedran.com/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I have not updated the color themes on Fedran at this point.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In specific, it was someone mentioning they didn't like my dark theme on their recently announced blogroll. Now, there is absolutely nothing wrong with someone not liking my sense of colors or even how I code websites, but a desire to change the colors has been sitting in the back of my head for a few months. That meant that the commentary reminded me of my own desires to change it and that lead into my fixating on redoing the colors.&lt;/p&gt;
&lt;h2&gt;Color Preferences&lt;/h2&gt;
&lt;p&gt;One of the problems is that I have a fondness for mono- and bichromatic color themes. You can see that on my covers, but I also realize that it creates rather plain (e.g., uninteresting) site theme. I started to really notice it once I started using &lt;a href="https://github.com/catppuccin/catppuccin"&gt;Catppuccin&lt;/a&gt; as one of my most common UI themes. There were colors on the screen! Multiple of theme!&lt;/p&gt;
&lt;p&gt;And I started wanting a few distinct of my own.&lt;/p&gt;
&lt;p&gt;At the same time, I continue to struggle with contrasts. When I was younger (teenager and 20s), I loved drawing on quadraile graph paper. But somewhere in my 30s, I started having trouble because the lines were making it hard for me to see my writing. So I went on a few adventures to find lighter lines and evetually switched to dotted grid pages. Then in my 40s, even those became too dark and I had to switch to the same thing my father used: plain white paper.&lt;/p&gt;
&lt;h2&gt;POV Colors&lt;/h2&gt;
&lt;p&gt;Related to this is my decision that every point of view in Fedran had a different color. I like that idea, mainly because I &lt;em&gt;love&lt;/em&gt; colors, I'm just not great with seeing differences. This is why &lt;a href="https://fedran.com/sand-and-blood/"&gt;Sand and Blood&lt;/a&gt; has a different color set than &lt;a href="https://fedran.com/allegro/"&gt;Allegro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each POV has a color hue and everything else is built off of that using &lt;a href="https://atmos.style/blog/lch-color-space"&gt;LCH colors&lt;/a&gt;. When I switched the covers to be bichromatic, I made the second color always be the contrast (rotate 180 degrees) from the primary color.&lt;/p&gt;
&lt;p&gt;Easy huh?&lt;/p&gt;
&lt;p&gt;However, this entire excercise pointed out that most people can't tell the difference between say 220 and 225 hue, so I really should spread out the colors a lot more. Even if I say a 6 degree difference in hues, that would limit me to sixty POVs. (Of course, since I'm still somewhat burned out, sixty is a lot right now.)&lt;/p&gt;
&lt;p&gt;So, I'm going to change it so each POV color is three colors: the primary color I had before and then two accent colors (which the first will be the contrast color for the existing assigned colors). I'm not quite ready for the full gaumt of colors, but maybe increasing them to three would give me more than enough space and still cater to my reduced color palette.&lt;/p&gt;
&lt;p&gt;It also harkens back to CGA colors, which is the first colors I saw on computers.&lt;/p&gt;
&lt;h2&gt;Segmenting Colors&lt;/h2&gt;
&lt;p&gt;The general idea was to break the color space into ten segments, numbers from zero to nine. The first color (&lt;code&gt;c0&lt;/code&gt;) would be based on the hue and the others would be 36° from that so they are evenly spread across the entire range of hues.&lt;/p&gt;
&lt;p&gt;&lt;img src="./color-grid-220.png" class="block" alt="Color Grid for 220°" /&gt;&lt;/p&gt;
&lt;p&gt;If the hue changes (like the POV hues), then everything else rotates to fit.&lt;/p&gt;
&lt;p&gt;&lt;img src="./color-grid-20.png" class="block" alt="Color Grid for 20°" /&gt;&lt;/p&gt;
&lt;p&gt;As you can see in the above images, there is also ten ranges of brightness (combination of saturation and lightness) that goes from zero to nine with zero being a not-quite-black version of the color and nine being a not-quite-white version.&lt;/p&gt;
&lt;p&gt;This effectively gives me a wide range of colors that aren't very close to each other but still a small enough set I'm not struggling with the differences of 218° and 223°.&lt;/p&gt;
&lt;h2&gt;CSS&lt;/h2&gt;
&lt;p&gt;Because I've really embraced CSS variables instead of going directly to SASS or LESS, this entire thing uses various CSS variables such as &lt;code&gt;--color-priduck-c0b0&lt;/code&gt; and &lt;code&gt;--color-priduck-c9b9&lt;/code&gt;. They use &lt;code&gt;calc()&lt;/code&gt; and &lt;code&gt;lch()&lt;/code&gt; to build the colors from the base hue of &lt;code&gt;--color-priduck-hue&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Eventually, I hope to create some symbolic ones like &lt;code&gt;--color-priduck-comment&lt;/code&gt; or something that would allow customizations.&lt;/p&gt;
&lt;p&gt;This will also lead into the &lt;code&gt;rgb()&lt;/code&gt; colors that I need to do the Fedran covers.&lt;/p&gt;
&lt;h2&gt;Libraries&lt;/h2&gt;
&lt;p&gt;I ended up writing two little NPM packages to support these and let me try it out. These are not even remotely documented because&amp;hellip; I would consider them alpha until I get Fedran integrated with them. By then, I should have something I feel is &amp;ldquo;right&amp;rdquo; but also expand it out into the other components I need such as the color-rotation code to make the covers and other stuff.&lt;/p&gt;
&lt;p&gt;Needless to say, if I like this, it will take me about six months to a year to finish integrating it into my system. Which means I want to make sure this is the direction I want to go.&lt;/p&gt;
&lt;p&gt;I also decided to call this the Priduck Color Theme, because &amp;ldquo;priduck&amp;rdquo; sounded funny, doesn't have a lot of search hits, and it reminds me a lot of the &lt;a href="https://www.youtube.com/channel/UC1KLPSDD6JT-PptihEqAJ2Q"&gt;Useless Duck Company&lt;/a&gt;, a set of hilarious shorts about inventing.&lt;/p&gt;
&lt;h3&gt;@priduck-color-theme/base for Node&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://src.mfgames.com/priduck-color-theme/priduck-color-theme-base-js"&gt;https://src.mfgames.com/priduck-color-theme/priduck-color-theme-base-js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Naturally, trying to build all of these was a little tedious so I wrote an ad-hoc programs to generate the color schemes using CSS variables. Then put it into a NPM package because I don't like copy/pasting things when I know I'm going to be tweaking it over time (including adding symbolic colors so I can have a default colors for language keywords, surfaces, and accents).&lt;/p&gt;
&lt;p&gt;This lets me just include it into the CSS files as needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;@import &amp;quot;~@priduck-color-theme/base/colors.css&amp;quot;;

:root {
    --color-priduck-hue: 220;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@priduck-color-theme/theme for Node&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://src.mfgames.com/priduck-color-theme/priduck-color-theme-theme-js"&gt;https://src.mfgames.com/priduck-color-theme/priduck-color-theme-theme-js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I also wrote another because there isn't a good way to write a DRY version of CSS themes that can handle default values, the browser providing &lt;code&gt;prefers-color-scheme&lt;/code&gt; and &lt;code&gt;prefers-contrast&lt;/code&gt;, and setting them via attributes in various combinations. So I wanted to be able to provide six files (dark/light and more/less contrast for both of those).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Re: Unsolicited opinions about CLI design</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/01/06/re-opinions-on-clis/" />
    <updated>2024-01-06T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/01/06/re-opinions-on-clis/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="cli" scheme="https://d.moonfire.us/tags/" label="CLI" />
    <summary type="html">Over on Gemini, there was a recent post about CLI design by Lark that caught my attention this morning about opinions on CLI design and I wanted to add my two cents.
</summary>
    <content type="html">&lt;p&gt;This week is one of those &amp;ldquo;little things&amp;rdquo; week where I get to do fun things, work on the little broke things around the house, and just relax. It also means I get more verbose and start doing blog posts because why not?&lt;/p&gt;
&lt;p&gt;Over on Gemini, there was a &lt;a href="gemini://lark.gay/posts/cli-opinions.gmi"&gt;recent post about CLI design&lt;/a&gt; by Lark that caught my attention this morning. Well, and one about last names, but that is a much different topic.&lt;/p&gt;
&lt;p&gt;As such, I just had a long conversation with one of my developers about our semi-annual goals. They wanted to document our primary CLI and asked me about opinions of their tasks. Ultimately, I suggested that their idea of creating a document that lists every option would be ultimately useless, expanding on the help from inside the CLI would be beneficial.&lt;/p&gt;
&lt;p&gt;This also gives me the impetus to talk about some of my own evolving opinions about CLI.&lt;/p&gt;
&lt;h2&gt;External Documentation&lt;/h2&gt;
&lt;p&gt;I'll start with external documentation. Our application, call it &lt;code&gt;bob&lt;/code&gt;, is a command-line that reuses the same business logic as our front end and services. It is intended to be a &amp;ldquo;big&amp;rdquo; one which means it has nested commands for individual tasks.&lt;/p&gt;
&lt;p&gt;Many of those tasks are written because we need to solve a problem Right Now™. Other are the tasks because upper management has, in their infinite wisdom, separated our DBAs into a separate team which handles all the database woes of our company and we no longer have the dedicated individual that we've enjoyed for thirteen years. Since there is now a 1-3 hour delay on getting a DBA, they have to follow a strict system of access, and I have to puppet (re, take over their screen and type because I can't have direct access and they don't know the system or type quickly), I've been writing tools for things that I can programmatically do through our normal users.&lt;/p&gt;
&lt;p&gt;This means that any external documentation is going to go stale. In a perfect world, I would write formal documentation as I code, but I also make a point of documenting every argument and command line as I go, so I'd rather have the &lt;code&gt;bob&lt;/code&gt; handle the documentation instead of trying to also update our internal wiki, which customers don't have access to despite the fact we ship &lt;code&gt;bob&lt;/code&gt; with our product also (for the same reasons I need it).&lt;/p&gt;
&lt;h2&gt;No Arguments&lt;/h2&gt;
&lt;p&gt;I completely agree with the &lt;code&gt;-long&lt;/code&gt; argument name that &lt;code&gt;find&lt;/code&gt; and PowerShell uses. It annoys the hell out of me. I also hate the &lt;code&gt;/v&lt;/code&gt; of other Windows programs.&lt;/p&gt;
&lt;p&gt;Same with exit codes. Different codes are awesome, when they are used properly. I'm looking at you, MadCap Flare.&lt;/p&gt;
&lt;h2&gt;Discovering&lt;/h2&gt;
&lt;p&gt;I think it would be obvious, but &lt;code&gt;bob&lt;/code&gt; is inspired by &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;az&lt;/code&gt; (thought more toward Azure's CLI). It uses nested verbs and they can go down reasonably deep.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;az account list
bob direct property calculate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In both cases, I want the container verbs (&lt;code&gt;az account&lt;/code&gt;, &lt;code&gt;bob direct&lt;/code&gt;, &lt;code&gt;bob direct property&lt;/code&gt;) to provide a short help, maybe some examples, than do a default behavior. This is because people who don't know the system need to be able to explore, but also know that there is more.&lt;/p&gt;
&lt;p&gt;This is contrast to &lt;code&gt;git remote&lt;/code&gt; which lists the remote, but &lt;code&gt;git remote add&lt;/code&gt; adds remotes. There is no &lt;code&gt;git remote list&lt;/code&gt; or &lt;code&gt;git remote ls&lt;/code&gt;, so that makes it hard to discover what the tool can do because there is no obvious indicator that there are more nested verbs.&lt;/p&gt;
&lt;p&gt;The Gitea CLI, &lt;code&gt;tea&lt;/code&gt;, also does this but in a slightly different manner. Both &lt;code&gt;tea repo&lt;/code&gt; and &lt;code&gt;tea repo ls&lt;/code&gt; both list repositories but &lt;code&gt;tea repo create&lt;/code&gt; will create a new one. That means if someone enters &lt;code&gt;tea repo&lt;/code&gt;, there is nothing to indicate the create exists but at least there is a way to explicitly list repositories.&lt;/p&gt;
&lt;p&gt;Since I value exploration and curiosity, I strongly lean toward container verbs are for discovery, not functionality. Ideally, a command that just gives input should also have a dedicated exit code (253 in most of my programs) for &amp;ldquo;showed help&amp;rdquo;.&lt;/p&gt;
&lt;h3&gt;Breaking Changes&lt;/h3&gt;
&lt;p&gt;The problem with the above statement is when a command needs to be broken into separate bits. For example, we used to have &lt;code&gt;bob direct export&lt;/code&gt; to import data directly into the database (our &lt;code&gt;direct&lt;/code&gt; commands are straight to the DB, our &lt;code&gt;api&lt;/code&gt; commands use the OpenAPI layer). However, we split export into export data and export tables (&lt;code&gt;bob direct export table&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;What about the existing scripts that made the assumption about &lt;code&gt;bob direct export&lt;/code&gt;? I don't want to be a versioning layer on our CLI and our customers don't understand &lt;a href="https://semver.org/"&gt;semantic versioning&lt;/a&gt; even if we used it (business insists on &lt;a href="https://dafoster.net/articles/2015/03/14/semantic-versioning-vs-romantic-versioning/"&gt;romantic versioning&lt;/a&gt;). I also don't want to limit our ability to evolve as our understanding of the tools and how they work changes.&lt;/p&gt;
&lt;p&gt;Ultimately, I go with three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plan ahead and add the nested verb if I think we're going to need it.&lt;/li&gt;
&lt;li&gt;Reorganize so we don't have to, which is why we have &lt;code&gt;bob direct criteria export&lt;/code&gt; and &lt;code&gt;bob direct property export&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Just break it (bump the major version if you can) and document the change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Levels of Help&lt;/h2&gt;
&lt;p&gt;I prefer three levels of help: synopsis, option, and verb.&lt;/p&gt;
&lt;p&gt;Synopsis is when you just run a program and it doesn't include the requirement arguments. Just give a little summary of what is needed. Depending on the parsing library, I'd rather see most command options, a list of verbs and what they do.&lt;/p&gt;
&lt;p&gt;Option is when someone passes in &lt;code&gt;--help&lt;/code&gt; (I do not like using &lt;code&gt;-h&lt;/code&gt; for help). That should be the list of all verbs, options, and arguments and details.&lt;/p&gt;
&lt;p&gt;This is where my opinions different from Lark. I hate when &lt;code&gt;git clone --help&lt;/code&gt; opens a pager. I know what I'm doing and either I'm passing it already through a pager of my choice or I'm scanning because PowerShell is slow enough I can read almost as fast as it prints. I want it to dump data, I want to be able to scroll up as I need. Needless to say, I despise that &lt;code&gt;git clone --help&lt;/code&gt; opens up a web browser on Windows. Switching programs is the last thing I want to do and it makes me feel like I'm losing control.&lt;/p&gt;
&lt;p&gt;Also, if I touch the mouse, I failed. And browsers on Windows almost always require mice touching.&lt;/p&gt;
&lt;p&gt;On the other hand, if it is given as a command or verb, such as &lt;code&gt;git clone help&lt;/code&gt;, then I'm okay with a novel-length list of help files and throwing it into a pager is fine. I want the help verb to be the full details with examples, discussions, and links. Need more? Then make &lt;code&gt;help&lt;/code&gt; have nested verbs that let me discover the detailed topics (but please no interactive exploration).&lt;/p&gt;
&lt;p&gt;That means I'm also okay with the example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ go get --help
usage: go get [-t] [-u] [-v] [build flags] [packages]
Run 'go help get' for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Though I would have preferred that &lt;code&gt;go get&lt;/code&gt; did the synopsis given above, &lt;code&gt;go get --help&lt;/code&gt; gave the synopsis and explained what &lt;code&gt;-t&lt;/code&gt;, &lt;code&gt;-u&lt;/code&gt;, and &lt;code&gt;-v&lt;/code&gt; are, and &lt;code&gt;go help get&lt;/code&gt; (I'd rather it be &lt;code&gt;go get help&lt;/code&gt; though) gave details like modules, examples, etc.&lt;/p&gt;
&lt;p&gt;I believe that &lt;code&gt;go get&lt;/code&gt; does something by itself, but I'd rather require an option that says &amp;ldquo;get all&amp;rdquo; (which is what I assume it does&amp;quot;) instead of inferring it, but that is a point I feel strongly about. If &lt;code&gt;go get&lt;/code&gt; does something, then skip that but my opinions that &lt;code&gt;go get --help&lt;/code&gt; should at least list the verbs and explain the options.&lt;/p&gt;
&lt;p&gt;Related to that, it frustrates me when &lt;code&gt;--help&lt;/code&gt; does not show help screens. I don't care if I have every argument and option set, &lt;code&gt;--help&lt;/code&gt; should always take priority.&lt;/p&gt;
&lt;h2&gt;Arguments and Options&lt;/h2&gt;
&lt;p&gt;There is one thing I struggle with the &lt;a href="https://clig.dev/#arguments-and-flags"&gt;CLI design&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prefer flags to args. It’s a bit more typing, but it makes it much clearer what is going on. It also makes it easier to make changes to how you accept input in the future. Sometimes when using args, it’s impossible to add new input without breaking existing behavior or creating ambiguity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There isn't a good way of knowing when an flag is required. No standard convention that says &amp;ldquo;you must have this&amp;rdquo; because we are so inconsistent with indicating optional verses not optional. I guess if the synopsis said &lt;code&gt;gary [--verbose] --input FILE&lt;/code&gt; maybe?&lt;/p&gt;
&lt;p&gt;But this is one that I need to work out in my head because I usually equate positions are required, flags are optional. But, intellectually, I agree with the statement, but it isn't what I'm doing these days.&lt;/p&gt;
&lt;p&gt;After thinking about it, I think I want to change to follow this one more.&lt;/p&gt;
&lt;h2&gt;Data Type of Options and Arguments&lt;/h2&gt;
&lt;p&gt;This is one of my frustrations, when I don't know if a flag takes a value or not, and what type of value does it take. In our system, we identify files or the type of input.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ bob direct user list --help
...
--no-color
--table-search REGEX
--output FILENAME
--user-search LOOKUP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&amp;ldquo;LOOKUP&amp;rdquo; has a special meaning for us such as &lt;code&gt;starts:XXX&lt;/code&gt;, &lt;code&gt;regex:XXX&lt;/code&gt;, &lt;code&gt;id:999&lt;/code&gt;, &lt;code&gt;key:XXXX&lt;/code&gt;, and &lt;code&gt;contains:XXX&lt;/code&gt; verses &lt;code&gt;XXX&lt;/code&gt; which is a case-insensitive exact match.)&lt;/p&gt;
&lt;p&gt;There are cases when I want to pass in the value, so there is a different between &lt;code&gt;--foo&lt;/code&gt; and &lt;code&gt;--foo yes&lt;/code&gt; and the help should say that. Also, using a generic value, like &lt;code&gt;tea&lt;/code&gt; does is frustrating.'&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ git repo create --help
...
   --gitignores value, --git value    list of gitignore templates (need --init)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Um, what is the value? And the help isn't what it does. It doesn't list the templates, it needs to have a template set but it doesn't tell me where to get a list of the templates.&lt;/p&gt;
&lt;h2&gt;Terminal Columns&lt;/h2&gt;
&lt;p&gt;Another frustration is that I don't like fancy tables in my CLI help. It might sound strange, but creating a nicely formatted table is great when you use standard columns:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ tea repo create --help
...
   --branch value                     use custom default branch (need --init)
   --description value, --desc value  add description to repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, saw you are jamming a narrow column of shell because your editor needs the bulk of the window but you need to handle some sidebar tasks or are writing code to use that CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ tea repo create --help
...
   --branch value                     use custom def
ault branch (need --init)
   --description value, --desc value  add descriptio
n to repo
   --gitignores value, --git value    list of gitign
ore templates (need --init)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... yeah, that isn't really readable. Wrapping stuff is a problem unless you take into account the number of columns that are currently on the screen. This is somewhere either the default needs to be screen wrapping, or we need a reactive formatting.&lt;/p&gt;
&lt;p&gt;In a &amp;ldquo;perfect&amp;rdquo; world, I'd rather the narrow one look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tea repo create --help --faked
--branch value
  use custom default branch (need --init)
--description value, --des value
  add description to repo
--gitignores value, --git value
  list of gitignore templates (need --init)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, infrequently options should be longer and or multiple aliases mean there is a bigger chance of word-wrapping. Overall, &lt;code&gt;ripgrep&lt;/code&gt; has a nicer default format for what I'm looking for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ rg --help
    --no-config
        When set, ripgrep will never read configuration files. When this flag
        is present, ripgrep will not respect the RIPGREP_CONFIG_PATH
        environment variable.

        If ripgrep ever grows a feature to automatically read configuration
        files in pre-defined locations, then this flag will also disable that
        behavior as well.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But even that doesn't handle columns well. I'd love to see a better convention here, even if it comes down to when everything should just be on a single line and let the terminal wrap.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ bob --help
--log-level LEVEL, -l LEVEL

Sets the logging level for the
console output. Possible optio
ns are: Error, Warning, Info, 
Debug, Verbose. Case-insensiti
vie and shortened values allow
ed. Default: $BOB_LOG_LEVEL, I
nfo.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since it usually comes up, I can't always zoom out the terminal and make the text smaller because a certain point, the blur radius from my eye surgery causes everything to turn into a muddled mess of colors.&lt;/p&gt;
&lt;h2&gt;Environment Variables&lt;/h2&gt;
&lt;p&gt;If an flag/option is driven by an environment variable (then it isn't a argument/positional), then list that in the &lt;code&gt;--help&lt;/code&gt;. I believe the Woodpecker CLI does that (but I don't have it installed right now), but seeing something like this is nice:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ bob direct property export --help
...
--connection DOTNET-SQL-CONNECTION The connection string to connect to
  the primary database. Value comes from $BOB_CONNECTION_STRING,
  &amp;quot;ConnectionString&amp;quot; from bob.config. Required.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That makes it a lot easier to understand, more so when the order of processing is also listed. If I'm using the example of &lt;code&gt;--help&lt;/code&gt; verses &lt;code&gt;help&lt;/code&gt;, those additional details are better suited for the command instead of the option.&lt;/p&gt;
&lt;h2&gt;Plural verses Singular&lt;/h2&gt;
&lt;p&gt;Singular. The same with my REST opinions. &lt;code&gt;tea&lt;/code&gt; uses both and it looks wrong to me:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tea --help
...
     issues, issue, i                  List, create and update issues
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Colors and Emojis&lt;/h2&gt;
&lt;p&gt;This is a hard one because I love and hate colors. Pretty colors are great, but I use terminals with black background, ones with light ones, and PowerShell with its hideous blue background. Sooner or later, a command with color screws up one of them because after decades of writing CLIs, we haven't come up with a convention to provide color preferences via environment variables.&lt;/p&gt;
&lt;p&gt;Also, a decent percentage of the population is color blind. I also find that I stop processing color in certain situations and start seeing things in desaturated colors. So, as much as I want to see all those pretty colors in output, either don't touch my colors so I can use whatever background I want or come up with a &lt;a href="xkcd://927"&gt;standard&lt;/a&gt; that everyone follows.&lt;/p&gt;
&lt;p&gt;I also love emojis, but there is a disconnect when some are black and white and others have full color. They are also dependant on fonts on the terminal.&lt;/p&gt;
&lt;h2&gt;Logging&lt;/h2&gt;
&lt;p&gt;Remarkably, logging flags cause me a lot of stress. As I see it, there are two philosophies when it comes to logging: flags or option.&lt;/p&gt;
&lt;p&gt;Flags is when you have &lt;code&gt;--verbose&lt;/code&gt; and &lt;code&gt;--quiet&lt;/code&gt;. The problem is the conflict. What if you provide both? Should one take priority? Should it blow up with an invalid options selected? Mutually exclusive options for those libraries who handle it? And then there is the &lt;code&gt;-vvvvvvv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The other is an option, such as a lot of Microsoft tools use (also &lt;code&gt;woodpecker-cli&lt;/code&gt;), which is a &lt;code&gt;--log-level LEVEL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Lately, I've been leaning toward the &lt;code&gt;--log-level&lt;/code&gt; approach, but give it options. Since I use &lt;a href="https://serilog.net/"&gt;Serilog&lt;/a&gt; heavily and my users are sloppy, I like the level to be as flexible as possible. So, while I might say &amp;ldquo;error, warning, info, debug, verbose&amp;rdquo; (I always think debug and verbose should be reversed with Serilog), I want them to be able to say &lt;code&gt;--log-level e&lt;/code&gt; or &lt;code&gt;--log-level ERR&lt;/code&gt; because that's how they think.&lt;/p&gt;
&lt;p&gt;I also frequently use the same tools in a monitored environment as my local machine, so occasionally I want more details in the log messages. That is also why I want to see more &lt;code&gt;--log-format FORMAT&lt;/code&gt; where format can be one-line JSON, plain, with full timestamps, extra details, etc.&lt;/p&gt;
&lt;p&gt;Finally, I have a common need to have log files written out to a text file. Usually this is &lt;code&gt;--log-file FILENAME&lt;/code&gt;, but that format and level should be independently configurable. &lt;code&gt;--log-file-format&lt;/code&gt; and &lt;code&gt;--log-file-level&lt;/code&gt; with the same options, defaulting to the top level ones if not provided.&lt;/p&gt;
&lt;p&gt;Of course, if &lt;code&gt;--verbose&lt;/code&gt; is just an alias for &lt;code&gt;--log-level verbose&lt;/code&gt;, that would be fine also. If I had to pick common or uncommon options, I would pick one set for the common.&lt;/p&gt;
&lt;h2&gt;Consistency&lt;/h2&gt;
&lt;p&gt;There is a bunch in here. Not all of them make sense for everyone, but they make sense for me. That comes down to the Standards Problem so I can't really say if they are &amp;ldquo;good&amp;rdquo; opinions or not, just my opinions and ones that I like and ones that I don't like.&lt;/p&gt;
&lt;p&gt;But I like talking about it because it helps refine my opinions and find new ideas that I have never thought about.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Teaching NixOS about OpenTofu</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/01/05/teaching-nixos-about-opentofu/" />
    <updated>2024-01-05T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/01/05/teaching-nixos-about-opentofu/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="opentofu" scheme="https://d.moonfire.us/tags/" label="OpenTofu" />
    <category term="pulumi" scheme="https://d.moonfire.us/tags/" label="Pulumi" />
    <category term="dreamhost" scheme="https://d.moonfire.us/tags/" label="DreamHost" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="just" scheme="https://d.moonfire.us/tags/" label="Just" />
    <category term="deltachat" scheme="https://d.moonfire.us/tags/" label="DeltaChat" />
    <summary type="html">In my endless quest to come up with a completely data-driven and reproducible environment, I decided to take a stab at a new instance automation tool: OpenTofu.
</summary>
    <content type="html">&lt;p&gt;In my endless quest to come up with a completely data-driven and reproducible environment, I decided to take a stab at a new automation tool: &lt;a href="https://opentofu.org/"&gt;OpenTofu&lt;/a&gt;. I've already gotten a good &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; setup, but I wanted to also be able to check in the setup for my instances (and to a smaller degree, my bare metal servers in my home lab) to expand on the functionality. It didn't hurt that work had settled on Terraform.&lt;/p&gt;
&lt;p&gt;Previously, I had taken a stab at &lt;a href="https://www.pulumi.com/"&gt;Pulumi&lt;/a&gt; (during my wedding anniversary trip in 2022). It was fun and I liked the code but I ended up gutting it later. For some reason, it quickly ended up feeling like a chore to play with. At that point, I figured I would just do things manually. But then I saw the announcement that OpenTofu had forked from Terraform because of enshittification of licenses (develop with an open license, then switch to a more limited one once profits became important). That little thing set me off and I decided to try it out.&lt;/p&gt;
&lt;h2&gt;Installing&lt;/h2&gt;
&lt;p&gt;Since all my infrastructure code is in a Nix flake, to get started just required me to add to the shell's packages.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;devShell.${system} = pkgs.mkShell {
  buildInputs = [
    pkgs.just
    pkgs.opentofu
    pkgs.openstackclient
  ];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also grabbed the OpenStack client because it made easier to find some of the nasty little identifiers I needed to import.&lt;/p&gt;
&lt;h2&gt;Configuration Files&lt;/h2&gt;
&lt;p&gt;The way tofu works, it grabs all the &lt;code&gt;*.tf&lt;/code&gt; files in the same directory. So inside my infrastructure flake, I have a &lt;code&gt;src/tofu&lt;/code&gt; directory with configuration files that make sense to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;000-providers.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;050-secrets.tf&lt;/code&gt; (.gitignored)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;050-secrets.tf.enc&lt;/code&gt; (SOPS encrypted based on my user key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;200-networks.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500-instance0.tf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500-instance1.tf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of them are picked up, merged together, and made into a single set of settings. I also use &lt;code&gt;tofu fmt&lt;/code&gt; a lot since I like to normalize my files on every commit.&lt;/p&gt;
&lt;h2&gt;OpenStack and DreamHost&lt;/h2&gt;
&lt;p&gt;Fortunately, my hosting provider of choice is &lt;a href="https://dreamhost.com/"&gt;DreamHost&lt;/a&gt;. They aren't the cheapest or the best, but they appear to be ethetical. Mostly, I stick with them because they went to the court to &lt;a href="https://techfreedom.org/victory-online-political-free-speech-dreamhost-case/"&gt;fight some overreaching gag orders&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;(I also tried DigitalOcean at the same time as Pulumi but dropped that also.)&lt;/p&gt;
&lt;p&gt;OpenTofu (via Terraform plugins) does a wonderful job of supporting both OpenStack and DreamHost DNS to tie everything together.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform {
  required_version = &amp;quot;&amp;gt;= 0.14.0&amp;quot;

  required_providers {
    dreamhost = {
      source  = &amp;quot;adamantal/dreamhost&amp;quot;
      version = &amp;quot;0.3.2&amp;quot;
    }

    openstack = {
      source  = &amp;quot;terraform-provider-openstack/openstack&amp;quot;
      version = &amp;quot;~&amp;gt; 1.53.0&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I added the &lt;code&gt;adamantal/dreamhost&lt;/code&gt; plugin so I could also assign the DNS record directly from my script and have everything working.&lt;/p&gt;
&lt;h2&gt;Secrets&lt;/h2&gt;
&lt;p&gt;Even though my infrastructure flake is in a private repository, I still encrypt all my secrets. I use SOPS for this, which means setting up the &lt;code&gt;.sops.yaml&lt;/code&gt; file to encrypt and then decrypting/encrypting files using `just``:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;decrypt: decrypt-clouds decrypt-secrets

decrypt-clouds:
    if [ ! -f clouds.yaml ];then sops -d clouds.yaml.enc &amp;gt; clouds.yaml;fi

encrypt-clouds:
    cp clouds.yaml clouds.yaml.enc
    sops -i -e clouds.yaml.enc

decrypt-secrets:
    if [ ! -f 050-secrets.tf ];then sops -d 050-secrets.tf.enc &amp;gt; 050-secrets.tf;fi

encrypt-secrets:
    cp 050-secrets.tf 050-secrets.tf.enc
    sops -i -e 050-secrets.tf.enc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do the same with my &lt;code&gt;.env&lt;/code&gt; file so I can get the information I need set up properly.&lt;/p&gt;
&lt;h2&gt;Creating an Instance&lt;/h2&gt;
&lt;p&gt;Here is a short segment for creating an instance on DreamHost (or most OpenStack providers).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &amp;quot;openstack_compute_instance_v2&amp;quot; &amp;quot;instance1&amp;quot; {
  provider    = openstack.dreamhost
  name        = &amp;quot;instance1&amp;quot; // It really isn't instance1, but just pretend
  key_pair    = &amp;quot;keypair1&amp;quot; // This is my SSH key set up somewhere else
  flavor_name = &amp;quot;gp1.supersonic&amp;quot; // gp1.supersonic means I don't need a swap disk

  user_data = &amp;lt;&amp;lt;-EOT
    #cloud-config
    runcmd:
      - curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIX_CHANNEL=nixos-unstable bash 2&amp;gt;&amp;amp;1 | tee /tmp/infect.log
    EOT

  # This sets up the boot device as /
  block_device {
    source_type           = &amp;quot;image&amp;quot;
    uuid                  = &amp;quot;2b2c61c6-324c-47f4-88c1-9ae8a978ddfd&amp;quot; # Ubuntu
    boot_index            = 0
    delete_on_termination = true
    destination_type      = &amp;quot;volume&amp;quot;
    multiattach           = false
    volume_size           = 80
  }

  network { // Also configured somewhere else
    name = openstack_networking_network_v2.public.name
  }
}

resource &amp;quot;dreamhost_dns_record&amp;quot; &amp;quot;instance1&amp;quot; {
  record = &amp;quot;instance1.mfgames.com&amp;quot;
  value  = openstack_compute_instance_v2.instance1.network[0].fixed_ip_v4
  type   = &amp;quot;A&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For me, the part is really cool is that I can bake in the &lt;a href="https://github.com/elitak/nixos-infect"&gt;NixOS infect&lt;/a&gt; script right in. To my surprise, it just ran the first time without errors (thought I had to wait about ten minutes after OpenTofu said it was done).&lt;/p&gt;
&lt;p&gt;All I had to do was either show the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;tofu plan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or apply the changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;tofu apply
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Importing an Instance&lt;/h2&gt;
&lt;p&gt;Actually, the first thing I did was import my existing instances into the system. This involves creating a &lt;code&gt;.tf&lt;/code&gt; file with the same basic setup at the other instance (some fields can be skipped but I was still learning), then import with the ID from OpenStack. Of course, getting the IDs was the hard part. Fortunately, this is where the OpenStack client comes into play. I can use that to get the list of servers, figure out the ID, then import it into Tofu.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ openstack --os-cloud dreamhost server list
+--------------------------------------+-----------+--------+------------+--------------------------+----------------+
| ID                                   | Name      | Status | Networks   | Image                    | Flavor         |
+--------------------------------------+-----------+--------+------------+--------------------------+----------------+
| 55f8ee35-31b2-4137-af1d-b7597d348271 | instance0 | ACTIVE | public=*** | N/A (booted from volume) | gp1.supersonic |
| 1a38092b-bbc5-46bd-9092-0df979ca8fe4 | instance1 | ACTIVE | public=*** | N/A (booted from volume) | gp1.supersonic |
$ tofu import openstack_compute_instance_v2.instance0 55f8ee35-31b2-4137-af1d-b7597d348271
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NixOS&lt;/h2&gt;
&lt;p&gt;Now, while this was great for setting up things, I also wanted to pull that data into my Nix infrastructure flake. Fortunately, OpenTofu has a way of exporting the data pulled from the cloud. To do that, I need to add an &lt;code&gt;output&lt;/code&gt; stanza at the bottom of my &lt;code&gt;500-instance1.tf&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;output &amp;quot;instance1_ipv4&amp;quot; {
  value = openstack_compute_instance_v2.instance1.network[0].fixed_ip_v4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I'm (recently) fond of using &lt;a href="/tags/just/"&gt;Just&lt;/a&gt; for automation, I banged up a little stanza that automatically creates a &lt;code&gt;default.nix&lt;/code&gt; file inside that directory every time I apply.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apply: decrypt format &amp;amp;&amp;amp; export
    tofu apply

format:
    tofu fmt

plan: decrypt format
    tofu plan

export:
    echo &amp;quot;inputs: {&amp;quot; &amp;gt; default.nix
    tofu output | sort | perl -ne 'chomp;s@_@.@g;print &amp;quot;  $_;\n&amp;quot;' &amp;gt;&amp;gt; default.nix
    echo &amp;quot;}&amp;quot; &amp;gt;&amp;gt; default.nix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had to use &lt;code&gt;_&lt;/code&gt; in the output since dotted notation isn't accepted, but I use &lt;code&gt;perl&lt;/code&gt; to convert those underscores into Nix-happy format.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs: {
  instance0.ipv4 = &amp;quot;1.2.3.4&amp;quot;;
  instance1.ipv4 = &amp;quot;2.3.4.5&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use this to pull into my &lt;code&gt;networking.nix&lt;/code&gt; which is used to drive things like configuring AdGuard, services like Maddy (for DeltaChat) and other services.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs:
let
  tofu = import ./tofu/default.nix { };
in
{
  instance0 = tofu.instance0;
  instance1 = tofu.instance1;
  instance2.ipv4 = &amp;quot;192.168.0.2&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there, I have a single place to get all my IP addresses:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs:
let
  ip = (import ../../../../networks.nix { }).instance0.ipv4;
in
{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It isn't the best or most graceful way of doing things, but I'm pretty happy how everything turned out. I made a few mistakes along the way of setting up Gitea Actions and had to drop and rebuild my instance0. That was just a matter of renaming &lt;code&gt;500-instance0.tf&lt;/code&gt;, applying to drop, and then name the file back. Then I had nice clean slate to push out a new closure.&lt;/p&gt;
&lt;p&gt;OpenTofu is much nicer than Pulmui. It didn't insist on having a cloud to maintain state, the file is checked into Git instead. It has a declarative language instead of code, and since I really don't need a lot of that logical flow, it just works for me. Plus I was able to inject into my &lt;code&gt;just deploy&lt;/code&gt; top-level script that pushes out changes to my home lab and all my instances in a single call.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Passing NixOS Flake Inputs Through Colmena</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/12/03/passing-nixos-flake-inputs-through-colmena/" />
    <updated>2023-12-03T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/12/03/passing-nixos-flake-inputs-through-colmena/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="colmena" scheme="https://d.moonfire.us/tags/" label="Colmena" />
    <summary type="html">How to pass a flake input from a top-level infrastructure flake into the individual nodes (servers) so a single lock file can handle all changes.
</summary>
    <content type="html">&lt;p&gt;I'm pass the one year anniversary of using &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; and I'm still learning a lot about the language and infrastructure. For the most part, I still feel good about the results even with my troubles playing video games and my underpowered hardware.&lt;/p&gt;
&lt;p&gt;In the beginning, I also decided to go &amp;ldquo;all in&amp;rdquo; on Flakes. They covered a lot of the things I wanted in Nix, has the isolation that played well with my writing and programming environment. They were also barely understood, shiny, and I like taking the hardest path possible.&lt;/p&gt;
&lt;p&gt;Once I realized I liked NixOS, I started putting it on other machines. For that, I settled on &lt;a href="https://github.com/zhaofengli/colmena"&gt;Colmena&lt;/a&gt;. It had pretty output and, for the most part, did what I wanted instead of the more complex systems that I couldn't understand due to my low skill in the Nix language.&lt;/p&gt;
&lt;h2&gt;Flake Inputs&lt;/h2&gt;
&lt;p&gt;One thing that had been vexing me for the last year is how to pass a flake input from infrastructure project (the repository that builds out all the servers) into the individual nodes (servers). This would let me install custom tools on servers or just use something outside of Nixpkgs that wouldn't require me to hunt down hashes every time the project built. Eventually, I hope to include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Author Intrusion (if it ever works)&lt;/li&gt;
&lt;li&gt;Fedran CLI&lt;/li&gt;
&lt;li&gt;FlakeHub (current curiosity)&lt;/li&gt;
&lt;li&gt;Catappuccin theme for Gitea&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since I was messing with NixOS over the last few days, I decided to make another attempt at solving the problem. A few questions on Matrix didn't answer, so I decided that a real-time chat isn't the best place for this type of question and went with the NixOS Discourse and &lt;a href="https://discourse.nixos.org/t/colmena-how-to-push-a-flake-input-into-the-individual-nodes/36332"&gt;posted there&lt;/a&gt;. Lo and behold, someone answered with exactly the answer I needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# flake.nix
{
  # An example flake to pass into the node.
  inputs.fh.url = &amp;quot;https://flakehub.com/f/DeterminateSystems/fh/*.tar.gz&amp;quot;;

  outputs = inputs @ {self, ...}: let
    system = &amp;quot;x86_64-linux&amp;quot;;
    pkgs = import inputs.nixpkgs {inherit system;};
  in rec {
    colmena = {
      meta = {
        # Inject the inputs for the top-level flake into a variable so the
        # nodes can pull them down consistently. I renamed this to flakes
        # instead of inputs since I always use `inputs` for my parameters
        # for nested Nix files.
        specialArgs.flakes = inputs; = {
          flakes = inputs;
          system = system; # I cannot figure out the &amp;quot;correct&amp;quot; way to do this yet.
        };
      };

      machine-a = import ./machine-a.nix;
    };
  }
}

# machine-a.nix
{config, pkgs, flakes, system, ...}: {
  environment.systemPackages = [
    flakes.fh.packages.${system}.default
  ]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nix as a Language&lt;/h2&gt;
&lt;p&gt;I still don't really &amp;ldquo;like&amp;rdquo; Nix, but I'm getting better at it. It is one of those things that grows on you, even though I have a strong preference for statically compiled languages because they catch many trivial typos.&lt;/p&gt;
&lt;p&gt;Automated handling of trivial things has become something I need. While I have pretty good attention to detail, I don't always have the bandwidth to full focus on an item. So anything that I can do to fix those without thinking is a good thing.&lt;/p&gt;
&lt;p&gt;Trying to solve this is a good example of my problems. I spent a year trying to solve this, occasionally asking on Matrix and other places. And a GitHub issues page really isn't the best place to ask beginning questions, though I have done that in the past.&lt;/p&gt;
&lt;p&gt;I also couldn't find an example of how to do things, which is one reason why I'm creating these posts. At least then, hopefully, someone who is struggling with the same things can stumble on the answer.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Enforcing Standards with NixOS</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/12/02/enforcing-standards-with-nixos/" />
    <updated>2023-12-02T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/12/02/enforcing-standards-with-nixos/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="conform" scheme="https://d.moonfire.us/tags/" label="Conform" />
    <category term="conventional-commits" scheme="https://d.moonfire.us/tags/" label="Conventional Commits" />
    <category term="editorconfig" scheme="https://d.moonfire.us/tags/" label="EditorConfig" />
    <category term="buck2" scheme="https://d.moonfire.us/tags/" label="Buck2" />
    <category term="direnv" scheme="https://d.moonfire.us/tags/" label="direnv" />
    <summary type="html">A way of using Nix and direnv to hook up standards for formatting and conventions.
</summary>
    <content type="html">&lt;p&gt;Some time ago, I stumbled into &lt;a href="https://github.com/divnix/std"&gt;std&lt;/a&gt;, a batteries-included development stuff. It looks like something I would really like to get into, mainly because it gave off notes of &lt;a href="https://buck2.build/"&gt;Buck2&lt;/a&gt;, which is something that interests me when dealing with microservice ecosystems and polyglot frameworks. And I know I love a polyglot solution to problems.&lt;/p&gt;
&lt;p&gt;There were a few things that I fought again. I didn't care for the menu system that always shows up (noise), it's instability (still alpha), and the difficulty getting it to work with my way of thinking. I could have worked on some of those and figure out how to accept what I couldn't change and alter what I needed to be productive.&lt;/p&gt;
&lt;p&gt;I don't really have that energy at the moment. I'm painfully aware that my time and attempt budget has been eroded by my family, drama, and the other things going on in my life. I find that I don't have the energy to do much and getting std to play with me was one of those things I decided to bump.&lt;/p&gt;
&lt;h2&gt;Automation Tools&lt;/h2&gt;
&lt;p&gt;However, there were some things I really liked about std that I wasn't aware of, namely &lt;a href="https://github.com/nix-community/nixago"&gt;Nixago&lt;/a&gt; which is a way of having the shell hook of a Nix setup automatically write out the various configuration files for things like &lt;a href="https://github.com/siderolabs/conform"&gt;Conform&lt;/a&gt; for Git messages (I like my conventional commits), &lt;a href="https://editorconfig.org/"&gt;EditorConfig&lt;/a&gt; for formatting, and &lt;a href="https://github.com/evilmartians/lefthook"&gt;Lefthook&lt;/a&gt; to make sure everything is honored. Standard also taught me about &lt;a href="https://github.com/numtide/treefmt"&gt;Treefmt&lt;/a&gt; which is a single command to reformat a command base.&lt;/p&gt;
&lt;p&gt;There was also a way of doing arbitrary configurations, such as maybe setting up my project configuration files or handling other things, but I couldn't figure out how with a cursory look.&lt;/p&gt;
&lt;p&gt;In short, all of those things I like to handle automatically instead of remembering all the little details.&lt;/p&gt;
&lt;p&gt;With me getting rid of std, I wanted to keep this. Ideally in a manner that I could eventually create a flake of my common configurations and then apply them to every story or programming project.&lt;/p&gt;
&lt;h2&gt;Necessity Calls&lt;/h2&gt;
&lt;p&gt;Getting rid of std meant I hand to figure it out. Last night, I sat down and went through the code with my growing skill at Nix (I still do not enjoy the language but I'm getting more fluent with it). I'm also messing with &lt;a href="https://flakehub.com/"&gt;FlakeHub&lt;/a&gt; so you'll see some elements from that library.&lt;/p&gt;
&lt;p&gt;I already use &lt;a href="https://direnv.net/"&gt;direnv&lt;/a&gt; for setting up my flakes as I enter directories. That is part of my normal tool set and I plan on using that for a great deal of time.&lt;/p&gt;
&lt;h2&gt;Layout&lt;/h2&gt;
&lt;p&gt;I like small, individual files. Naturally this means I would like every automated system to have its own file but grouped together in a folder to make it obvious how they are used. (Needless to say, I don't advocate &lt;a href="https://medium.com/lost-but-coding/in-programming-folder-structure-doesnt-matter-as-much-as-you-think-71deecca6028"&gt;coding without folders&lt;/a&gt; but that is also how I work/)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ find src -type d
src
src/configs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Inputs&lt;/h2&gt;
&lt;p&gt;The first part is pulling in the inputs for Nixago and its extensions.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# flake.nix
{
  inputs = {
    nixpkgs.url = &amp;quot;https://flakehub.com/f/NixOS/nixpkgs/*.tar.gz&amp;quot;;

    nixago.url = &amp;quot;github:jmgilman/nixago&amp;quot;;
    nixago.inputs.nixpkgs.follows = &amp;quot;nixpkgs&amp;quot;;

    nixago-exts.url = &amp;quot;github:nix-community/nixago-extensions&amp;quot;;
    nixago-exts.inputs.nixpkgs.follows = &amp;quot;nixpkgs&amp;quot;;
  };

  # Flake outputs that other flakes can use
  outputs = inputs @ { self, nixpkgs, nixago, nixago-exts }:
    let
      # This bit comes from Flakehub's init and seems to be a reasonable pattern.
      supportedSystems = [ &amp;quot;x86_64-linux&amp;quot; ];
      forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
        inherit system;
        pkgs = import nixpkgs { inherit system; };
      });
    in
    {
      devShells = forEachSupportedSystem ({ system, pkgs }:
        let
          # This pulls in the configurations from the configuration directory.
          configs = import ./src/configs/default.nix { inherit system pkgs nixago nixago-exts; };
        in
        {
          default = pkgs.mkShell {
            # Pinned packages available in the environment
            packages = with pkgs; [
              git         # Needed for life until I find something more awesome
              nixpkgs-fmt # Needed for Lefthook
              treefmt     # Needed for Lefthook
              lefthook    # Needed for Lefthook
            ];

            # Configuration setup
            shellHook = ''
              ${configs.shellHook}
              lefthook install
            '';
          };
        });
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Configurations&lt;/h2&gt;
&lt;p&gt;The basic default is just so I have a single line to configure all the libraries. This just acts as an index file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/configs/default.nix
inputs:
inputs.nixago.lib.${inputs.system}.makeAll [
  (import ./conform.nix (inputs))
  (import ./editorconfig.nix (inputs))
  (import ./lefthook.nix (inputs))
  (import ./prettier.nix (inputs))
  (import ./treefmt.nix (inputs))
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prettier&lt;/h3&gt;
&lt;p&gt;Prettier was the first one I used, since I have very little customization in it. The nixago-exts is an extension library that figures out a lot of the formats so I don't have to.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/configs/prettier.nix
inputs @ { system, nixago, nixago-exts, ... }:
nixago-exts.prettier.${system} {
  data = {
    printWidth = 80;
    proseWrap = &amp;quot;always&amp;quot;;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lefthook&lt;/h3&gt;
&lt;p&gt;Lefthook's configuration is the same, but you'll notice there is no &lt;code&gt;data =&lt;/code&gt; element like most of the others. This threw me because it is different than the others. I also found that I had to add &lt;code&gt;&amp;amp;&amp;amp; git add {staged_files}&lt;/code&gt; from most of the examples I saw others when I commit, it would reformat the code but then leave them modified for the &lt;em&gt;next&lt;/em&gt; check in. Adding the files fixes that and keeps things relatively speedy.&lt;/p&gt;
&lt;p&gt;You also can see how I refer to specific paths for the executables while cleaning up the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/configs/lefthook.nix
inputs @ { system, pkgs, nixago, nixago-exts, ... }:
nixago-exts.lefthook.${system} {
  commit-msg = {
    commands = {
      # Runs conform on commit-msg hook to ensure commit messages are
      # compliant.
      conform = {
        run = &amp;quot;${pkgs.conform}/bin/conform enforce --commit-msg-file {1}&amp;quot;;
      };
    };
  };
  pre-commit = {
    commands = {
      # Runs treefmt on pre-commit hook to ensure checked-in source code is
      # properly formatted.
      treefmt = {
        run = &amp;quot;${pkgs.treefmt}/bin/treefmt {staged_files} &amp;amp;&amp;amp; git add {staged_files}&amp;quot;;
      };
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a side note, I have not found a single &lt;em&gt;fast&lt;/em&gt; C# reformatter that only handles a few files. It has been intensely frustrating because I really like ReSharper's &amp;ldquo;Silently Clean&amp;rdquo; feature and I don't have a way of doing it from the command line in a reasonable period of time.&lt;/p&gt;
&lt;h3&gt;Conform&lt;/h3&gt;
&lt;p&gt;Conform is nice because it enforces Git commit messages for conventional commits.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/configs/conform.nix
inputs @ { system, nixago, nixago-exts, ... }:
nixago-exts.conform.${system} {
  commit = {
    header = { length = 89; };
    conventional = {
      # Only allow these types of conventional commits (inspired by Angular)
      types = [
        &amp;quot;build&amp;quot;
        &amp;quot;chore&amp;quot;
        &amp;quot;ci&amp;quot;
        &amp;quot;docs&amp;quot;
        &amp;quot;feat&amp;quot;
        &amp;quot;fix&amp;quot;
        &amp;quot;perf&amp;quot;
        &amp;quot;refactor&amp;quot;
        &amp;quot;style&amp;quot;
        &amp;quot;test&amp;quot;
      ];

      # If you want scopes, then add:
      #scopes = [&amp;quot;allows&amp;quot; &amp;quot;scopes&amp;quot; &amp;quot;here&amp;quot;];
    };
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Treefmt&lt;/h2&gt;
&lt;p&gt;Cleaning up code is something that is tedious but really needs to be done to lower the bar of allowing others into the code. It is also something that can be &amp;ldquo;mostly&amp;rdquo; automated, which I'm also in favor of. Sadly, there are gaps in the tools that I want, like a C# or Rust formatter that will organize members (such as grouping public properties together and making them alphabetical), but I can live without those.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# src/configs/treefmt.nix
inputs @ { pkgs, ... }:
let
  data = {
    formatter = {
      prettier = {
        command = &amp;quot;${pkgs.nodePackages.prettier}/bin/prettier&amp;quot;;
        options = [ &amp;quot;--write&amp;quot; ];
        includes = [
          &amp;quot;*.css&amp;quot;
          &amp;quot;*.html&amp;quot;
          &amp;quot;*.js&amp;quot;
          &amp;quot;*.json&amp;quot;
          &amp;quot;*.jsx&amp;quot;
          &amp;quot;*.md&amp;quot;
          &amp;quot;*.mdx&amp;quot;
          &amp;quot;*.scss&amp;quot;
          &amp;quot;*.ts&amp;quot;
          &amp;quot;*.yaml&amp;quot;
          #&amp;quot;*.toml&amp;quot;
        ];
        excludes = [ &amp;quot;**.min.js&amp;quot; ];
      };

      nix = {
        command = &amp;quot;nixpkgs-fmt&amp;quot;;
        includes = [ &amp;quot;*.nix&amp;quot; ];
      };
    };
  };
in
{
  # I don't understand the reason why many Nix examples define
  # the data in the let section and then just inherit it here.
  inherit data;
  output = &amp;quot;treefmt.toml&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;EditorConfig&lt;/h3&gt;
&lt;p&gt;I think one of the best things that came out of the last decade or so of coding was a slow migration to having a semi-universal file for configuring line endings, trimming white space, and the others. Also, both Microsoft and Jetbrains have embraced EditorConfig so I can check in a file that reduces the trivial (but needed) rejects for pull requests. I want more tools to use this and extend them because I don't want to bother with line indents, tabs verses spaces (tabs lost, but I've accepted spaces now), and formatting rules (braces on new lines).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# src/configs/editorconfig.nix
inputs: # I don't use the inputs, I just wanted all the calls in `default.nix` to be consistent.
let
  data = {
    root = true;

    &amp;quot;*&amp;quot; = {
      end_of_line = &amp;quot;lf&amp;quot;;
      insert_final_newline = true;
      trim_trailing_whitespace = true;
      charset = &amp;quot;utf-8&amp;quot;;
      indent_style = &amp;quot;space&amp;quot;;
      indent_size = 4;
      indent_brace_style = &amp;quot;K&amp;amp;R&amp;quot;;
      max_line_length = 80;
      tab_width = 4;
      curly_bracket_next_line = true;
    };

    &amp;quot;*.md&amp;quot; = {
      max_line_length = &amp;quot;off&amp;quot;;
    };

    &amp;quot;package.json&amp;quot; = {
      indent_style = &amp;quot;space&amp;quot;;
      indent_size = 2;
      tab_width = 2;
    };

    &amp;quot;{LICENSES/**,LICENSE}&amp;quot; = {
      end_of_line = &amp;quot;unset&amp;quot;;
      insert_final_newline = &amp;quot;unset&amp;quot;;
      trim_trailing_whitespace = &amp;quot;unset&amp;quot;;
      charset = &amp;quot;unset&amp;quot;;
      indent_style = &amp;quot;unset&amp;quot;;
      indent_size = &amp;quot;unset&amp;quot;;
    };
  };
in
{
  inherit data;
  hook.mode = &amp;quot;copy&amp;quot;;
  output = &amp;quot;.editorconfig&amp;quot;;
  format = &amp;quot;toml&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously, the C# version is huge with lots of settings to fit my standards.&lt;/p&gt;
&lt;h2&gt;Benefits&lt;/h2&gt;
&lt;p&gt;The nice part about this is all I have to do is change into the directory and direnv will automatically make sure all the files are correct and up to date. Since I'm mostly on the command line, this works out beautifully for me and how I work.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ cd bakfu
direnv: loading ~/src/bakfu/.envrc
direnv: using flake
evaluating derivation 'git+file:///home/dmoonfire/src/bakfu#devShells.x86_64-linux.default'
nixago: updating repository files
nixago: '.conform.yaml' link updated
nixago: '.editorconfig' copy is up to date
nixago: 'lefthook.yml' link updated
nixago: '.prettierrc.json' link updated
nixago: 'treefmt.toml' link updated
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_LDFLAGS +NIX_STORE +NM +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Moving Parts&lt;/h2&gt;
&lt;p&gt;There is a problem with this, in that it is a lot of moving parts to basically write out a file that could easily be checked into code once and be done with. I fully admit that we are going through a lot to hoops that could easily be done with simple files with only one exception.&lt;/p&gt;
&lt;p&gt;The biggest exception is Lefthook. Someone needs to run &lt;code&gt;lefthook init&lt;/code&gt; after the repository is cloned to ensure the hooks are all configured so it enforces the commit messages and makes sure the code is formatted before committing. Since Git won't ever provide that, having the shell hook from the flake enforce it cuts out a tedious step that is easily overlooked.&lt;/p&gt;
&lt;p&gt;The other reason for going with this approach is my ability to update it. Each of my stories are in their own repository for a variety of reasons, but the structure and layout of them are typically manipulated en masse as I update a standard. I also do theme and style changes across the boards, such as finding a new font or fixing the ebook generation.&lt;/p&gt;
&lt;p&gt;With the flake setup, I could easily migrate these configurations to a dedicated flake that is shared across all of them, and then just update the lock for that file to enforce the latest iteration of an evolving standard.&lt;/p&gt;
&lt;p&gt;And standards are evolving. While I have common patterns (braces on newlines), how I format the code, organize files, or hook up patterns changes. Sometimes it is a little incremental change, sometimes it is a sweeping change as I switch build systems or introduce a standard format. Last year, I set it up so every project would generate a EPUB and PDF file and work with my Gitea and Woodpecker CI setup.&lt;/p&gt;
&lt;p&gt;If I can make those changes cut across all the projects, then it is less effort for me to get conformity but also let me work in an environment I'm comfortable with. Being able to make sure every tool I want is available (such as &lt;a href="/tags/author-intrusion/"&gt;Author Intrusion&lt;/a&gt; or &lt;a href="/tags/markdown/"&gt;markdowny&lt;/a&gt;) means I don't have to think about the plumbing and just do the part that is fun: write.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Formats and Registries</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/11/30/package-management-formats/" />
    <updated>2023-11-30T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/11/30/package-management-formats/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">Thoughts on setting up formats and layering registries for those formats on top of each other.
</summary>
    <content type="html">&lt;p&gt;Since my mind has been on it, I wanted to work out some of the ideas I had for formats in my packaging system. In this case, I'm going to focus on a single one, NuGet, because I have a fair amount of experience with this and it has some of the complexities that are throwing me.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configuration Files&lt;/h2&gt;
&lt;p&gt;All of the configuration for the system will be in a series of JSON5 (or JSON or whatever formats are officially supported) that are merged together with any conflict producing an error message and stopping the systems.&lt;/p&gt;
&lt;p&gt;Assuming &lt;code&gt;$GITDIR&lt;/code&gt; is the top-level directory for a Git repository, then the configuration would be in &lt;code&gt;$GITDIR/.config/bakfu&lt;/code&gt;. All the files will be gathered together, but a &lt;code&gt;.gitignore&lt;/code&gt; could ignore &lt;code&gt;*.user.*&lt;/code&gt; which means authentication information could be stored locally but have a common configuration on top of that.&lt;/p&gt;
&lt;p&gt;In this case, the files all have the same schema which is a required component because it also identifies the version of the file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;{
    &amp;quot;$schema&amp;quot;: &amp;quot;...&amp;quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any more details on how we look for configuration files will have to wait for another post.&lt;/p&gt;
&lt;h2&gt;What is a Format?&lt;/h2&gt;
&lt;p&gt;Right now, a &amp;ldquo;format&amp;rdquo; is a specific format of a package, such as NuGet package or a NPM one. Inspired by SGML catalogs and how I like to see things in Git repositories, a format looks roughly like this JSON5.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;{
    // In this file, the various components don't have to be URL encoded.
    &amp;quot;formats&amp;quot;: {
        &amp;quot;nuget&amp;quot;: {
            &amp;quot;defaults&amp;quot;: {
                &amp;quot;authority&amp;quot;: &amp;quot;nuget&amp;quot;,
            },
            &amp;quot;authorities&amp;quot;: {
                &amp;quot;nuget&amp;quot;: {},
            },
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we merge files, that means I could create project-specific authority for packages that aren't (and probably never will be) on the official NuGet server. For example, my personal Forgejo instance at &lt;a href="https://src.mfgames.com/mfgames-cil"&gt;https://src.mfgames.com/mfgames-cil&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;{
    // In this file, the various components don't have to be URL encoded.
    &amp;quot;formats&amp;quot;: {
        &amp;quot;nuget&amp;quot;: {
            &amp;quot;authorities&amp;quot;: {
                &amp;quot;src.mfgames.com/mfgames-cil&amp;quot;: {},
            },
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Authorities&lt;/h2&gt;
&lt;p&gt;The authority itself is the complex part of the file because it needs to handle how to search for packages, how to download them, authorization needed, and how verification is done.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;// merged formats.nuget.authorities:
&amp;quot;nuget&amp;quot;: {
    // &amp;quot;enabled&amp;quot;: true, // Implied so it can be disabled
    &amp;quot;registries&amp;quot;: {
        &amp;quot;nuget.org&amp;quot;: {
            &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
            &amp;quot;url&amp;quot;: &amp;quot;https://api.nuget.org/v3/index.json&amp;quot;,
        },
    ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another file could merge additional registries in, much like you can have a proxy feed in DevOps.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;// merged formats.nuget.authorities:
&amp;quot;nuget&amp;quot;: {
    &amp;quot;registries&amp;quot;: {
        &amp;quot;nuget.org&amp;quot;: {
            &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
            &amp;quot;url&amp;quot;: &amp;quot;https://api.nuget.org/v3/index.json&amp;quot;,
        },
        &amp;quot;example&amp;quot;: {
            &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
            &amp;quot;url&amp;quot;: &amp;quot;https://example.org/proxied-feed/v3/index.json&amp;quot;,
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Controlling Order&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;NuGet.config&lt;/code&gt; file, there is also the ability to clear out the list of registries and use only a single set of identified registries. In this case, it would be a combination of disabling the known ones, changing the search for other files (the later post), and using a set of ordering controls.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;// merged formats.nuget.authorities:
&amp;quot;nuget&amp;quot;: {
    &amp;quot;registries&amp;quot;: {
        &amp;quot;nuget.org&amp;quot;: {
            &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
            &amp;quot;url&amp;quot;: &amp;quot;https://api.nuget.org/v3/index.json&amp;quot;,
            &amp;quot;enabled&amp;quot;: &amp;quot;false&amp;quot;,
            // Below implies &amp;quot;search&amp;quot;: { &amp;quot;after&amp;quot;: [&amp;quot;example&amp;quot;] } }
        },
        &amp;quot;example&amp;quot;: {
            &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
            &amp;quot;url&amp;quot;: &amp;quot;https://example.org/proxied-feed/v3/index.json&amp;quot;,
            &amp;quot;search&amp;quot;: {
                &amp;quot;before&amp;quot;: [&amp;quot;nuget.org&amp;quot;],
            }
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Additional Packages&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;NuGet.config&lt;/code&gt;, it is possible to &lt;a href="https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping"&gt;map a set of packages&lt;/a&gt; to a given URL, such as all &lt;code&gt;MfGames*&lt;/code&gt; can only be found at a specific server. In those cases, that should be treated as a separate authority with its own set of registries (that may be a duplicate if it is also a proxy feed).&lt;/p&gt;
&lt;p&gt;In the example below, &lt;code&gt;contoso&lt;/code&gt; would be treated as a separate authority than the &lt;code&gt;nuget&lt;/code&gt; default.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- NuGet.config --&amp;gt;
&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;packageSourceMapping&amp;gt;
    &amp;lt;packageSource key=&amp;quot;nuget.org&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
    &amp;lt;packageSource key=&amp;quot;contoso.com&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;Contoso.*&amp;quot; /&amp;gt;
      &amp;lt;package pattern=&amp;quot;NuGet.Common&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
  &amp;lt;/packageSourceMapping&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Protocol&lt;/h3&gt;
&lt;p&gt;The protocol determines how the registry is accessed. I could see a number of possibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NuGet V3 protocol, obviously NuGet-centric&lt;/li&gt;
&lt;li&gt;NPM access&lt;/li&gt;
&lt;li&gt;Directory location&lt;/li&gt;
&lt;li&gt;gRPC proxy server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since this library needs to be implemented across a number of platforms and libraries, I would expect that unknown protocols would be filtered out (maybe with a warning) and then the ones that can be accessed are used. If there are no valid ones, then the system should blow up.&lt;/p&gt;
&lt;p&gt;The protocol also determines what additional settings might be required such as authentication, file system layout, or the like. It would be obvious specific to that protocol, so it is thrown into a generic &amp;ldquo;settings&amp;rdquo; object to control those things.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;&amp;quot;local&amp;quot;: {
    &amp;quot;protocol&amp;quot;: &amp;quot;file-v1&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;file:///${env:HOME}/src/other/project&amp;quot;,
    &amp;quot;settings&amp;quot;: {
        // The layout for &amp;quot;bob&amp;quot; would be &amp;quot;b/bob&amp;quot;.
        &amp;quot;package&amp;quot;: &amp;quot;${PACKAGE_NAME:0-0}/${PACKAGE_NAME}/${PACKAGE_VERSION}&amp;quot;,
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Search Controls&lt;/h2&gt;
&lt;p&gt;Another aspect if how to handle searching. NuGet will search all the sources at the same time and the first one that responds gets it. However, in some cases, one might want only certain ones to be searched and then stop if it isn't there (such as a full feed proxy verses a subset proxy feed).&lt;/p&gt;
&lt;p&gt;I would see this as controlled by two components, at the authority and for an individual repository.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json5"&gt;// merged formats
&amp;quot;nuget&amp;quot;: {
    &amp;quot;authorities&amp;quot;: {
        &amp;quot;nuget&amp;quot;: {
            &amp;quot;search&amp;quot;: {
                &amp;quot;concurrent&amp;quot;: true,
                &amp;quot;defaultOrder&amp;quot;: &amp;quot;Alphabetical&amp;quot;,
            },
            &amp;quot;registries&amp;quot;: {
                &amp;quot;nuget.org&amp;quot;: {
                    &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
                    &amp;quot;url&amp;quot;: &amp;quot;https://api.nuget.org/v3/index.json&amp;quot;,
                    &amp;quot;search&amp;quot;: {
                        &amp;quot;notFound&amp;quot;: &amp;quot;Stop&amp;quot;,
                        &amp;quot;timeout&amp;quot;: {
                            &amp;quot;time&amp;quot;: &amp;quot;00:01:00&amp;quot;,
                            &amp;quot;action&amp;quot;: &amp;quot;Retry&amp;quot;,
                            &amp;quot;maximumRetries&amp;quot;: 3,
                        },
                    },
                },
                &amp;quot;example&amp;quot;: {
                    &amp;quot;protocol&amp;quot;: &amp;quot;nuget-v3&amp;quot;,
                    &amp;quot;url&amp;quot;: &amp;quot;https://example.org/proxied-feed/v3/index.json&amp;quot;,
                    &amp;quot;search&amp;quot;: {
                        &amp;quot;notFound&amp;quot;: &amp;quot;Continue&amp;quot;,
                    },
                },
            },
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Well, that's my thoughts on authorities and how to search them for packages. One thing you might notice is that I &lt;em&gt;don't&lt;/em&gt; have offline packages in the above examples. I want to treat offline (cached) files as a first-class concepts in the packaging and so that requires its own discussion. Not to mention, the cache is for all formats, not just one.&lt;/p&gt;
&lt;p&gt;I also want to eventually introduce the ability to have services provide opinions on packages. This way, I could set up a service that translates CVE alerts into controls of the packages found or allow a project-specific settings that would hide packages that were incompatible with the current project. I just don't know how to call them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Computer science cannot solve two things: cache invalidation, how to name things, and off-by-one errors.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Identifiers 2</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/09/20/package-management-identifiers-2/" />
    <updated>2023-09-20T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/09/20/package-management-identifiers-2/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">Using URNs to identify packages and some re-thinking of concepts.
</summary>
    <content type="html">&lt;p&gt;It's been a many (seven) months since I started working on the ideas of a package management system. A lot's happened but something got me down the path again and I decided to look at it again. This isn't a straight journey where I write every single post ahead of time but instead its more of ramblings and thoughts.&lt;/p&gt;
&lt;p&gt;Or, in the words of one of my favorite puzzle games, &lt;em&gt;The Thirtieth Guest&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Feeling lonely?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mistakes Were Made&lt;/h2&gt;
&lt;p&gt;While I was reading &lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;the first identifiers post&lt;/a&gt;, I realized I made a few mistakes. One was that I was using a URL instead of a &lt;a href="https://en.wikipedia.org/wiki/Uniform_Resource_Name"&gt;URN&lt;/a&gt; to identify a package:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In contrast, URNs were conceived as persistent, location-independent identifiers assigned within defined namespaces, typically by an authority responsible for the namespace, so that they are globally unique and persistent over long periods of time, even after the resource which they identify ceases to exist or becomes unavailable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When it comes to identifying a package, that is exactly what we want. A persistent identifier that doesn't point to a specific location. When we want Markdowny, an important part is that we don't want to mandate &lt;em&gt;where&lt;/em&gt; to get it, but enough to identify it.&lt;/p&gt;
&lt;h2&gt;Rehashing as URN Components&lt;/h2&gt;
&lt;p&gt;URNs always start with &lt;code&gt;urn:&lt;/code&gt; and a registered &amp;ldquo;code&amp;rdquo;. We're going to pretend &lt;code&gt;bakfu&lt;/code&gt; is the registered code, so that means all the package identifiers would be &lt;code&gt;urn:bakfu:&lt;/code&gt; then something.&lt;/p&gt;
&lt;p&gt;Likewise, I think it is important to know when something is a package identifier (say &lt;code&gt;pkg:&lt;/code&gt;) or a reference to a package which has ranges (&lt;code&gt;ref:&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;EDIT: After looking at my notes, I already figured out why I didn't need this so I removed it from the content below.&lt;/p&gt;
&lt;p&gt;From earlier posts, I decided on the package format being a component with well-known versions (npm, nuget, deno) and arbitrary ones (domain-based).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;urn:bakfu:npm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;urn:bakfu:minetest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;urn:bakfu:authorintrusion.com/spell-check&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the later examples below, I'm going to cut off the &lt;code&gt;urn:bakfu:&lt;/code&gt; as noise so &lt;code&gt;npm&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Authority&lt;/h2&gt;
&lt;p&gt;I don't have a lot of problems or doubt with the components above. The next part, on the other hand is a bit more complicated and fluid. As I develop more, I think we should have separate package repositories/registeries instead of putting everything at npmjs.com or nuget.org. However, that leads into potential name and identifier conflicts.&lt;/p&gt;
&lt;p&gt;Using the example from my life, when I started Nitride, I made all the namespaces &amp;ldquo;Nitride&amp;rdquo; and was going to buy a developer SSL to push it up to nuget.org. However, by the time I got to a stable point, there was then a Nitride already there and that didn't work.&lt;/p&gt;
&lt;p&gt;(This is also one reason why I don't like identifiers that aren't namespaced.)&lt;/p&gt;
&lt;p&gt;At the same time, I want to keep these URNs relatively &amp;ldquo;simple&amp;rdquo; for the 99% cases. In those cases, that means I want to aim for something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm:markdowny&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm:@mfgames-writing/format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nuget:Humanizer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But, if there is a non-default location, the URN needs to have some mechanism that identifies the &amp;ldquo;authority&amp;rdquo; of a package. This authority doesn't need to be a URL, just a unique key to distinguish between two packages with the same identifier.&lt;/p&gt;
&lt;p&gt;Originally I thought about something like &lt;code&gt;npm:///markdowny&lt;/code&gt; based on using &lt;code&gt;file:///&lt;/code&gt; to reference the local file system but allow a domain and directory to be used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm://mfgames.com/markdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm://mfgames.com/@mfgames-writing/epub2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm://example.org/~user/@example-organization/example-package&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem with that is the last one. Where does the directory structure end, where does the package begin? If the entire URL is opaque, then it would be easy to leave as-is, but because this has to translate, there needs to be an unequivocal way of splitting them into an authority and a package identifier.&lt;/p&gt;
&lt;p&gt;URL encoding to the rescue.&lt;/p&gt;
&lt;p&gt;If we treat the optional directory structure (on the optional authority domain) as a single &amp;ldquo;unit&amp;rdquo;, then we can keep the slash to separate authority from the identifier but still keep the identifier in its most common format (NPM uses slashes):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm:markdowny&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm:///markdowny&lt;/code&gt; (same as above)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm://npmjs.com/@mfgames-writing/epub2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm://npmjs.com%2F~dmoonfire/@mfgames-writing/epub2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think this would work because we can have a rule that states that the component after the package type has either two slashes for an authority and the next slash ends that component. Everything after that is the full identifier.&lt;/p&gt;
&lt;h3&gt;Well-Known URLs&lt;/h3&gt;
&lt;p&gt;I'm fond of the &lt;a href="https://en.wikipedia.org/wiki/Well-known_URI"&gt;.well-known/&lt;/a&gt; infrastructure that has built up over the years. I could easily envision that this could translate into an actual URL to help identify the location of the packages if not known.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://npmjs.com/.well-known/bakfu/npm/npmjs.com%2F%7Edmoonfire/@mfgames-writing/epub2"&gt;https://npmjs.com/.well-known/bakfu/npm/npmjs.com%2F~dmoonfire/@mfgames-writing/epub2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The resulting JSON file would give common locations where to find it. So going to the @mfgames-writing/epub well-known URL would then give the URLs for the official servers or locations, such as npmjs.com, my local package repository, an IPFS address, or whatever makes sense.&lt;/p&gt;
&lt;p&gt;The reason it won't use query strings like &lt;a href="https://webfinger.net/"&gt;webfinger&lt;/a&gt; is because query strings don't play well with static sites and I use static sites pretty heavily.&lt;/p&gt;
&lt;h2&gt;Qualified Identifiers&lt;/h2&gt;
&lt;p&gt;I think the ideas from the original identifiers post for qualified identifiers still have merit, but without the &lt;code&gt;bakfu:&lt;/code&gt; prefix because it ends up just being noise. I think these should be limited and defined ahead of time since there is flexibility on the features.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;java:org.example.hyphenated_name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm:markdowny?version=1.1.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo:serde?version=1.0.152&amp;amp;feature[]=derive&amp;amp;feature[]=rc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo:serde?version=1.0.152&amp;amp;platform=x86_64-unknown-linux-gnu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Additional Versions&lt;/h3&gt;
&lt;p&gt;If the package version (as opposed to the content version) is needed, then &lt;code&gt;&amp;amp;package=1.0.0&lt;/code&gt; can be used. Likewise, if the Bakfu itself needs to be bumped, then &lt;code&gt;&amp;amp;bakfu=1.2.0&lt;/code&gt; can be used.&lt;/p&gt;
&lt;p&gt;I thought about making versions arbitrary, but I couldn't imagine a case where a package would have two different versions for two purposes. Those would be two separate packages in that case.&lt;/p&gt;
&lt;h3&gt;Overriding Packages&lt;/h3&gt;
&lt;p&gt;One of the reasons of this exercise is how to do a modification to a library that is already releases but the users learn after the fact that it breaks semantic versioning (SlimMessageBus). One constraint to this is that according to the &lt;a href="https://semver.org/#what-do-i-do-if-i-accidentally-release-a-backward-incompatible-change-as-a-minor-version"&gt;specification&lt;/a&gt;, the version of the content cannot change once published.&lt;/p&gt;
&lt;p&gt;That is why the package has its own version, to indicate that the package metadata such as the dependencies and requirements, can change independently of the contents. In most cases, &lt;code&gt;package=1.0.0&lt;/code&gt; but a proxy service could add in the modified dependency and call it &lt;code&gt;package=1.0.1-service&lt;/code&gt; which would then cause the packaging system to prefer the highest version of the package with the same version.&lt;/p&gt;
&lt;p&gt;There is some gaps because if we had a Bakfu-aware packaging system, someone could create a package and then keep bumping the package version higher to override anyone's overrides but I think this is a case where an upstream package modification should be blocked if there is an override given. At least until that new version can be reviewed and accepted.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The main reason to have these package identifiers is just to distinguish a package uniquely across the entire ecosystem. I strongly believe there needs to be a decoupling of the location verses identifier because of the other goals in this project: moving from one host to another, caching packages, being able to provide a curated list, blocking malicious packages, and to add after-the-fact changes.&lt;/p&gt;
&lt;p&gt;Overall, I think this fits my need for something that is roughly ascetic (&lt;code&gt;urn:bakfu:npm:markdowny?version=1.0.1&lt;/code&gt;), has a most-common use of something simple and readable (&lt;code&gt;urn:bakfu:npm:markdowny&lt;/code&gt;), but still allows distributed packages and cases where there are name collisions (&lt;code&gt;urn:bakfu:nuget://mfgames.com/Nitride&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;It also can be reduced to a common form based on context such as removing the &lt;code&gt;urn:bakfu:&lt;/code&gt; which makes the simplest version &lt;code&gt;npm:markdowny&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Also, it shouldn't be hard to create a normalized rule to turn it into a proper C# or Rust structure for doing the next steps.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Fedran Infrastructure Redux</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/06/27/fedran-infrastructure-2/" />
    <updated>2023-06-27T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/06/27/fedran-infrastructure-2/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="nix-standard" scheme="https://d.moonfire.us/tags/" label="Nix Standard" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <summary type="html">Just a follow up on a few things I had to do to get my Fedran infrastructure behaving better and not chewing up time and bandwidth once a day.
</summary>
    <content type="html">&lt;p&gt;I know I said that I was going to work on writing after working on &lt;a href="/blog/2023/06/24/fedran-infrastructure/"&gt;the previous post&lt;/a&gt;, but I tried to speed up the pipelines because an hour was overwhelming and bothered me.&lt;/p&gt;
&lt;p&gt;Thanks to a bit of obsession, I figured it out.&lt;/p&gt;
&lt;p&gt;The bulk of the pipeline was spent download and building Nix packages. This normally isn't a problem, but I want to be able to run an agent in my home lab where I'm charged for bandwidth overages. Pulling down half a gig of packages every time I built my website was also not being a good steward of the Internet as a whole.&lt;/p&gt;
&lt;p&gt;I had bookmarked a link to kotatsuyaki's &lt;a href="https://blog.kotatsu.dev/posts/2023-04-21-woodpecker-nix-caching/"&gt;Locally Cached Nix CI with Woodpecker&lt;/a&gt; which was based on &lt;a href="https://kevincox.ca/2022/01/02/nix-in-docker-caching/"&gt;Kevin Cox's work&lt;/a&gt; and tried it a couple of times. I usually got hung up on switching from Docker to Podman but eventually got it mostly working, hit some snags, and found a slightly-less-than-optimal approach instead.&lt;/p&gt;
&lt;h2&gt;Lazy Configuration&lt;/h2&gt;
&lt;p&gt;It took me a day to figure out how to get Podman working with my setup. Naturally, OCI images in Nix change name so it was &lt;code&gt;docker-woodpecker-server&lt;/code&gt; in Docker and &lt;code&gt;podman-woodpecker-server&lt;/code&gt; on Podman. This meant I had to also switch the various systemd links that tie in changing secrets with restarting the OCI images.&lt;/p&gt;
&lt;p&gt;As usual, once I realized I was going back and forth (usually on the third time), I decided to automate that.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# woodpecker-agent.nix
{
  config,
  pkgs,
  lib,
  ...
}: let
  tag = &amp;quot;next-eaae6b44c7&amp;quot;;
  container =
    if config.virtualisation.podman.enable
    then &amp;quot;podman&amp;quot;
    else &amp;quot;docker&amp;quot;;
in {
  sops.secrets = {
    woodpecker-ci-agent = {
      restartUnits = [&amp;quot;${container}-woodpecker-ci-agent.service&amp;quot;];
    };
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm not fond of Nix as a language, but it was nice being able to have it pick up which configuration was set up properly and then rename the various systemd units as appropriate.&lt;/p&gt;
&lt;h2&gt;Mounting Nix Store&lt;/h2&gt;
&lt;p&gt;The crux of the problem was storing Nix packages so I didn't have to download them every time I built the package. This is really important when I start doing mass changes to my writing projects since one of those causes ~137 pipelines to be triggered. Since each one uses an identical &lt;code&gt;flake.lock&lt;/code&gt; (because of the Rust CLI), caching those packages also means that it can generate each of the PDFs and EPUBs automatically when I update packages, change style, or need to rebuild things.&lt;/p&gt;
&lt;p&gt;Originally I tried the example in the above link and it worked beautifully. I did have to make the pipeline trusted, but I run my own CI server and I don't build pull requests, so I have that locked down to avoid too much security exposure.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .woodpecker.yaml
pipeline:
  run-greet:
    image: nixos/nix
    commands:
      - echo 'experimental-features = flakes nix-command' &amp;gt;&amp;gt; /etc/nix/nix.conf
      - nix run --store unix:///mnt/nix/var/nix/daemon-socket/socket?root=/mnt .#greet -L
    volumes:
      - /nix:/mnt/nix:ro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do think I may have had better luck if I included Kevin's command in the above link.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I have seen some issues when using &amp;ndash;store. I found that this can be fixed by additionally passing &amp;ndash;eval-store local.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Though by the time I realized I should have tried that, I found a working solution and needed to stop messing with an incremental change without significant improvement over what I had.&lt;/p&gt;
&lt;p&gt;When I migrated my Fedran pipeline over to the above &lt;code&gt;--store&lt;/code&gt; implementation, it blew up with being unable to link DLLs/shared libraries. First it was &lt;code&gt;sodium23&lt;/code&gt;, then &lt;code&gt;lowdown&lt;/code&gt;, then something else. Each time, I added more packages to the build until I hit &lt;code&gt;libnixeval.so&lt;/code&gt;. That one&amp;hellip; I couldn't fix.&lt;/p&gt;
&lt;p&gt;The main problem was the &lt;code&gt;run-greet&lt;/code&gt; pipeline didn't need to call the &lt;code&gt;nix&lt;/code&gt; executable, but I was using &lt;a href="/tags/nix-standard/"&gt;Standard&lt;/a&gt; which does. That means, I couldn't use &lt;code&gt;std //cli/apps/default:run&lt;/code&gt; to run anything which basically meant I had to use &lt;code&gt;cargo build&lt;/code&gt; which negated the entire purpose of using Nix packages to cache. I also couldn't use &lt;code&gt;nix run .&lt;/code&gt; to take advantage of the Nix caching.&lt;/p&gt;
&lt;p&gt;In the end, I switched back to Docker but with Kevin's first suggestion: just create a volume in Docker to share the Nix store. This worked out well&amp;hellip; but I had to make a tweak from Kevin's original suggestion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .gitlab.yaml (I don't remember what it is called anymore)
[[runners]]
    executor = &amp;quot;docker&amp;quot;
    [runners.docker]
        volumes = [&amp;quot;/nix&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found it worked better if I used a named volume:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .woodpecker.yaml
pipeline:
    deploy:
        image: nixpkgs/nix-flakes
        commands:
            - nix develop --command ./src/website/scripts/ci.sh
        when:
            event: [push, manual, cron]
            branch: main
        volumes:
            - woodpecker-nix-store:/nix # named instead of just `- /nix`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that worked beautifully. The build time when down from 48 minutes for the last run to 6 minutes at the most current one. Plus it barely downloaded anything thanks to using Crane for Rust in Nix, and the general package management. If I created a second store to keep the Git repositories and only did a &lt;code&gt;git pull&lt;/code&gt; instead, I could shave it down even more.&lt;/p&gt;
&lt;h2&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;It did occur to me that if I left what I figured out as-is, sooner or later I would run out of space. (I also was reminded by I always create a dedicated &lt;code&gt;/var/lib/docker&lt;/code&gt; partition in this). So on my nightly job, I added this stanza to my &lt;code&gt;.woodpecker.yaml&lt;/code&gt; file (which I haven't tested very well):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .woodpecker.yaml
pipeline:
    clean:
        image: nixpkgs/nix-flakes
        commands:
            - nix-collect-garbage --delete-older-than 15d
        when:
            event: [cron]
            branch: main
        volumes:
            - woodpecker-nix-store:/nix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I did it right, then it should keep everything cleaned up and tidy. I'll find out in a month or so if that is true.&lt;/p&gt;
&lt;h2&gt;Checkout&lt;/h2&gt;
&lt;p&gt;One thing to be said about having so many project pipelines is that I hammer my system when I do a mass change (updating the locks for example). This meant I encountered a strange bug where Woodpecker's checkout plugin starts to fail somewhere around the tenth rapid-fire pipeline.&lt;/p&gt;
&lt;p&gt;To get around it, I switched from using the built-in clone to do it manually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# .woodpecker.yaml
skip_clone: true

pipeline:
    clone:
        image: alpine/git
        commands:
            - git clone https://fedran:$GITEA_TOKEN@src.mfgames.com/$DRONE_REPO.git . --depth 1
            - git checkout $DRONE_COMMIT
        secrets:
            - gitea_token
        when:
            event: [push, manual, cron]
            branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I put it in, I haven't seen check out failures due to being unable to get the user name and password from the prompt. I could have also used &lt;code&gt;nixpkgs/nix-flakes&lt;/code&gt; instead of &lt;code&gt;alpine/git&lt;/code&gt;, but didn't. This works and it's fast.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;I really need to write. I got a closure point. Everything works, I wrote issues up for the ones that aren't breaking, so I can stop obsessing and instead focus on the 10 kwd obligation I have by the end of the month. Which is to say&amp;hellip; probably not going to happen but I'm going to still try.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Fedran Infrastructure</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/06/24/fedran-infrastructure/" />
    <updated>2023-06-24T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/06/24/fedran-infrastructure/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="flight-of-the-scions" scheme="https://d.moonfire.us/tags/" label="Flight of the Scions" />
    <category term="sand-and-blood" scheme="https://d.moonfire.us/tags/" label="Sand and Blood" />
    <category term="sand-and-ash" scheme="https://d.moonfire.us/tags/" label="Sand and Ash" />
    <category term="second-hand-dresses" scheme="https://d.moonfire.us/tags/" label="Second-Hand Dresses" />
    <category term="nor-curse-be-found" scheme="https://d.moonfire.us/tags/" label="Nor Curse Be Found" />
    <category term="allegro" scheme="https://d.moonfire.us/tags/" label="Allegro" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="nix-standard" scheme="https://d.moonfire.us/tags/" label="Nix Standard" />
    <category term="nitride" scheme="https://d.moonfire.us/tags/" label="Nitride" />
    <category term="rust" scheme="https://d.moonfire.us/tags/" label="Rust" />
    <category term="self-published-fantasy-blog-off" scheme="https://d.moonfire.us/tags/" label="Self-Published Fantasy Blog-Off" />
    <category term="reddit" scheme="https://d.moonfire.us/tags/" label="Reddit" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <summary type="html">In the last month, I've been doing a lot of writing and programming. I also finished a good-sized around of "fun coding" with Fedran.
</summary>
    <content type="html">&lt;p&gt;In the first weeks of March, June, September, and December, life gets complicated for my day job. Usually that means the pressure gathers in intensity in the preceding weeks and then winds down after the quarterly release.&lt;/p&gt;
&lt;p&gt;It is time when my other obligations start to press into my attention and I'm too busy to focus on them. Naturally that means as the release winds down, I try to get focus on the others.&lt;/p&gt;
&lt;p&gt;This month, I managed to get twenty thousand words on a commissioned novel. I also stumbled into a few things on the Internet that set off my fixation and I ended up working on them for a few weeks as a &amp;ldquo;play coding&amp;rdquo; to relieve the stress. This month, it was almost entirely related to &lt;a href="//fedran.com/"&gt;Fedran&lt;/a&gt;, my fantasy setting that I also use to create what I'm hoping is a full-featured website suitable for spoilers, &amp;ldquo;who is this character again,&amp;rdquo; and just background stories.&lt;/p&gt;
&lt;h2&gt;Fedran Monorepo&lt;/h2&gt;
&lt;p&gt;Some time ago, I decided to bulk most of the &amp;ldquo;backend&amp;rdquo; of Fedran to a single Git repository. I'm still keeping my sources (all the stories, novels, and poetry) in individual repositories though I did go through the exercise to see if I wanted it (I do, there are too many benefits). But the infrastructure is a different story.&lt;/p&gt;
&lt;p&gt;There isn't much there right now, just a standard directory structure to locate the files, the &lt;a href="/tags/nitride/"&gt;Nitride&lt;/a&gt; website, and a Rust CLI which sets up the individual stories in a standard configuration, lets me do queries across the projects, and starts to help me coordinate everything together.&lt;/p&gt;
&lt;p&gt;Both have built up over the years as I've tried to automate one problem after the other. Moving them into a single repository meant I needed to back out a lot of the cruft that was to coordinate the multiple repositories. I didn't have to manually build the Rust CLI anymore, I didn't need to integrate download the projects into it, I didn't need all the configurations and tokens because there was now one official layout and structure to work against.&lt;/p&gt;
&lt;p&gt;Last last month, I got the website migrated over. This week, I got the CLI fully integrated, actually building, and doing things. This means when I edit a chapter to add content warnings or add details, the website deployment will pick up the changes without me having to remember (e.g., not getting it done more twice or thrice a year).&lt;/p&gt;
&lt;p&gt;It does add a half hour to the build process because I'm apparently incapable of picking a single language to work on and I haven't figured out caching on my &lt;a href="/tags/woodpecker-ci/"&gt;Woodpecker CI&lt;/a&gt; server, so it downloads the entire repository, a half GB of Rust packages, another quarter of NuGet packages, and about the same in Node packages to build it.&lt;/p&gt;
&lt;p&gt;That I can work on.&lt;/p&gt;
&lt;p&gt;This took a while, mainly because I was struggling to get it integrated into a standalone and remove the bulk of the overhead I needed when it was separate.&lt;/p&gt;
&lt;h2&gt;NixOS and Standard&lt;/h2&gt;
&lt;p&gt;Last week, while wandering around the internet, I stumbled on &lt;a href="https://std.divnix.com/"&gt;Standard&lt;/a&gt;, a setup for NixOS flakes (like I use for Fedran's monorepo) that is opinionated but also seems to fit a lot with my thing.&lt;/p&gt;
&lt;p&gt;That was a fun few days (four) to migrate the Fedran site over, and it fixed some problems I was having in general (read, I actually have the CLI integrated into the build now).&lt;/p&gt;
&lt;p&gt;The success means I want to change over my other infrastructure repository which drives most of my home lab but&amp;hellip; I need to say no to that until after August.&lt;/p&gt;
&lt;p&gt;But, one thing that really stuck with me is that it hits the same buttons that I found when I was reading about Buck2 and Bazel, two other monorepo systems that fit well with a lot of professional and personal development going on in my life. Plus, I just like typing this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std //cli/apps/default:run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std //website/app/default:build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And have it do all the right things including getting all the dependencies. I can easily see that tying in my aggregate books, timelines, and other stuff nicely into a single unified system.&lt;/p&gt;
&lt;h2&gt;Future Plans&lt;/h2&gt;
&lt;p&gt;I like building Fedran's infrastructure as much as writing. More so lately because I burned out a little bit and need to work on finishing &lt;a href="/tags/nor-curse-be-found/"&gt;Nor Curse Be Found&lt;/a&gt; and &lt;a href="/tags/second-hand-dresses/"&gt;Second-Hand Dresses&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Speaking of &lt;em&gt;Dresses&lt;/em&gt;, I'm down to a single editing question but that ties into a &lt;em&gt;different&lt;/em&gt; program I need to work on, rebuilding my calendar system so it can play with C# and Rust. I've been using my Typescript based on for about a decade now and I have a huge list of things that need to work.&lt;/p&gt;
&lt;p&gt;Who knew an editor question of &amp;ldquo;what day is it&amp;rdquo; would trigger that paralysis, but I want to solve it because one of the things I lost with my migration to Nitride was my (awesome) timeline functionality. I was proud of that and want it back.&lt;/p&gt;
&lt;p&gt;The calendar is also critical for my constraint system. Because I'm coordinating across multiple process, and I had a terrible time with &lt;a href="/tags/raging-alone/"&gt;Raging Alone&lt;/a&gt;'s documentation process, I realized I need to have something that lets me say things like &amp;ldquo;this character is eight years older than this one&amp;rdquo; or &amp;ldquo;this happens two months after the events of this story&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;(I also need a map, but I'm trying really hard not to think about that.)&lt;/p&gt;
&lt;h2&gt;Nor Curse Be Found&lt;/h2&gt;
&lt;p&gt;Speaking of &lt;em&gt;Nor Curse Be Found&lt;/em&gt;. I hit a little bit of a writer's block on the current chapter and I'm not happy with it. But work and my writing obligations are making it difficult to focus on it, which is why I haven't done much with it lately. I can only have so many writing projects going on and writing that pays bills has a higher priority over writing that makes me feel good.&lt;/p&gt;
&lt;h2&gt;Reddit&lt;/h2&gt;
&lt;p&gt;While Reddit was imploding, I stumbled onto a post in /r/fantasy asking for resident authors. Since I've been commenting off and one for a few years (and being good about not saying &amp;quot;read my book!&amp;quot;), I decided to &lt;a href="https://www.reddit.com/r/Fantasy/comments/14bwwws/comment/johz1em/"&gt;throw my hat into the ring&lt;/a&gt;. Not sure what would come of it, but it wouldn't hurt since right now &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt; currently has one review on Amazon and zero reviews on Goodreads (and just about nowhere else).&lt;/p&gt;
&lt;h2&gt;Self-Published Fantasy Blog Off&lt;/h2&gt;
&lt;p&gt;Over the years, I've withdrawn from both Facebook and Twitter. One of the conquences of this action is that I don't know about a lot of things going on, but one thing I've been occasionally checking has been the &lt;a href="https://en.wikipedia.org/wiki/Self-Published_Fantasy_Blog-Off"&gt;Self-Published Fantasy Blog-Off&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="/tags/sand-and-blood/"&gt;Sand and Blood&lt;/a&gt; was a semi-finalist in the first one and I felt I was &lt;em&gt;so close&lt;/em&gt; to getting to at least finalist. I lost the finalist for &lt;a href="https://www.grimdarkmagazine.com/review-the-thief-who-pulled-on-troubles-braids-by-michael-mcclung/"&gt;The Thief Who Pulled on Trouble's Braids&lt;/a&gt; who would eventually be the winner.&lt;/p&gt;
&lt;p&gt;But I was hoping that maybe &lt;em&gt;Flight of the Scions&lt;/em&gt; could finally have a chance. So I was waiting for the announcement from &lt;a href="http://www.marklawrence.buzz/"&gt;Mark Lawrence&lt;/a&gt; but it never came. Last night, when I decided to check one more time, I saw the reason.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SPFBO 9 is closed. We reached the 300 entries in 41 minutes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Without watching for when the next round comes around, I might have to just hang up my hat on trying to make another SPFBO and just chalk it up to an &amp;ldquo;almost win&amp;rdquo; with &lt;em&gt;Blood&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;Wel, I need to put down the programming hat for a while because we're coming up to the end of the month and I need to write another ten thousand words on my end of month obligation. The beginning of the month will be my commissioned novel (hoping to get 10-20k words by the 15th).&lt;/p&gt;
&lt;p&gt;That means my next &amp;ldquo;fun break&amp;rdquo; is in about three weeks. I need to work on the &lt;a href="/tags/typewriter-press/"&gt;Typewriter Press&lt;/a&gt; website because I published another book for an author last week and I need to do my due diligence to help them sell as many copies as possible.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>NixOS, Gitea/Forgejo, and Catppuccin</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/05/13/nixos-gitea-forgejo-and-catppuccin/" />
    <updated>2023-05-13T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/05/13/nixos-gitea-forgejo-and-catppuccin/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="gitea" scheme="https://d.moonfire.us/tags/" label="Gitea" />
    <category term="forgejo" scheme="https://d.moonfire.us/tags/" label="Forgejo" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="catppuccin" scheme="https://d.moonfire.us/tags/" label="Catppuccin" />
    <summary type="html">How to pull the official Catppuccin theme into Gitea using NixOS.
</summary>
    <content type="html">&lt;p&gt;I spent a couple hours today figuring out how to get the official &lt;a href="/tags/catppuccin/"&gt;Catppuccin&lt;/a&gt; theme for &lt;a href="/tags/gitea/"&gt;Gitea&lt;/a&gt; and &lt;a href="/tags/forgejo/"&gt;Forgejo&lt;/a&gt; integrated into my &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; environment. Originally, I had taken the user theme for Codeberg, tweaked and cleaned it up for my server, and then used that. But, I decided to go a little further and submitted a request to make it official. Someone then made an entirely new package and made it the &lt;a href="https://github.com/catppuccin/gitea"&gt;official version&lt;/a&gt;. After a little pouting, I decided that I'd rather have someone else maintain it so I can focus on other things.&lt;/p&gt;
&lt;p&gt;The first part was pulling it down. Originally, I thought I would grab it via the Nix flake, but that ended up being too difficult. After fumbling around, I came up with using &lt;code&gt;fetchzip&lt;/code&gt; (which apparently does tarballs also) because the release files don't have a top-level directory (all the CSS are on the root).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;inputs @ {
  config,
  pkgs,
  lib,
  ...
}: let
  theme = pkgs.fetchzip {
    url = &amp;quot;https://github.com/catppuccin/gitea/releases/download/v0.2.1/catppuccin-gitea.tar.gz&amp;quot;;
    sha256 = &amp;quot;sha256-HqVLW58lKPn81p3gTSjzkACHSBbmqPqeobAlJMubb8Y=&amp;quot;;
    stripRoot = false;
  };
in {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there, I needed to set it up. There aren't a lot of good ways (that I've found) with setting up data structures for services on NixOS, so I set everything up in the preStart of the service. The stanza below gets all the files into the right place.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;systemd.services.gitea = {
  preStart = lib.mkAfter ''
    rm -rf ${config.services.gitea.stateDir}/custom/public
    mkdir -p ${config.services.gitea.stateDir}/custom/public
    ln -sf ${theme} ${config.services.gitea.stateDir}/custom/public/css
  '';
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last bit is to tell Gitea about the themes. I considered writing them all down, but there are quite a few of them (four basic types plus accents for each one), so I decided to dynamically build the list from the input.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;services.gitea = {
  settings = {
    ui = {
      THEMES =
        builtins.concatStringsSep
        &amp;quot;,&amp;quot;
        ([&amp;quot;auto&amp;quot;]
          ++ (map (name: lib.removePrefix &amp;quot;theme-&amp;quot; (lib.removeSuffix &amp;quot;.css&amp;quot; name))
            (builtins.attrNames (builtins.readDir theme))));
      DEFAULT_THEME = &amp;quot;catppuccin-mocha-blue&amp;quot;;
    };
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to ottidmes from the &lt;code&gt;#nix:nixos.org&lt;/code&gt; matrix room for all the help.&lt;/p&gt;
&lt;p&gt;In other news, I switched from Gitea to Forgejo on &lt;a href="https://src.mfgames.com/"&gt;src.mfgames.com&lt;/a&gt;. There wasn't a compelling reason other than I feel that I fit the philosophy more. I also disabled registrations on that server for now. If you want to send me an update, just use my &lt;a href="/contact/"&gt;contact page&lt;/a&gt; and send directly.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Rumors of my dead garden links were exaggerated</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/03/15/dead-links/" />
    <updated>2023-03-15T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/03/15/dead-links/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="exit-planning" scheme="https://d.moonfire.us/tags/" label="Exit Planning" />
    <summary type="html">Fixed an embarrassing removal of a lot of links in my Exit Planning and other garden plots due to an unrelated feature.
</summary>
    <content type="html">&lt;p&gt;Whenever tax season comes around, I start to talk about &lt;a href="/garden/exit-planning/"&gt;Exit Planning&lt;/a&gt; because it is &amp;ldquo;death and taxes.&amp;rdquo; I'm pretty proud of it as a project and as a living documentation of something I think a lot of us are missing, how to prepare for our deaths (or incapacitation).&lt;/p&gt;
&lt;p&gt;What I didn't realize was that when I put in the dead link removal code for the &lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;package management&lt;/a&gt; to let me write posts ahead of time, I also broke all the links in the gardens.&lt;/p&gt;
&lt;p&gt;That cornucopia of links that seems to represent my thought posts had disappeared so everyone just so a bland introduction page without knowing there were two dozen links interspersed inside it.&lt;/p&gt;
&lt;p&gt;Thankfully, &lt;a href="https://fosstodon.org/@benjaminhollon"&gt;Benjamin Hollon&lt;/a&gt; on Mastodon &lt;a href="https://fosstodon.org/@benjaminhollon/109951024605192949"&gt;noted&lt;/a&gt; that all my links were dead, so I finished tracking it down and fixed it. Thank you!&lt;/p&gt;
&lt;p&gt;Now, to move that HTML dead link code into &lt;a href="/tags/mfgames-nitride/"&gt;Nitride&lt;/a&gt; proper so I can use it with all my sites.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Dependencies</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/02/13/package-management-dependencies/" />
    <updated>2023-02-13T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/02/13/package-management-dependencies/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.
</summary>
    <content type="html">&lt;p&gt;While packages can be self-contained, usually they have dependencies with other packages or even frameworks to avoid duplicating code. Without dependencies, packages would be forced to contain common logic that other packages share, and that code may become stale or not updated as frequently introducing potential bugs.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Simple Dependencies&lt;/h2&gt;
&lt;p&gt;For most systems, dependencies are very simple: this package requires these packages in this range. When working with Bakfu, this is almost identical to the package identifier but without the &lt;code&gt;bakfu:version&lt;/code&gt; and instead a range of values that are acceptable.&lt;/p&gt;
&lt;p&gt;Using &lt;a href="https://src.mfgames.com/mfgames-cil/-/packages/nuget/mfgames.nitride/0.15.1"&gt;MfGames.Nitride?version=0.15.1&lt;/a&gt; as an example, here is a fragment of what that could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    // Basically this is &amp;quot;bakfu:nuget:MfGames.Nitride?framework=net6.0&amp;quot;
    type: &amp;quot;nuget&amp;quot;,
    id: &amp;quot;MfGames.Nitride&amp;quot;,
    attributes: {
        &amp;quot;framework&amp;quot;: &amp;quot;net6.0&amp;quot;,
    },
    content: {
        version: &amp;quot;0.15.1&amp;quot;,
        dependencies: [
            &amp;quot;bakfu:nuget:Autofac?framework=net6.0&amp;quot;: &amp;quot;[6.4.0, 7.0.0)&amp;quot;,
            &amp;quot;bakfu:nuget:FluentValidation?framework=netstandard2.1&amp;quot;: &amp;quot;[11.2.1, 12.0.0)&amp;quot;,
            &amp;quot;bakfu:nuget:MfGames.Gallium?framework=net6.0&amp;quot;: &amp;quot;[0.4.0, 0.5.0)&amp;quot;,
        ],
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Replacements and Substitutions&lt;/h2&gt;
&lt;p&gt;While the above works in most cases, there are situations when it doesn't. Probably the easiest one to explain in in Minetest with multiple mods that replace the built-in &lt;code&gt;beds&lt;/code&gt; module: &lt;a href="https://content.minetest.net/packages/TenPlus1/beds/"&gt;TenPlus1's&lt;/a&gt; and &lt;a href="https://content.minetest.net/packages/sorcerykid/beds/"&gt;SorceryKid's&lt;/a&gt;. In all three of the cases, they are installed as &lt;code&gt;beds&lt;/code&gt; which is used for the dependency, but they are distinguished enough for our package system despite the same name.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bakfu:minetest:beds&lt;/li&gt;
&lt;li&gt;bakfu:minetest:TenPlus1/beds&lt;/li&gt;
&lt;li&gt;bakfu:minetest:sorcerykid/beds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the C# world, I've encountered this with the BouncyCastle libraries. In specific, we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/BouncyCastle"&gt;BouncyCastle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/Portable.BouncyCastle"&gt;Portable.BouncyCastle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of these libraries have a 1.8.9 version and both of them have exactly the same DLL once it is build, &lt;code&gt;BouncyCastle.dll&lt;/code&gt;. In the past, this has caused us a lot of problem because while the two versions are API compatible (they have the same application binary interface, or ABI), they also overlap each other and have different packaging systems (one is only for .NET Framework, portable is more universally usable and currently maintained).&lt;/p&gt;
&lt;p&gt;Likewise, if someone abandoned a package in a fit of anger, say the &lt;a href="https://www.theverge.com/2016/3/24/11300840/how-an-irate-developer-briefly-broke-javascript"&gt;leftpad drama in the JavaScript&lt;/a&gt;, a strict package identifier means a replacement cannot be find. The same thing happens if a project maintainer dies &lt;a href="https://d.moonfire.us/garden/exit-planning/"&gt;without planning for a successor&lt;/a&gt;, there is a schism in the community causing a soft- or even hard-fork, or even someone using a Git repository or local copy to add some customization they need. Regardless, there are reasonable reasons for having two packages that proclaim to be the same version.&lt;/p&gt;
&lt;p&gt;To handle this, packages should list the aliases of what they provide (including themselves if needed) and that is what dependent packages would use. Using the BouncyCastle for an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;// BouncyCastle
{
    content: {
        id: &amp;quot;BouncyCastle&amp;quot;,
        version: &amp;quot;1.8.9&amp;quot;,
        attributes: {
            &amp;quot;framework&amp;quot;: &amp;quot;net&amp;quot;,
        },
        provides: [ &amp;quot;bakfu:nuget:BouncyCastle?bakfu:version=1.8.9&amp;amp;framework=net&amp;quot; ],
    },
}

// Portable.BouncyCastle
{
    content: {
        id: &amp;quot;Portable.BouncyCastle&amp;quot;,
        version: &amp;quot;1.8.9&amp;quot;,
        attributes: {
            &amp;quot;framework&amp;quot;: &amp;quot;net40&amp;quot;,
        },
        provides: [
            &amp;quot;bakfu:nuget:Portable.BouncyCastle?bakfu:version=1.8.9&amp;amp;framework=net40&amp;quot;,
            &amp;quot;bakfu:nuget:BouncyCastle?bakfu:version=1.8.9&amp;amp;framework=net&amp;quot;,
        ],
    },
}

// A third package
{
    content: {
        id: &amp;quot;Something.Else&amp;quot;,
        version: &amp;quot;1.0.0&amp;quot;,
        requires: {
            &amp;quot;bakfu:nuget:BouncyCastle?framework=net&amp;quot;: &amp;quot;[1.8.9,2.0.0)&amp;quot;,
        },
        // A missing provides would just produce the identity package
        // in this case, it would imply:
        // provides: { &amp;quot;bakfu.nuget:Something.Else?bakfu:version=1.0.0&amp;quot; }
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above example, if a module doesn't have a &lt;code&gt;provides&lt;/code&gt; element, then we could assume that it is just the identity version. &amp;ldquo;Something.Else&amp;rdquo; version 1.0.0 becomes &lt;code&gt;bakfu.nuget:Something.Else?version=1.0.0&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Publish and Subscribe&lt;/h2&gt;
&lt;p&gt;The above example doesn't handle optional dependencies. For example, Minetest modules can check for the presence of other modules and then hook up to them. Atom (one of my favorite editors that was sadly killed) had the idea of services where a package could provide a service (that had a version) and other packages could &amp;ldquo;consume&amp;rdquo; them. Using a fragment of my own &lt;code&gt;spell-check&lt;/code&gt;, it looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;// from spell-check
&amp;quot;consumedServices&amp;quot;: {
  &amp;quot;spell-check&amp;quot;: {
    &amp;quot;versions&amp;quot;: {
      &amp;quot;^1.0.0&amp;quot;: &amp;quot;consumeSpellCheckers&amp;quot;
    }
  }
},

// from spell-check-project
&amp;quot;providedServices&amp;quot;: {
  &amp;quot;spell-check&amp;quot;: {
    &amp;quot;versions&amp;quot;: {
      &amp;quot;1.0.0&amp;quot;: &amp;quot;provideSpellCheck&amp;quot;
    }
  }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above case, the editor would call use the callback method for every package that &amp;ldquo;provided&amp;rdquo; a service which would return an opaque object, which would then be handed to &lt;code&gt;spell-check&lt;/code&gt; consumed callback (&lt;code&gt;consumeSpellCheckers&lt;/code&gt;) as an array. That allowed optional dependencies by publishing a service and then having other packages subscribe to them.&lt;/p&gt;
&lt;p&gt;Even if the publisher (&lt;code&gt;spell-check&lt;/code&gt; in the above example) would get every subscriber, then doesn't mean that the publisher then could call something back into every subscriber that would let you wire the two together to allow for a reverse- or even bi-directional flow of data.&lt;/p&gt;
&lt;p&gt;Since this also has system-specific elements, we use the same &lt;code&gt;attributes&lt;/code&gt; construct to give flexibility to extend outside of Bakfu.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    content: {
        id: &amp;quot;spell-check&amp;quot;,
        version: &amp;quot;0.77.1&amp;quot;,
        publish: {
            &amp;quot;bakfu:atom-service:spell-check&amp;quot;: {
                accept: &amp;quot;[1.0.0,2.0.0)&amp;quot;,
                attributes: {
                    callback: &amp;quot;consumeSpellCheckers&amp;quot;,
                },
            },
        },
    },
}

{
    content: {
        id: &amp;quot;spell-check-project&amp;quot;,
        version: &amp;quot;0.7.2&amp;quot;,
        subscribe: [
            &amp;quot;bakfu:atom-service:spell-check&amp;quot;: {
                accept: &amp;quot;[1.0.0,2.0.0)&amp;quot;,
                attributes: {
                    callback: &amp;quot;provideSpellCheck&amp;quot;,
                },
            },
        ],
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How the application handles those services isn't in the domain of Bakfu, just the gathering and providing the information that links the two. It would also be based on the application if there can be multiple conflicting publishers (can two packages both publish the same service and version?) or not.&lt;/p&gt;
&lt;h2&gt;Categorizing Dependencies&lt;/h2&gt;
&lt;p&gt;The last major topic to cover for this post is how to categorize dependencies. NPM and C# both have the concept of development and production dependencies. NPM is a little easier to work with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;// package.json
{
    dependencies: {
        &amp;quot;package-1&amp;quot;: &amp;quot;1.2.3&amp;quot;,
    },
    devDependencies: {
        &amp;quot;package-2&amp;quot;: &amp;quot;4.5.6&amp;quot;,
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust is more complicated because enabling a feature also changes the dependencies of the package. Many crates don't include serialization, but if you add the &lt;code&gt;serde&lt;/code&gt; feature, it would include &lt;code&gt;serde&lt;/code&gt; and any other requirements. In effect, this is an optional dependency but determined at build item.&lt;/p&gt;
&lt;p&gt;It also changes how we organize dependencies above by adding an extra layer inside the &lt;code&gt;requires&lt;/code&gt; element that provides an application-specific set of keys. In Rust, this may also include every feature as a separate element of dependencies.&lt;/p&gt;
&lt;p&gt;(I could have started with this, but I wanted to build the blocks and work out the ideas on a single dependency before getting into the more complex situations.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    content: {
        id: &amp;quot;Something.Else&amp;quot;,
        version: &amp;quot;1.0.0&amp;quot;,
        requires: {
            production: {
                &amp;quot;bakfu:nuget:BouncyCastle?framework=net&amp;quot;: &amp;quot;[1.8.9,2.0.0)&amp;quot;,
            },
            development: {
                &amp;quot;bakfu:nuget:DoesNotValidateInput&amp;quot;: &amp;quot;[1.0.0,2.0.0)&amp;quot;,
            },
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another reason to have this categorization is to allow us to identify what packages are vulnerable in an &lt;a href="https://overreacted.io/npm-audit-broken-by-design/"&gt;intelligent mannner&lt;/a&gt; instead of just assuming any and all vulnerabilities are important. In other words, if you trust your developers not to use a DDoSing regular expression in their code, you don't have to patch that code.&lt;/p&gt;
&lt;h2&gt;Expanding Dependency References&lt;/h2&gt;
&lt;p&gt;One of the areas I'm not sure about is now to describe dependences and provides. In the above examples, I'm using the URI format but my gut feeling is that it should be the more verbose form to allow for extensions.&lt;/p&gt;
&lt;p&gt;The previous example would then look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    content: {
        id: &amp;quot;Something.Else&amp;quot;,
        version: &amp;quot;1.0.0&amp;quot;,
        requires: {
            production: [
                {
                    type: &amp;quot;nuget&amp;quot;,
                    id: &amp;quot;BouncyCastle&amp;quot;,
                    accept: &amp;quot;[1.8.9,2.0.0)&amp;quot;,
                    attributes: {
                        framework: &amp;quot;net&amp;quot;,
                    },
                },
            ],
            development: [
                {
                    type: &amp;quot;nuget&amp;quot;,
                    id: &amp;quot;DoesNotValidateInput&amp;quot;,
                    accept: &amp;quot;[1.0.0,2.0.0)&amp;quot;,
                },
            },
        },
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm just not entirely sure about which one is the &amp;ldquo;best&amp;rdquo; but I'm also trying to work out ideas for a complete system, so I will probably use the URI unless the expanded makes sense. Ultimately, I really won't know until I've gotten through all the analysis and ideas before I can say one way or the other.&lt;/p&gt;
&lt;p&gt;(Though, I do lean toward more strict schema which means that the expanded form will probably be the final one.)&lt;/p&gt;
&lt;p&gt;I'm also not sure if the Bakfu format really needs to have &lt;code&gt;bakfu:&lt;/code&gt; in front of identifiers. I mean, it is inside the &amp;ldquo;bakfu&amp;rdquo; file.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Identifiers</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/02/12/package-management-identifiers/" />
    <updated>2023-02-12T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/02/12/package-management-identifiers/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">A short discussion on how to identify packages.
</summary>
    <content type="html">&lt;p&gt;The other really big topic when it comes to package is how to identify them. This is a lot more complicated because I view packages in a polyglot manner instead of a single foreign ecosystem.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Previous Updates&lt;/h2&gt;
&lt;p&gt;While I got to thinking (this is a living series), I didn't like the upgrade format in the versions post of this series. In there, I suggested this for Bakfu:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    content: {
        version: &amp;quot;1.10.0&amp;quot;,
        majorUpdate: &amp;quot;[1.10.0, 1.15.0)&amp;quot;,
        minorUpdate: &amp;quot;[1.10.0, 1.11.0)&amp;quot;,
    },
    package: {
        version: &amp;quot;2.0.0&amp;quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I think putting the update rules in their own category makes sense since they related to each other but also to handle adding other rules as needed in there. I thought about putting everything in a version, but then I would have &lt;code&gt;{version: { current: &amp;quot;1.2.3&amp;quot; }}&lt;/code&gt; which didn't feel right.&lt;/p&gt;
&lt;p&gt;We also should have the package format version.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    bakfu: {
        version: &amp;quot;0.0.1&amp;quot;,
    },
    content: {
        version: &amp;quot;1.10.0&amp;quot;,
        update: {
            major: &amp;quot;[1.10.0, 1.15.0)&amp;quot;,
            minor: &amp;quot;[1.10.0, 1.11.0)&amp;quot;,
        },
    },
    package: {
        version: &amp;quot;2.0.0&amp;quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Identifier Schemas&lt;/h2&gt;
&lt;p&gt;A package identifier is one of those things that varies greatly between different languages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NPM: &lt;code&gt;markdowny&lt;/code&gt;, &lt;code&gt;@mfgames-writing/format&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;NuGet: &lt;code&gt;MfGames.Nitride&lt;/code&gt;, &lt;code&gt;System.Collections.Generic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Minetest: &lt;code&gt;beds&lt;/code&gt;, &lt;code&gt;moreores&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java: &lt;code&gt;com.mfgames.ruby&lt;/code&gt; (an old game of mine)&lt;/li&gt;
&lt;li&gt;Go: &lt;code&gt;github.com/callicoder/packer/numbers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cargo: &lt;code&gt;serde&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Atom: &lt;code&gt;spell-check&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gives a pretty good overview of most languages that use packages. I don't think there are many other ways of referencing them. In addition to the ones above, I want to be able to allow anyone to create an arbitrary package ecosystem because I don't need to have a game using my calendar's packages (unless it wants to) but they are distinct.&lt;/p&gt;
&lt;h2&gt;Package Groups and Organizations&lt;/h2&gt;
&lt;p&gt;I will be up front, I don't like flat package systems such as Node and Cargo. They have a significant problem with name squatting (where someone &amp;ldquo;reserves&amp;rdquo; a package name that is ideal) and also because many of the packages are generic (Minetest's &lt;code&gt;beds&lt;/code&gt; or Node's &lt;code&gt;time&lt;/code&gt;) even if they are as capable.&lt;/p&gt;
&lt;p&gt;If you notice, almost all of my packages start with &lt;code&gt;mfgames&lt;/code&gt; or &lt;code&gt;MfGames&lt;/code&gt;. I do that to avoid conflicts with other packages. I thought I wouldn't for &lt;a href="/tags/mfgames-nitride/"&gt;Nitride&lt;/a&gt; but when I finally created the packages, someone had put one up on nuget.org and I promptly turned it back to &lt;code&gt;MfGames.Nitride&lt;/code&gt;. At this point, if someone is using &lt;code&gt;MfGames.&lt;/code&gt; in their packages, it's intentional (and probably malicious).&lt;/p&gt;
&lt;p&gt;Scoped packages are better, more so if there is a way of controlling who can use that scope. On nuget.org, you can reserve a prefix which is how they prevent someone from uploading a &lt;code&gt;Microsoft.HackYourComputer&lt;/code&gt; package. In theory, I could do the same with &lt;code&gt;MfGames&lt;/code&gt; but it requires some dependencies that I don't have lying around (cash).&lt;/p&gt;
&lt;p&gt;That said, I don't like NPM's &lt;code&gt;@organization&lt;/code&gt; because they &lt;em&gt;also&lt;/em&gt; use &lt;code&gt;@1.3.2&lt;/code&gt; for versions. So, then you have package and version identifiers as &lt;code&gt;@mfgames-writing/format@3.4.0&lt;/code&gt;. It isn't &amp;ldquo;pretty&amp;rdquo; and I am frequently driven by what I think looks good. As such, I'm inclined to avoid the &lt;code&gt;@&lt;/code&gt; whenever possible.&lt;/p&gt;
&lt;h2&gt;Polyglot Identifiers&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;pol·y·glot /ˈpälēˌɡlät/ -&amp;gt; adj. knowing or using several languages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When working with code, not only do we use different packages, we use packages that require entirely different systems. Probably one of the most common one for me is the bane of my JavaScript and TypeScript development: &lt;code&gt;node-gyp&lt;/code&gt;. Node-gyp requires a C compiler and Python to run, but it is for Node packages. So, that means that if we really want to identify a package (and its dependencies, a later post), we need to be able to address all of those dependencies in the system addressing scheme.&lt;/p&gt;
&lt;h2&gt;Uniform Resource Identifiers (URIs)&lt;/h2&gt;
&lt;p&gt;Since I want to be able to identify packages by something that jumps languages, it seems like a perfect use of a &lt;a href="https://en.wikipedia.org/wiki/Uniform_Resource_Identifier"&gt;URI&lt;/a&gt;. In short, a URI looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;URI = scheme &amp;ldquo;:&amp;rdquo; [&amp;quot;//&amp;quot; authority] path [&amp;quot;?&amp;quot; query] [&amp;quot;#&amp;quot; fragment]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The most common one we see are URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fedran.com/flight-of-the-scions/"&gt;https://fedran.com/flight-of-the-scions/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the above example, &lt;code&gt;https&lt;/code&gt; is the scheme, &lt;code&gt;//fedran.com&lt;/code&gt; is the authority, and &lt;code&gt;flight-of-the-scions/&lt;/code&gt; is the path. If we take the same idea, we could create a pseudo version of package identifiers that use the same thing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/markdowny"&gt;npm:markdowny&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/Humanizer"&gt;nuget:Humanizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/charmbracelet/lipgloss"&gt;go:github.com/charmbracelet/lipgloss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://content.minetest.net/packages/TenPlus1/ethereal/"&gt;minetest:ethereal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/clap"&gt;cargo:clap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(Side note, Humanizer is an awesome package for C# and everything Charm.sh is very pretty. Markdowny is my own tool for working with Markdown + YAML files, so I think it is fantastic.)&lt;/p&gt;
&lt;p&gt;This works for well-known package systems, but what about arbitrary ones? Say I create something for Author Intrusion, I could use &lt;code&gt;authorintrusion:spell-check&lt;/code&gt; but that would require registering a URI if I want to avoid conflict. A more efficient way (that I also will help with some future ideas) is to prefix them with &lt;code&gt;bakfu:&lt;/code&gt; instead and then allow domain or scoped registrations for non-well-known ecosystems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bakfu:npm:markdown&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bakfu:go:github.com/charmbracelet/lipgloss&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bakfu:authorintrusion.com/spell-check&lt;/code&gt; (I really should do something about that website.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Well-Known Systems and Aliases&lt;/h2&gt;
&lt;p&gt;Sadly, this does assume a centralized repository for the well-known (e.g., &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt;, &lt;code&gt;nuget&lt;/code&gt;) packages. It also leans into DNS instead of using other addressable schemas or pet names, I could also use &lt;code&gt;dotnet.microsoft.com&lt;/code&gt; for &lt;code&gt;nuget&lt;/code&gt; (since that is the general packaging system), &lt;code&gt;nodejs.org&lt;/code&gt;, and &lt;code&gt;go.dev&lt;/code&gt; for those languages.&lt;/p&gt;
&lt;p&gt;If I do go with the well-known, I would also have to establish a extension process that would allow communities to introduce new aliases or &amp;ldquo;register&amp;rdquo; their own to avoid conflict. While that isn't something I'd want to do, if this entire idea goes beyond me, it would be the first thing I would establish because I like to play well with others.&lt;/p&gt;
&lt;p&gt;I would also call it BEEP - Bakfu Emerging Extension Process so then I could have &lt;code&gt;BEEP-0000&lt;/code&gt; be the first one.&lt;/p&gt;
&lt;h2&gt;Qualified Identifiers&lt;/h2&gt;
&lt;p&gt;Using the URI, we can use URL-style query parameters to create a key/value pair for additional details about the package such as Cargo's features, .NET's frameworks, or whatever variants are important to include.&lt;/p&gt;
&lt;p&gt;For ones used by Bakfu, using a XML-style prefix would be more clear than either having a name collision (&lt;code&gt;version=&lt;/code&gt; could be common) or an arbitrary character prefix (&lt;code&gt;_&lt;/code&gt;). In these cases, &lt;code&gt;bakfu:version&lt;/code&gt; would be useful and allow for further extensions.&lt;/p&gt;
&lt;p&gt;For keys that need multiple, such as crate's features, the key would be suffixed by &lt;code&gt;[]&lt;/code&gt; to indicate an array (&amp;ldquo;Ruby style&amp;rdquo; is what I've heard this called).&lt;/p&gt;
&lt;p&gt;Rust has both platforms and featuers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bakfu:cargo:serde?bakfu:version=1.0.152&amp;amp;feature[]=derive&amp;amp;feature[]=rc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bakfu:cargo:serde?bakfu:version=1.0.152&amp;amp;platform=x86_64-unknown-linux-gnu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NuGet has frameworks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bakfu:nuget:Autofac?bakfu:version=6.5.0&amp;amp;framework=netstandard2.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bakfu:nuget:Autofac?bakfu:version=6.5.0&amp;amp;framework=net6.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given that, and the future discussion on dependencies, I think it would make sense to make it a query parameter instead of using the &lt;code&gt;#&lt;/code&gt; fragment. Also, some foreign ecosystems don't have versions (Minetest, at least one of the Python systems) which means even a version is optional.&lt;/p&gt;
&lt;h2&gt;Expanded Form&lt;/h2&gt;
&lt;p&gt;To simplify coding, the above URL could easily be broken down into a standard JSON structure for less condensed format with the &amp;ldquo;bakfu:&amp;rdquo; attributes pulled up a level.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    // bakfu:cargo:serde?bakfu:version=1.0.152&amp;amp;feature[]=derive&amp;amp;feature[]=rc&amp;amp;platform=x86_64-unknown-linux-gnu

    type: &amp;quot;cargo&amp;quot;,
    id: &amp;quot;serde&amp;quot;,
    version: &amp;quot;1.0.152&amp;quot;,
    attributes: {
        &amp;quot;platform&amp;quot;: &amp;quot;x86_64-unknown-linux-gnu&amp;quot;,
        &amp;quot;features&amp;quot;: [&amp;quot;derive&amp;quot;, &amp;quot;rc&amp;quot;],
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the moment, I'm going to use both formats but I lean toward the more expanded form in actual files with the condensed URI form when trying to explain things. I'm sure I will bounce back and forth between something that feels &amp;ldquo;right&amp;rdquo;. You may also notice that I spell out most things because I don't like abbreviations while communicating concepts.&lt;/p&gt;
&lt;h2&gt;Distributed Packages&lt;/h2&gt;
&lt;p&gt;This is for a future topic, but I am planning on addressing distributed packages that don't encourage using only a single source for the data. So, the URI above only addresses the framework-level components that identify a distinct package irregardless if you find it on nuget.org, npmjs.com, gitlab.com, or even &lt;a href="https://src.mfgames.com/"&gt;src.mfgames.com&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Versions</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/02/08/package-management-versions/" />
    <updated>2023-02-08T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/02/08/package-management-versions/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">A short discussion on packages and their versions.
</summary>
    <content type="html">&lt;p&gt;The start of almost every package journey starts with versions. This is how one distinguishes between different copies and hopefully makes sense of when to get the latest version verses only updating a few or not updating at all.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Semantic Dream&lt;/h2&gt;
&lt;p&gt;These days, almost every package system proclaims that they use &lt;a href="https://semver.org/"&gt;semantic versions&lt;/a&gt; and write the infrastructure as such. I love semantic versions with a passion, including putting them in my novels and stories. They are relatively clear and concise, and easy for me to understand: breaking-things.adding-things.fixing-things.&lt;/p&gt;
&lt;p&gt;The problem is, semantic versions are hard to manage when it comes to packages and dependencies, not to mention the business practices.&lt;/p&gt;
&lt;h2&gt;Romantic Versions&lt;/h2&gt;
&lt;p&gt;The biggest one are the ones who won't use it. One of the library I use during my day job is &lt;a href="https://korzh.com/"&gt;EasyQuery&lt;/a&gt; because they have a .NET and a JS library and give me a lot of flexibility in creating dynamic queries. They also are responsive to bug issues. What they don't do is follow semantic versions. They use romantic versions with every x.y version being a breaking change from the previous one. This applies to their code on NPM and NuGet.&lt;/p&gt;
&lt;p&gt;It is easy to manage, but you have to know that you can't go from 7.0.2 to 7.2.0 without major work, though semantic version says that it should have been a safe change.&lt;/p&gt;
&lt;p&gt;My day job product is also romantically versioned, so I deal with this on a quarterly basis.&lt;/p&gt;
&lt;h2&gt;Dependency Changes&lt;/h2&gt;
&lt;p&gt;Another example is &lt;code&gt;SlimMessageBus.Host&lt;/code&gt;. In version &lt;a href="https://www.fuget.org/packages/SlimMessageBus.Host/1.14.1"&gt;1.14.1&lt;/a&gt;, it requires &amp;ldquo;.netstandard2.0&amp;rdquo;. In &lt;a href="https://www.fuget.org/packages/SlimMessageBus.Host/1.15.0"&gt;1.15.0&lt;/a&gt;, it is &amp;ldquo;.netstandard2.1&amp;rdquo;. While this seems like safe change, it was a minor version bump that made it impossible for us to update because our product is still bound to &amp;ldquo;.netstandard2.0&amp;rdquo; until we can get off WebForms.&lt;/p&gt;
&lt;h2&gt;Version Ranges&lt;/h2&gt;
&lt;p&gt;Since it is important, there are also a number of ways of communicating version ranges. Node uses the &lt;code&gt;^&lt;/code&gt; for semantic versions and &lt;code&gt;~&lt;/code&gt; for &amp;ldquo;only patch updates&amp;rdquo; (basically what EasyQuery uses).&lt;/p&gt;
&lt;p&gt;NuGet uses a &lt;a href="https://learn.microsoft.com/en-us/nuget/concepts/package-versioning"&gt;different system&lt;/a&gt; such as &lt;code&gt;[1.0.0, 1.15.0)&lt;/code&gt; with the &lt;code&gt;[]&lt;/code&gt; meaning inclusive and &lt;code&gt;()&lt;/code&gt; meaning exclusive. This is my preferred system, mainly because it hearkens back to my computer science days and allows a mid-version break such as SlimMessageBus. In that regard, it is more flexible because &lt;code&gt;^&lt;/code&gt; and &lt;code&gt;~&lt;/code&gt; can translate directly into the verison range.&lt;/p&gt;
&lt;h2&gt;Being Explicit&lt;/h2&gt;
&lt;p&gt;I haven't gotten to dependencies yet, but the developer knows when something is safe update a package verses when it is not. EasyQuery knows that a patch is the breaking point, it would be better that they be able to communicate that as part of their metadata instead of requiring a customer support call. In effect, using the first two semantic version components (major and minor), EasyQuery 7.0.0 would be:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    version: &amp;quot;7.0.0&amp;quot;,
    majorUpdate: &amp;quot;[7.0.0, 7.1.0)&amp;quot;,
    minorUpdate: &amp;quot;[7.0.0, 7.1.1)&amp;quot;, // The third number is their minor versions
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the other hand, SlimMessageBus would use (using an older version for illustrative purposes):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    version: &amp;quot;1.10.0&amp;quot;,
    majorUpdate: &amp;quot;[1.10.0, 1.15.0)&amp;quot;,
    minorUpdate: &amp;quot;[1.10.0, 1.11.0)&amp;quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm assuming that anything more precise is a patch and is safe to update. I'm also think Bakfu should lean heavily on well-defined undefined values. So if &lt;code&gt;majorUpdate&lt;/code&gt;, then assume semantic version. If &lt;code&gt;minorUpdate&lt;/code&gt; is missing, then use &lt;code&gt;majorUpdate&lt;/code&gt; (which may be assumed).&lt;/p&gt;
&lt;h2&gt;Package Versions&lt;/h2&gt;
&lt;p&gt;The last bit is one that I've encountered a number of times while packaging books, but also while I was doing &lt;a href="https://manpages.debian.org/stretch/dpkg-dev/deb-version.5.en.html"&gt;Debian packages&lt;/a&gt;. The version of the package should be independent of the content. Using the above example, if SlimMessageBus realized they had made a breaking change, they should be able to update the 1.14.0 version of their package to indicate that 1.15.0 is a major breaking change.&lt;/p&gt;
&lt;p&gt;Today, their only choice would be to remove the 1.15.0 (and any later) version and create a 2.0.0 version. This can be a lot of work because most likely someone realized it after there were a number of updates already out in the field.&lt;/p&gt;
&lt;p&gt;Instead, if the package version was decoupled from the content version, then it would be possible to retroactively update critical information such as dependencies and safe upgrade ranges. Using SlimMessageBus again:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    package: {
        version: &amp;quot;1.0.0&amp;quot;,
    },
    content: {
        version: &amp;quot;1.10.0&amp;quot;,
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After they realize the breaking change:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;{
    package: {
        version: &amp;quot;2.0.0&amp;quot;,
    },
    content: {
        version: &amp;quot;1.10.0&amp;quot;,
        majorUpdate: &amp;quot;[1.10.0, 1.15.0)&amp;quot;,
        minorUpdate: &amp;quot;[1.10.0, 1.11.0)&amp;quot;,
    },
}
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Package Management - Introduction</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/02/07/package-management-introduction/" />
    <updated>2023-02-07T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/02/07/package-management-introduction/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="package-management" scheme="https://d.moonfire.us/tags/" label="Package Management" />
    <category term="mfgames-culture" scheme="https://d.moonfire.us/tags/" label="MfGames.Culture" />
    <category term="author-intrusion" scheme="https://d.moonfire.us/tags/" label="Author Intrusion" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.
</summary>
    <content type="html">&lt;p&gt;As I'm working through one project, I'm frequently planning the next. I noticed that a number of my future ideas, fantasies as you will, all have one thing in common: packages. It doesn't if it is a game idea or working on &lt;a href="/tags/mfgames-culture/"&gt;programming calendars&lt;/a&gt; or &lt;a href="/tags/author-intrusion/"&gt;Author Intrusion&lt;/a&gt;, they all need some form of package management.&lt;/p&gt;
&lt;p&gt;Given that and the rule of three, it makes sense to sit back and working that problem independent of the individual projects. Some people call this procrastinations. I like to call it composition. After a few months of rolling it around in my head, plus a bit of thoughts from Drew DeVault's &lt;a href="https://drewdevault.com/2021/09/27/Let-distros-do-their-job.html"&gt;various&lt;/a&gt; &lt;a href="https://drewdevault.com/2018/01/10/Learn-your-package-manager.html"&gt;posts&lt;/a&gt; on the topic, I figured I'd codify my ideas and see what I can do to create something I'm happy with in the future.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be a series of posts, but I have no idea of how fast I'll be writing them out or if it will pan out, but this is a list of the related posts. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/07/package-management-introduction/"&gt;2023-02-07 Package Management - Introduction&lt;/a&gt; - The beginning of a short series as I write down my ideas of an ideal package system with the future goal of writing a generic system called Bakfu.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/08/package-management-versions/"&gt;2023-02-08 Package Management - Versions&lt;/a&gt; - A short discussion on packages and their versions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/12/package-management-identifiers/"&gt;2023-02-12 Package Management - Identifiers&lt;/a&gt; - A short discussion on how to identify packages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/02/13/package-management-dependencies/"&gt;2023-02-13 Package Management - Dependencies&lt;/a&gt; - A review of different styles of package dependencies, including across different ecosystems, and then an attempt to consolidate them into a single unified system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/09/20/package-management-identifiers-2/"&gt;2023-09-20 Package Management - Identifiers 2&lt;/a&gt; - Using URNs to identify packages and some re-thinking of concepts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2023/11/30/package-management-formats/"&gt;2023-11-30 Package Management - Formats and Registries&lt;/a&gt; - Thoughts on setting up formats and layering registries for those formats on top of each other.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Package Systems&lt;/h2&gt;
&lt;p&gt;Packages are awesome because they allow over developers provide components, they split apart logic into smaller chunks which make it easier to understand, and they can have different update paths instead of needing a single monolithic deployment. This also means, I'm more likely to be able to focus on a small part instead of having to &amp;ldquo;swap in&amp;rdquo; an entire project. That is one thing that has made Nitride a lot more enjoyable to work with.&lt;/p&gt;
&lt;p&gt;At their core idea, a package is just a &amp;ldquo;blob&amp;rdquo; of something along with metadata. For purposes of this series, I don't care what is in the blob. It could be C# code, a WASM binary, or Rust source. The thing I've gotten hung up is that we have a ton of different package systems (e.g., LISP modules, Python packages, NPM, Minetest, Debian packages) and each one was written for a specific use and has different rules. Some are nicer on the disk, others support signed code, and some have methods to avoid squatters. Every community wants to write their own for their specific uses, but to me it feels like we are reinventing the wheel so many times when we could be focusing on more fun things (unless packaging systems is your idea of fun, it is mine).&lt;/p&gt;
&lt;p&gt;(Yes, I'm aware that I want to write my own for my own specific purpose, but that's going to happen anyways. It's just a matter of what framework I write it in.)&lt;/p&gt;
&lt;h2&gt;Terminology&lt;/h2&gt;
&lt;p&gt;Since I'm thinking about doing a series of posts, here are some terms I plan on using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Application - Whatever needs a package, be it a game, library, or utility.&lt;/li&gt;
&lt;li&gt;Bakfu - My hypothetical system for managing packages. This is the Lojban word for &amp;ldquo;package&amp;rdquo; and seems like a decent handle for what I'm working for.&lt;/li&gt;
&lt;li&gt;Foreign - Another packaging system.&lt;/li&gt;
&lt;li&gt;Package Ecosystem - All the servers, tools, and the packages themselves for a given type of packaging. This includes bakfu and foreign packaging ecosystems.&lt;/li&gt;
&lt;li&gt;Package Server - The servers used to query and distribute packages.&lt;/li&gt;
&lt;li&gt;Package - A specific type of package. This will have bakfu, application, or foreign as an adjective when necessary.&lt;/li&gt;
&lt;li&gt;Package Content - The &amp;ldquo;thing&amp;rdquo; being packaged. This can be a set of files, a word list, assets for a game, or even a foreign package. For purposes of these posts, this is a generic &amp;ldquo;blob&amp;rdquo; of data that is specific to an application.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bakfu&lt;/h2&gt;
&lt;p&gt;I want something generic. More specifically, I want to find what I consider are the &amp;ldquo;best&amp;rdquo; parts of the various systems I've worked with, solve problems that I've experienced, and maybe come up with a set of libraries and utilities that let me implement a packaging ecosystem that only requires me to override the application-specific components while delegating the rest of the ecosystem to bakfu.&lt;/p&gt;
&lt;p&gt;I have no intent on this being a &amp;ldquo;standard&amp;rdquo; because everyone likes to reinvent the wheel. Everyone has their own idea of what is the &amp;ldquo;right&amp;rdquo; way of handling packages, even when there is a defacto standard already implemented.&lt;/p&gt;
&lt;p&gt;That isn't to say this is only going to be for me. Like most of my work, I'm hoping to create something generic enough that others can use it for any purpose and in the languages of their choice and still get some of the benefits I envision.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>A Week of Dependencies plus Nor Curse Be Found 13</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/01/29/a-week-of-dependicies/" />
    <updated>2023-01-29T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/01/29/a-week-of-dependicies/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <category term="markdowny" scheme="https://d.moonfire.us/tags/" label="Markdowny" />
    <category term="nor-curse-be-found" scheme="https://d.moonfire.us/tags/" label="Nor Curse Be Found" />
    <category term="entanglement-2021" scheme="https://d.moonfire.us/tags/" label="Entanglement 2021" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="project-layout" scheme="https://d.moonfire.us/tags/" label="Project Layout" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <summary type="html">To write a chapter in *Nor Curse Be Found*, I ended up spending three days working on projects and tasks that needed to be done to provide the tools I use while writing.
</summary>
    <content type="html">&lt;p&gt;I'm trying to get get back to the one-week iterations/sprints for my personal projects. As one of my goals for the year, I want to write at least one short story or a chapter a month. &amp;ldquo;At least&amp;rdquo; being the goal since I want to write more, but writing &lt;a href="https://fedran.com/"&gt;Fedran&lt;/a&gt; is still a struggle because of burnout, &lt;a href="/tags/entanglement-2021/"&gt;entanglement&lt;/a&gt;, and just life.&lt;/p&gt;
&lt;h2&gt;Markdowny&lt;/h2&gt;
&lt;p&gt;So I dedicated this week to writing chapter thirteen of &lt;a href="/tags/nor-curse-be-found/"&gt;Nor Curse Be Found&lt;/a&gt;, but in the process of setting it up, I finally hit the point where bugs in &lt;a href="/tags/markdown/"&gt;Markdowny&lt;/a&gt; were too much for me. The main one is that I used the wrong shebang in the scripts but didn't realize it until I switched to &lt;a href="/tags/nixos/"&gt;NixOs&lt;/a&gt;. The key part is that I need to use &lt;code&gt;/usr/bin/env bash&lt;/code&gt; for all my shebangs instead of assuming the path to &lt;code&gt;bash&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;# !/usr/bin/env bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So that meant I needed to go into that project to fix it (and add a few features I've been wanting). But that was so old that I spent a few days bringing it up to fix my new &lt;a href="/tags/project-layout/"&gt;project layout&lt;/a&gt; so I had a consistent environment. While doing that, I ended up on yet another tangent to create a script that could create my script files. (Not sure where to store that, to be honest.)&lt;/p&gt;
&lt;p&gt;But then I finished the script to get the layout working to get Markdown working. After another day or so to coerce &lt;code&gt;markdowny&lt;/code&gt; into &lt;a href="/tags/woodpecker-ci/"&gt;Woodpecker CI&lt;/a&gt;, I was finally ready to actual write a feature I've been needing for a while.&lt;/p&gt;
&lt;h2&gt;New Feature&lt;/h2&gt;
&lt;p&gt;To submit entries to my writing group, I need to post a &amp;ldquo;events so far&amp;rdquo; document because it could be weeks or months between submissions. I use &lt;code&gt;markdowny&lt;/code&gt; to do that with the &lt;code&gt;list&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ markdowny list chapters/*.md | head -n 5
1. Always Moving: While Linsan waits for her mother to come home, she bounces around on the furniture and talks to her father. She announces that she has named a violin her father is making Palisis and learns that the violin is for her father's first wife who got married to her mother's best friend.

2. Early Lessons: Years later, Linsan is learning how to play the violin from her father. The lesson is interrupted when Dukan, her father's best friend and manager for the business, visits in a panic to tell him that the family's workshop in the valley is on fire.

3. Home Early: Unable to visit the burnt remains of the family's workshop, Linsan comes home to find her father work in depression. He had given up working on instruments and switched to writing articles about music. She goes into the attic to put some books away and finds Palisis in a corner, returned after Marin's death. She plays it, but then finds out that no one had ever played it before.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I needed a bit more details, so I decided to expand it to use Handlebars to give me a template for the chapters and to access all of the components inside the YAML header.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ markdowny list ~/src/fedran/sources/allegro/chapters/*.md --template='- Chapter {{_number}} - {{{title}}} =&amp;gt; {{{summary}}}' --trim-whitespace | head -n 5
- Chapter 1 - Always Moving =&amp;gt; While Linsan waits for her mother to come home, she bounces around on the furniture and talks to her father. She announces that she has named a violin her father is making Palisis and learns that the violin is for her father's first wife who got married to her mother's best friend.
- Chapter 2 - Early Lessons =&amp;gt; Years later, Linsan is learning how to play the violin from her father. The lesson is interrupted when Dukan, her father's best friend and manager for the business, visits in a panic to tell him that the family's workshop in the valley is on fire.
- Chapter 3 - Home Early =&amp;gt; Unable to visit the burnt remains of the family's workshop, Linsan comes home to find her father work in depression. He had given up working on instruments and switched to writing articles about music. She goes into the attic to put some books away and finds Palisis in a corner, returned after Marin's death. She plays it, but then finds out that no one had ever played it before.
- Chapter 4 - Solace in Memories =&amp;gt; As Linsan frequently did, she visited the family's ruins after school. The spot gave her peace despite everything they had lost. However, a bully from school, Dukan's daughter Brook, follows after her and they fight. During the brawl, they both manifest their powers: Linsan with music and Brook with concussion powers.
- Chapter 5 - Bitter Partings =&amp;gt; Linsan comes limping home after her fight with Brook. Her parents are surprised she is there, but then Dukan and Brook show up. Dukan has his daughter apologizes and then offers to send money to the Sterlig's. Linsan's father tries to refuse it, but Dukan phrases it as helping Linsan and they accept.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It might not be much, but it was something I've been looking for. It also means that I can include things like POV, when it happened, or other details in the list to help work timelines, locations, and the like. Or use it as a simple index page generator for HTML and Gemini pages, if someone found a use for that.&lt;/p&gt;
&lt;h3&gt;Tables&lt;/h3&gt;
&lt;p&gt;Of course, there is also a table approach to the same thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;markdowny table chapters/*.md --fields _basename title when.start locations.primary | head -n 5
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left;"&gt;_basename&lt;/th&gt;
&lt;th style="text-align: left;"&gt;title&lt;/th&gt;
&lt;th style="text-align: left;"&gt;when.start&lt;/th&gt;
&lt;th style="text-align: left;"&gt;locations.primary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-01.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Rutejìmo&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 4::22&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-02.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Confession&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 4::75&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left;"&gt;chapter-03.md&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Morning&lt;/td&gt;
&lt;td style="text-align: left;"&gt;1471/3/28 MTR 11::71&lt;/td&gt;
&lt;td style="text-align: left;"&gt;Shimusogo Valley&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Nor Curse Be Found&lt;/h2&gt;
&lt;p&gt;Despite all that, I got another chapter of Nor Curse Be Found written this afternoon. It isn't polished as I hope, I'm still struggling with the &amp;ldquo;feel&amp;rdquo; for the chapter and how the prince responds to things, but that is also something I have to finish to really understand and go back to edit.&lt;/p&gt;
&lt;p&gt;Overall, it was a good day (it took me three days to write the chapter).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Faking Time Magic with Nitride</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2023/01/21/faking-time-magic-with-nitride/" />
    <updated>2023-01-21T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2023/01/21/faking-time-magic-with-nitride/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <summary type="html">Just a little summary of how to handle time while generating sites with Nitride.
</summary>
    <content type="html">&lt;p&gt;It's been a while since I've talked about &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; and I thought I would do a short post about working with time-sensitive posts. In most cases, this is dating blob posts but it can also be how I dole out weekly chapters while building up a buffer while I enjoy some down time.&lt;/p&gt;
&lt;h2&gt;NodaTime&lt;/h2&gt;
&lt;p&gt;All the time elements in Nitride use &lt;a href="https://nodatime.org/"&gt;NodaTime&lt;/a&gt; instead of the base class library's &lt;code&gt;DateTime&lt;/code&gt;. Part of this is because it predates the advent of &lt;code&gt;LocalDate&lt;/code&gt; but also because NodaTime has good abstractions around providing the time for reliable manipulations of time, an obsession with correctness, and also a deep understanding of the complexities of temporal elements.&lt;/p&gt;
&lt;p&gt;None of these really matter, but I like the library and I felt it was a solid base. This is also one reason why Nitride is opinionated.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;To use the temporal library, you need to include the &lt;a href="https://src.mfgames.com/mfgames-cil/-/packages/nuget/mfgames.nitride.temporal"&gt;NuGet package&lt;/a&gt; into your project and then either inject the module into the class or use the extension method.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;NitrideBuilder builder = new NitrideBuilder(args)
    .UseTemporal(
        config =&amp;gt;
        {
            config
                .WithDateTimeZone(&amp;quot;America/Chicago&amp;quot;)
                .WithDateOptionCommandLineOption();
        });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The configuration is optional, but this shows the two most common options. The first sets the time zone for all the local dates to handle when your site generates even if your server or build machine is UTC or in a different time zone.&lt;/p&gt;
&lt;p&gt;The other adds a &lt;code&gt;--date YYYY-MM-DD&lt;/code&gt; option to the command line to let you choose the &amp;ldquo;now&amp;rdquo; that the site is running, such as to preview next week's post or see how everything will turn out. If the &lt;code&gt;--date&lt;/code&gt; is not provided or the command line option isn't used, then &amp;ldquo;now&amp;rdquo; will be now. Well, then, but it was now then, but if you run it now, then it will now. Well, then. (&lt;a href="https://en.wikipedia.org/wiki/Spaceballs"&gt;Spaceballs&lt;/a&gt; reference.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;dotnet run -- build --date 2023-01-25
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Basics&lt;/h2&gt;
&lt;p&gt;Even with the module, nothing more is going to happen than additional logging line in the output. Because this is an &lt;span class="missing-link" data-path="/tags/entity-component-system/"&gt;Entity-Component-System&lt;/span&gt;, adding time to an &lt;code&gt;Entity&lt;/code&gt; is as simple as setting the instant.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Instant instant;

var entity = new Entity().Set(instant);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, this only applies to a single entry, so there are two operations included that will mass assign &lt;code&gt;Instant&lt;/code&gt; components to entities. The first is &lt;code&gt;SetInstantFromComponent&lt;/code&gt;. This gets a &lt;code&gt;LocalDate&lt;/code&gt;, &lt;code&gt;DateTime&lt;/code&gt;, &lt;code&gt;DateTimeOffset&lt;/code&gt;, or &lt;code&gt;Instant&lt;/code&gt; from the entity (&lt;code&gt;Model.Date&lt;/code&gt; in this example) and returns it. If this returns a null, then no instant will be assigned.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class Model
{
    public string Access { get; set; } = &amp;quot;public&amp;quot;;
    public DateTime? Date { get; set; }
}

public Pipeline : PipelineBase
{
    private SetInstantFromComponent&amp;lt;PageModel&amp;gt; op;

    public Pipeline(
        SetInstantFromComponent&amp;lt;PageModel&amp;gt; op)
    {
        this.op = op
        .WithGetDateTimeObject(entity =&amp;gt; entity.Get&amp;lt;Model&amp;gt;().Date);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        return entities
            .Run(op)
            .ToList()
            .ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a basic functionality, it is pretty simple. However, the most common way I need to assign an &lt;code&gt;Instant&lt;/code&gt; are my blog posts which all have the following pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./posts/2023-01-21/faking-time-magic-with-nitride/index.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To handle this common condition, there is a second operation: &lt;code&gt;SetInstantFromPath&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;SetInstantFromPath op;

return entities
    .Run(op)
    .ToList()
    .ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one doesn't require any customization to it because it uses a regular expression with named groups to calculate the three components. This is the same as setting the regular expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;return entities
    .Run(op.WithPathRegex(@&amp;quot;(?&amp;lt;year&amp;gt;\d{4})[/-](?&amp;lt;month&amp;gt;\d{2})[/-](?&amp;lt;day&amp;gt;\d{2})&amp;quot;))
    .ToList()
    .ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously the default is ISO date time, but I'm not going to support anything besides that in the defaults since you can writ a custom regular expression with named groups to handle any arbitrary format. The default also handles &lt;code&gt;2028/01-21-faking-time-magic-with-nitride.md&lt;/code&gt; as well as my normal format. In the end, the &lt;code&gt;Entity&lt;/code&gt; has an &lt;code&gt;Instant&lt;/code&gt; component.&lt;/p&gt;
&lt;h2&gt;Using Instants&lt;/h2&gt;
&lt;p&gt;Of course, setting an &lt;code&gt;Instant&lt;/code&gt; requires it to be used somewhere. Since it is a component, it is relatively easy to get the latest X posts from a list of entities.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var latest = entities
    .WhereEntity&amp;lt;Instant&amp;gt;()
    .OrderByDescening(entity =&amp;gt; entity.Get&amp;lt;Instant&amp;gt;())
    .Take(x)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is used to create feeds, but that is a topic for later since generating Atom feeds needs some more attention before I'm comfortable with it.&lt;/p&gt;
&lt;p&gt;The instant can also be used to remove future instants via the creatively named &lt;code&gt;FilterOutFutureInstant&lt;/code&gt; which has no configurations thank to the use of the &lt;code&gt;--date&lt;/code&gt; parameter and &lt;code&gt;TimeService&lt;/code&gt; in its dependency-injected constructor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FilterOutFutureInstant op;

return entities
    .Run(op)
    .ToList()
    .ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In most cases, I gather up all the pages, generate the project pages so I can show future chapters but grayed out, then remove the future ones before producing the HTML so no one can &amp;ldquo;cheat&amp;rdquo; by just adding one to the page.&lt;/p&gt;
&lt;h2&gt;Schedules&lt;/h2&gt;
&lt;p&gt;The above operations and concepts pretty much got me through generating &lt;a href="//d.moonfire.us"&gt;d.moonfire.us&lt;/a&gt;. On the other hand, &lt;a href="//fedran.com"&gt;fedran.com&lt;/a&gt; needed something more since I don't date my chapters. Instead, they are things like &lt;a href="//fedran.com/sand-and-blood/chapter-01/"&gt;fedran.com/sand-and-blood/chapter-01/&lt;/a&gt; and they need to be sent out every week until I run out of buffer or fail to keep up.&lt;/p&gt;
&lt;p&gt;To support that, I added a &lt;a href="https://src.mfgames.com/mfgames-cil/-/packages/nuget/mfgames.nitride.temporal"&gt;second NuGet package&lt;/a&gt;, &lt;code&gt;MfGames.Temporal.Schedules&lt;/code&gt; which contains more complex operations that adjust or change an entity's attributes based on the current date. In both cases, they are driven off a &amp;ldquo;path&amp;rdquo; which may be any arbitrary component but default to the entity's &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; component. They allow me to set up a schedule in a separate YAML file (could be JSON, could be on the page) and then they change.&lt;/p&gt;
&lt;h3&gt;PeriodicPathRegexSchedule&lt;/h3&gt;
&lt;p&gt;The first is &lt;code&gt;PeriodicPathRegexSchedule&lt;/code&gt; which is the initial evolution of writing a schedule for static sites. It uses a regular expression to capture the numeric part of a path (&lt;code&gt;12&lt;/code&gt; from &lt;code&gt;chapter-12&lt;/code&gt;) and figure out how long to show it. If we use the &lt;code&gt;Model.Access&lt;/code&gt; above to control access, we can easily make chapters available for subscribers and then spread them out a week at a time starting at the beginning of the year.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;schedules:
  # Patron and Ko-Fi subscribers get it all at once
  - pathRegex: chapters/chapter-(\d+)
    scheduleStart: 2000-01-01
    schedulePeriod: instant
    access: subscribers
  - pathRegex: chapters/chapter-(\d+)
    scheduleStart: 2023-01-01
    schedulePeriod: 1 week
    access: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Depending on the &amp;ldquo;now&amp;rdquo; when the site is run, all the chapters are available to patrons but they will be made public at the rate of one per week starting with the first chapter on January 1st. Of course, with regular expressions, I could easily stop posting chapters for a while by splitting the regular expression in two to cover two ranges. Or I can have two schedules that have a tier-1 and a tier-2 release with a public release a week after tier-2 gets it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;schedules:
  - pathRegex: chapters/chapter-(\d+)
    scheduleStart: 2000-01-01
    schedulePeriod: tier-1
    access: subscribers
  - pathRegex: chapters/chapter-(\d+)
    scheduleStart: 2023-01-01
    schedulePeriod: 1 week
    access: tier-2
  - pathRegex: chapters/chapter-(\d+)
    scheduleStart: 2023-01-08
    schedulePeriod: 1 week
    access: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can also get complicated when I had to take breaks from chapters:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;schedules:
  # Patron and Ko-Fi subscribers get it all at once
  - pathRegex: chapters/chapter-([0-1]\d|2[0-4])
    scheduleStart: 2000-01-01
    schedulePeriod: 2 weeks
    access: public
  - pathRegex: chapters/chapter-(2[5-9]|[3-9]\d)
    scheduleStart: 2023-01-01
    schedulePeriod: 2 weeks
    access: public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously, implementing this is more complicated then setting the instant from the path.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class PageSchedule : NumericalPathSchedule
{
    public Access { get; set; }

    protected override Entity Apply(Entity entity, int number, Instant instant)
    {
        var model = entity.Get&amp;lt;Model&amp;gt;();
        model.Access = this.Access;
        return entity.Set(instant, model);
    }
}

List&amp;lt;PageSchedule&amp;gt; schedules;
Ienumerable&amp;lt;Entity&amp;gt; entities;

return applySchedules.WithSchedules(_ =&amp;gt; schedules).Run(entities);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IndexedPathRegexSchedule&lt;/h3&gt;
&lt;p&gt;The second method for schedules is &lt;code&gt;IndexedPathRegexSchedule&lt;/code&gt;. This is the most complicated to set up in C# code, but I find the easiest to comprehend in the YAML code. Instead of having a complex set of schedules and multiple regular expressions, it goes with a single regular expression to identify the numerical component and then has a dictionary of elements that say &amp;ldquo;from chapters X until the next, for chapters Y and later, do this, etc.&amp;rdquo;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;schedules:
    pathRegex: chapters/chapter-(\d+)
    indexes:
        1:
            scheduleStart: 2025-01-01
            schedulePeriod: instant # means all the chapters 1-10 at once
            access: subscribers
        10:
            scheduleStart: 2030-01-01 # chapter 10 on 2030-01-01, chapter 11 on 2023-01-08, etc.
            schedulePeriod: 1 week
            access: subscribers
        30:
            schedulePeriod: never # never going to see these
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one works easier for me, mainly because Fedran doesn't have subscriber tiers or complex logic but I do have frequent pauses while posting. Also, the schedule isn't an outer list (though it could be, &lt;code&gt;ApplySchedules&lt;/code&gt; applies every schedule in order so you can have two of these without a problem).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class Schedule : IndexedSchedule
{
    public string? Access { get; set; }

    protected override Entity Apply(
        Entity entity,
        int number,
        Instant instant)
    {
        TestModel model = entity.Get&amp;lt;TestModel&amp;gt;();
        model.Access = this.Access;
        return entity.SetAll(instant, model);
    }
}

IndexedPathRegexSchedule&amp;lt;Schedule&amp;gt; schedules;
Ienumerable&amp;lt;Entity&amp;gt; entities;

return applySchedules.WithSchedules(_ =&amp;gt; new [] { schedules }).Run(entities);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As you may notice, these don't filter out or do anything other than set the &lt;code&gt;Instant&lt;/code&gt; and maybe some other components of an &lt;code&gt;Entity&lt;/code&gt;. This follows the Single Responsibility Principle (SRP) but also &amp;ldquo;do one thing well&amp;rdquo;. If you want to filter it out, use an operation for that. If you want to dynamically change the contents of a page based on the current day, that's a schedule.&lt;/p&gt;
&lt;p&gt;This is also a starting point. Everything is fluid. I have thirteen websites I'm planning on converting over to Nitride over the next year or so. The fourth site is mfgames.com when I'll start documenting all of this and probably start pushing to see if others want to use it. Right now, the only documentation comes from &lt;a href="https://src.mfgames.com/mfgames-cil/"&gt;https://src.mfgames.com/mfgames-cil/&lt;/a&gt; in the examples, read me files, and tests.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Capabilities</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/12/21/capabilities/" />
    <updated>2022-12-21T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/12/21/capabilities/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mastodon" scheme="https://d.moonfire.us/tags/" label="Mastodon" />
    <category term="pleroma" scheme="https://d.moonfire.us/tags/" label="Pleroma" />
    <category term="gotosocial" scheme="https://d.moonfire.us/tags/" label="GoToSocial" />
    <category term="emoji" scheme="https://d.moonfire.us/tags/" label="Emoji" />
    <category term="markdown" scheme="https://d.moonfire.us/tags/" label="Markdown" />
    <summary type="html">Lately, I've been thinking about capabilities of server APIs and how the clients interact with them. This is driven by a couple things I'm considering doing in the future but also with the frustration that certain features in the fediverse are difficult to implement for clients.
</summary>
    <content type="html">&lt;p&gt;Lately, I've been thinking about capabilities of server APIs and how the clients interact with them. This is driven by a couple things I'm considering doing in the future but also with the frustration that certain features in the fediverse are difficult to implement for clients.&lt;/p&gt;
&lt;p&gt;In specific, I'm very fond of &lt;a href="/tags/markdown/"&gt;Markdown&lt;/a&gt; posts and &lt;a href="/tags/emoji/"&gt;emoji responses&lt;/a&gt;. I love Markdown for all things and I like the low-attention responses of emojis when you don't need to have a big post to say &amp;ldquo;I'm sorry&amp;rdquo; or &amp;ldquo;that blows&amp;rdquo;. My family and work buddies use emoji responses fairly heavily, and it is well supported with Slack, Discord, and even Microsoft Teams as of this week.&lt;/p&gt;
&lt;p&gt;Mastodon decided &lt;a href="https://github.com/mastodon/mastodon/issues/4106"&gt;not to implement them&lt;/a&gt; which is one reason I like &lt;a href="https://pleroma.social/"&gt;Pleroma&lt;/a&gt; and &lt;a href="https://join.misskey.page/en-US/"&gt;Misskey&lt;/a&gt;, though Misskey has historically been too heavy to put on my servers so one of my accounts uses Pleroma.&lt;/p&gt;
&lt;p&gt;However, my favorite Android client, &lt;a href="https://tusky.app/"&gt;Tusky&lt;/a&gt; handles Markdown fairly well on the Glitch instance I'm on, but doesn't for Pleroma and it doesn't do emoji responses at all. &lt;a href="https://husky.adol.pw/"&gt;Huksy&lt;/a&gt; supports these as a fork of Tusky specifically for Pleroma. But Husky doesn't really play well with the Glitch, which means I struggle to find One Client to Rule Them All™.&lt;/p&gt;
&lt;p&gt;Likewise, I want to switch my Pleroma for &lt;a href="https://docs.gotosocial.org/en/latest/"&gt;GoToSocial&lt;/a&gt; for various reasons, but they also support Markdown but not emoji responses. Mainly because there is no standard for emoji responses and if I want to get emoji responses, I need broader acceptance or patterns to work with.&lt;/p&gt;
&lt;p&gt;So, this comes down to capabilities. I want there was a way of having Tusky be able to query the server to determine it can handle emoji responses, so it can turn on that logic so I don't have to have Tusky and Husky. Simple, but I also want it to have the ability to know if the server can handle Markdown (base Mastodon can't, Glitch can) without doing various server identification scans.&lt;/p&gt;
&lt;p&gt;This means a fedi server should have some ability to say &amp;ldquo;I can handle this feature&amp;rdquo; that can be queried. If there was a standard for that, then Tusky or a CLI or any other client could selectively enable or disable the feature as needed.&lt;/p&gt;
&lt;h2&gt;Fine-Grained&lt;/h2&gt;
&lt;p&gt;This is talking about a fine-grained approach to APIs. We have many different fedi servers out there, being developed and also in production. Likewise, we have a number of clients that are also growing as the fedi becomes more popular. I think we're going to see features moving from server to server as they become interesting and there is no way every client can make that choice for every server.&lt;/p&gt;
&lt;p&gt;Identifying those abilities will make it easier to say &amp;ldquo;this client supports Markdown for those servers who have it&amp;rdquo;. It decouples the tie between the servers and the clients by giving a bit of an abstraction, not unlike how the &lt;a href="https://microsoft.github.io/language-server-protocol/"&gt;Language Server Protocol&lt;/a&gt; has made developing language refactoring a lot easier across multiple clients by working to an interface.&lt;/p&gt;
&lt;h2&gt;Approach 1 - Instance Information&lt;/h2&gt;
&lt;p&gt;The first approach, which is somewhat specific to fedi servers, is to update the instance information (&lt;a href="https://docs.joinmastodon.org/methods/instance/#v2"&gt;Mastodon&lt;/a&gt;, Pleroma doesn't have it implemented) to provide that information to clients. This would require updating Glitch and submitting a patch to Pleroma (and maybe Misskey) once some sort of understand/agreement could be found on how to identify these features.&lt;/p&gt;
&lt;p&gt;This is probably the easiest and I think would really benefit the client ecosystem as a whole, not only to give me the features I want.&lt;/p&gt;
&lt;h2&gt;Approach 2 - OpenAPI Support&lt;/h2&gt;
&lt;p&gt;A more generic approach, which is beyond just fedi servers, is to encourage most API servers to also expose their &lt;a href="https://spec.openapis.org/oas/v3.1.0"&gt;OpenAPI v3.1.0&lt;/a&gt; file in a well-known location that describes all the endpoints they support for that specific instance. This has benefits beyond just describing what they can handle.&lt;/p&gt;
&lt;p&gt;However, knowing there is an endpoint that can handle something doesn't mean it works the same way. Pleroma may have the same abilities as Glitch for Markdown, but they may not be communicating them in the same way or require different inputs. There needs to be a signal that indicates a specification is in use.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;p&gt;OpenAPI has a &lt;code&gt;$ref&lt;/code&gt; which can be used to describe an endpoint; this reference is a &lt;a href="https://json-spec.readthedocs.io/reference.html"&gt;JSON Reference&lt;/a&gt; that allows an endpoint to point to another specification either in the same document or to an &lt;a href="https://swagger.io/docs/specification/using-ref/"&gt;external URL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is the external URL that interests me. If that points to a fragment of OpenAPI specification that is documented for a specific feature, that would be a signal that the endpoint impelments that specific feature. In other words, if there was an &lt;code&gt;/api/emoji&lt;/code&gt; endpoint that is described as:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
    &amp;quot;$ref&amp;quot;: &amp;quot;https://well.known.location/emoji.xml&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then a client could use that information to assume that the endpoint has those abilities and can handle those requests.&lt;/p&gt;
&lt;h3&gt;Capabilities&lt;/h3&gt;
&lt;p&gt;As mentioned before, Mastodon doesn't have that ability since a posting is a posting, be it a response or a full text. OpenAPI also has the ability to add &lt;a href="https://spec.openapis.org/oas/v3.1.0#specification-extensions"&gt;additional specifications&lt;/a&gt; to an endpoint, such as a &lt;code&gt;x-additional-refs&lt;/code&gt; that has an array of &lt;code&gt;$ref&lt;/code&gt; object that do point to specific ones.&lt;/p&gt;
&lt;p&gt;That way, then Mastodon &lt;a href="https://docs.joinmastodon.org/methods/statuses/#create"&gt;post status endpoint&lt;/a&gt; could add a &lt;code&gt;x-additional-$refs&lt;/code&gt; that would allow it to describe the ability to have polls (added in 2.8.0), schedules (2.7.0), in a clear manner that clients would know it could handle those correctly without guessing or inferring from server information. Or&amp;hellip; emoji.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
    &amp;quot;paths&amp;quot;: {
        &amp;quot;/api/v1/statuses&amp;quot;: {
            &amp;quot;post&amp;quot;: {
                &amp;quot;operationId&amp;quot;: &amp;quot;postStatus&amp;quot;,
                &amp;quot;parameters&amp;quot;: [],
                &amp;quot;x-additional-$refs&amp;quot;: [
                    &amp;quot;https://well.known.location/emoji.xml&amp;quot;
                ]
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach also appeals to some other projects I have in mind, so I'm curious to see what others think of it.&lt;/p&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;Obviously, I have a specific goal in mind: Markdown and emoji. But hopefully there is also a benefit for allowing other servers to provide this information and make it easier to create clients and interfaces to their servers.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Ceph and NixOS</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/12/10/ceph-and-nixos/" />
    <updated>2022-12-10T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/12/10/ceph-and-nixos/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="ceph" scheme="https://d.moonfire.us/tags/" label="Ceph" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="entanglement-2021" scheme="https://d.moonfire.us/tags/" label="Entanglement 2021" />
    <summary type="html">Some things I've figured out trying to get Ceph working on my NixOS system.
</summary>
    <content type="html">&lt;p&gt;Recently along the continuing chain of my &lt;a href="/tags/entanglement-2021/"&gt;entanglement&lt;/a&gt;, the media server died. We've been ignoring the symptoms for a while such as randomly crashing with hard drive failures, refusing to start up, and occasionally making a high-pitched noise. All signs that something terrible was going to happy but we were in the middle of other problems and just pushed it off.&lt;/p&gt;
&lt;p&gt;Then it died properly and wouldn't get back up.&lt;/p&gt;
&lt;h2&gt;The Plans&lt;/h2&gt;
&lt;p&gt;Over the years, I've been looking at &lt;a href="https://ceph.com/en/"&gt;Ceph&lt;/a&gt; ever since &lt;a href="https://dreamhost.com/"&gt;DreamHost&lt;/a&gt; did a blog post about it. It seemed perfect for my previous experiences in crashing RAIDs and trying to find enough disk space to ram &amp;ldquo;just one more database&amp;rdquo; needed to get an analysis done. These same problems leaked into my media collection and keeping track of Partner's photo shoots.&lt;/p&gt;
&lt;p&gt;When the drives crashed, I figured I'd sit down and do a Ceph cluster instead and see if it would be easier to bring more drives online as I run out without having to tear down servers and replace drives or find another spot to fit the server. Plus the whole idea of being able to have duplicates appealed to me.&lt;/p&gt;
&lt;p&gt;After a bit of hemming and hawing, I picked up five 6 TB drives and waited half a month for them to show up (because I'm avoiding Amazon as much as possible and &lt;a href="https://newegg.com/"&gt;NewEgg&lt;/a&gt; is great (except that I cannot run their application on my phone).&lt;/p&gt;
&lt;h2&gt;Initial Set Up&lt;/h2&gt;
&lt;p&gt;After putting in the first drive, I promptly set up the other four for Ceph. Because of various bugs, frustrations, and learning curve difficulties, it took me almost a week. I was expecting NixOS to be able to set up drives, but it couldn't. It has options for things, but there are a lot of little fiddly bits and dials to do manually and then turn them into the Nixian way after the fact.&lt;/p&gt;
&lt;h2&gt;OSDs and NixOS&lt;/h2&gt;
&lt;p&gt;Setting up an OSD on Nix seems pretty simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.ceph.osd = {
  enable = true;
  daemons = [&amp;quot;0&amp;quot;];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But that was not meant to be. NixOS doesn't really set things up for you, so you have to do it manually. Also, Ceph plays well with &lt;code&gt;systemd&lt;/code&gt; but it does not play with Nix's version of systemd.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ceph-volume lvm create --data /dev/sdb --no-systemd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above command will create the proper entries in &lt;code&gt;/var/lib/ceph/osd/osd-0&lt;/code&gt; and things will &lt;em&gt;appear&lt;/em&gt; to be working fine. But I found this is a lie. When the system restarts, it will helpfully wipe out the entire contents of &lt;code&gt;/var/lib/ceph/osd/osd-0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To fix that, after I call &lt;code&gt;ceph-volume&lt;/code&gt; above, I had to do this to get the state to recover restarting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /var/lib/ceph/osd
tar -cjf ~/osd-0.tar.bz2 osd-0
systemctl restart ceph-osd-0.service
tar -xjf ~/osd-0.tar.bz2
chown -R ceph:disk osd-0
systemctl restart ceph-osd-0.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, &lt;code&gt;systemctl restart ceph-osd.target&lt;/code&gt; didn't blow away all my files. This is because there are some tmpfile rules in the systemd configuration for OSD that don't seem to be in the mon, mds, or mgr entries.&lt;/p&gt;
&lt;h2&gt;Corrupted Drives&lt;/h2&gt;
&lt;p&gt;It took another week of copying as much as I could off the existing corrupted drives. Thankful, I could mount them with &lt;code&gt;ntfs-3g&lt;/code&gt; which means &lt;code&gt;rsync&lt;/code&gt; could grind through them, spending about an hour for every file that was unrecoverable before moving on.&lt;/p&gt;
&lt;p&gt;Over the years, I've had to recover bad drives thrice now. One time it took me almost two weeks to recover what I could off of Partner's laptop drives. This was one of the big reasons why I decided to go with Ceph, to handle the cases when we have photoshoots and large files and then lose the hard drive they are stored on.&lt;/p&gt;
&lt;h2&gt;Mistake One&lt;/h2&gt;
&lt;p&gt;I managed to get it up to find out my first mistake: Ceph needs 1 GB of RAM for every 1 TB of disk and I had 24 TB on a 16 GB machine. I also only had a 8 GB swap partition for the machine, which means everything was just grinding away.&lt;/p&gt;
&lt;p&gt;To compensate, I could have removed the drives but I had just spent a week copying files over and basically the old drives were toast. So I spent a few hundred dollars and picked up two cheap Dell business towers from NewEgg instead. When they showed up, I started the process of moving one of the drive which involves &amp;ldquo;out&amp;rdquo; the OSD (the disk) and then Ceph gracefully moved the files off that drive so it can be safely moved.&lt;/p&gt;
&lt;h2&gt;Mistake Two&lt;/h2&gt;
&lt;p&gt;The second mistake was a minor one, the new computers didn't have drive rails so I needed to order a few more ports while I waited for the first disk to be cleared off so I could move it to a second drive I decided to put the 3.5&amp;quot; drives into the 5.25&amp;quot; bay because I didn't need the DVD
and it was easier to run the wires.&lt;/p&gt;
&lt;h2&gt;Mistake Three&lt;/h2&gt;
&lt;p&gt;The third mistake was probably a big one. I picked the wrong drive to &amp;ldquo;out&amp;rdquo; and moved a live drive into the second machine. I also learned that Ceph is very tolerant of moving said drives but it gets very cranky. Since the old PC (my first Ceph server) was struggling, the rubber for the mounting screws cracked so I decided to go the slow approach and just &amp;ldquo;in&amp;rdquo; the drive I thought I was going to remove, &amp;ldquo;out&amp;rdquo; the one I'm actually moving, and used a Sharpie to identify said drives so I don't make that mistake again.&lt;/p&gt;
&lt;h2&gt;Adding Monitors&lt;/h2&gt;
&lt;p&gt;When I had three systems, I decided I needed to bring up a monitor on all three to get some balancing. Not to mention having a monitor go down means the entire system crashed. However, this took me a few tries because of how NixOS handles systems.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export MID=$(hostname)
export MIP=$(host $MID.local | cut -f 4 -d ' ')

cd /var/lib/ceph/mon
mkdir ceph-$MID
cd ceph-$MID
mkdir /tmp/add-ceph-mon
ceph auth get mon. -o /tmp/add-ceph-mon/keyring
ceph mon getmap -o /tmp/add-ceph-mon/map
ceph-mon -i $MID --mkfs --monmap /tmp/add-ceph-mon/map --keyring /tmp/add-ceph-mon/keyring
ceph-mon -i $MID --public-addr $MIP
rm -rf /tmp/add-ceph-mon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Starting up the new monitor is fine as long as you &lt;em&gt;do not&lt;/em&gt; touch the NixOS configuration files. This first part uses &lt;code&gt;killall&lt;/code&gt; and &lt;code&gt;host&lt;/code&gt; to make the monitor ID equal to the server. When it comes up, it will be running in daemon mode which means NixOS will forget it on restart:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root        1792  1.0  0.3 575324 59284 ?        Ssl  17:00   0:00 ceph-mon -i notil --public-addr 192.168.2.36
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, I kill that process, either with &lt;code&gt;kill 1792&lt;/code&gt; or &lt;code&gt;killall ceph-mon&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, I configure the server to start up the server and use &lt;code&gt;colmena&lt;/code&gt; to push out the changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.ceph = {
  mon.enable = true;
  mon.daemons = [&amp;quot;paruk&amp;quot;];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I make sure everything is finally up, &lt;em&gt;then&lt;/em&gt; I add them to the initial hosts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.ceph = {
  global = {
    monInitialMembers = &amp;quot;muliq,notil,paruk&amp;quot;;
    monHost = &amp;quot;muliq.local,notil.local,paruk.local&amp;quot;;
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;Overall, I'm still happy with this. There is a &lt;em&gt;huge&lt;/em&gt; learning curve when it comes to Ceph and NixOS, mainly in how they interact with each other. I assume there will be more difficulties but I seem to be heading in the right direction.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Migrated Fedran to Nitride</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/11/25/fedran-and-nitride/" />
    <updated>2022-11-25T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/11/25/fedran-and-nitride/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="flight-of-the-scions" scheme="https://d.moonfire.us/tags/" label="Flight of the Scions" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="gatsby" scheme="https://d.moonfire.us/tags/" label="Gatsby" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <summary type="html">Some thoughts about migrating Fedran to Nitride.
</summary>
    <content type="html">&lt;p&gt;Part of my push to get &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt; published, I had to rewrite my website because the &lt;a href="https://fedran.com/flight-of-the-scions/"&gt;landing page&lt;/a&gt; couldn't get any commerical links or be updated. That meant I had two options before me, either to figure out the &lt;a href="/tags/gatsby/"&gt;Gatsby&lt;/a&gt; errors that caused me to stop adding new content years ago or move forward and migrate the site to &lt;a href="/tags/nitride/"&gt;Nitride&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since moving forward with Nitride would benefit me in the long wrong (I could add new content) but would also set me back (not everything moved along), I decided Nitride was the best approach. So, in the middle of everything going on, I sat down and migrated the website.&lt;/p&gt;
&lt;h2&gt;Appearance&lt;/h2&gt;
&lt;p&gt;I went with a much different look for the site, mostly based on the CSS theme for &lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt;, but with the &amp;ldquo;standard&amp;rdquo; green theme I use for Fedran. I also knew I would be eventually hosting it on &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; so much of the complex layouts and multiple columns were taken away in favor of CSS Grids and linear lists. This also let me create a &amp;ldquo;dark&amp;rdquo; and &amp;ldquo;light&amp;rdquo; mode since the original Fedran site predated my evolving preference for dark mode (and the CSS to support it). As a bonus, I went through and made sure everything passed Firefox's accessibility page for AA contrast.&lt;/p&gt;
&lt;p&gt;This did make the site a bit more &amp;ldquo;generic looking&amp;rdquo; but that is also where my preferences are leading too. Complex sites are pretty to use but I wanted something easier to navigate and less confusing (a frequent complaint). It also meant I got rid of those fancy buttons with the &lt;a href="https://fedran.com/identifiers/"&gt;identifiers&lt;/a&gt; on the left and the various keywords on the right in favor of something like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Flight of the Scions (0047-00) → Kanéko • Novel (129k)&lt;/li&gt;
&lt;li&gt;Sand and Ash (0100-01) → Rutejìmo • Novel (78k)&lt;/li&gt;
&lt;li&gt;Sand and Blood (0100-00) → Rutejìmo • Novel (69k)&lt;/li&gt;
&lt;li&gt;Sand and Bone (0100-02) → Rutejìmo • Novel (82k)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;On the other hand, as I integrate genres and other information, the bullet list version will scale much easier and I won't struggle to layout it out in neat columns. (You can also see I integrated word counts into the listings, which is new).&lt;/p&gt;
&lt;h2&gt;Migration&lt;/h2&gt;
&lt;p&gt;The migration went surprisingly easy. I was originally planning on holding off fedran.com until the end, because it is my &amp;ldquo;flagship&amp;rdquo; and most complicated site, but the various styles and concepts that I was working with were too different as long as I didn't try to integrate the custom calendar or build up the time line (two features that were removed but will come back eventually). The idea for multiple category lists came from the MediaWiki version of the site. I change the format how plugins were listed on the page, but that was just to work around &lt;a href="https://github.com/xoofx/markdig"&gt;MarkDig's&lt;/a&gt; and &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; functionality. The only big problem with my wiki links which I originally had a Markdown links with no paths such as &lt;code&gt;[Rutejìmo]()&lt;/code&gt; but eventually wrote a proper MarkDig extension so I could say &lt;code&gt;[[Rutejìmo]]&lt;/code&gt; instead and fit with better conventions.&lt;/p&gt;
&lt;p&gt;The biggest thing was the 80+ Git repositories. I had already decided to move the retrieval out of the Nitride process (as opposed to Gatsby trying to do everything), so I had those prepared in a shell script called before building. And I had already tried out some ideas when integrating my &lt;a href="/garden/"&gt;garden&lt;/a&gt; where a Nitride pipeline looped through the projects and called a builder on each one before gathering all the results into a single pipeline.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var entities = this.fileSystem    
    .EnumerateItems(&amp;quot;/fedran/sources&amp;quot;, SearchOption.TopDirectoryOnly)
    .OrderBy(a =&amp;gt; a.Path.ToString())
    .SelectMany(this.CreateProject)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Being able to set the &amp;ldquo;/&amp;rdquo; of a project to the input files is a great feature of Zio and I use it for most projects that need to view a directory structure without worrying about where that directory is located.&lt;/p&gt;
&lt;p&gt;Overall, it took two weeks.&lt;/p&gt;
&lt;h2&gt;New Features&lt;/h2&gt;
&lt;p&gt;There are two really cool features that I'm proud of. The first is that dead links (e.g., pages I haven't written) are not automatically turned into spans with a class. That means if a link is there, it will go to a page as opposed to having so many be missing. I think this will improve the experience a lot while still giving me the hooks to fill in the pages and eventually aim for a 100% coverage of pages.&lt;/p&gt;
&lt;p&gt;The other related to the Fedran's POV coloring scheme. Every character has a different color associated with them. The entire idea was that you could identify the series by the primary colors in the cover. Well, as part of the rewrite, I switched from SASS variables to CSS variables which meant I could inject a POV-specific stylesheet on the pages so now the POV charcters pages have the same color scheme as their covers, such as &lt;a href="https://fedran.com/sand-and-blood/"&gt;Sand and Blood&lt;/a&gt;, &lt;a href="https://fedran.com/rutejimo/"&gt;Rutejìmo&lt;/a&gt;, &lt;a href="https://fedran.com/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt;, and &lt;a href="https://fedran.com/allegro/"&gt;Allegro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's a little touch but I love how the colors change based on the page. And it covers both the POV charcter's entry and all of their sources.&lt;/p&gt;
&lt;h2&gt;Gemini&lt;/h2&gt;
&lt;p&gt;One of the other reasons I wanted to migrate to Nitride was because I wanted to also generate Gemtext pages and create a &lt;a href="https://gemini.circumlunar.space/"&gt;Gemini pod&lt;/a&gt;. This is a bit of a &amp;ldquo;silly&amp;rdquo; project since I know most readers aren't going to ever be on Gemini, but the &amp;ldquo;small network&amp;rdquo; is nice and I like sticking my work out where outliers might find it.&lt;/p&gt;
&lt;p&gt;Because I wrote Nitride with Gemini in mind and had already done this site, it ended up only being a few days to create something I thought was pleasing and to push it out as &lt;a href="gemini://fedran.com/"&gt;a pod&lt;/a&gt;. It was nice as things I've been working on for over a year just fell into place.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There are a lot of next steps when it comes to that site. I still seem to be struggling to write and programming is a nice distraction, but I really want to get my time line back, which requires me to port my custom calendar (MfGames Culture) code to C# and get the &amp;ldquo;what is every chapter or scene where this character participated in or was referenced from&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Then, start to fill out the pages since I really should write up &lt;em&gt;Flight of the Scions&lt;/em&gt; in the site.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Various Updates - Good and Bad</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/10/26/personal-update/" />
    <updated>2022-10-26T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/10/26/personal-update/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <category term="entanglement-2021" scheme="https://d.moonfire.us/tags/" label="Entanglement 2021" />
    <category term="flight-of-the-scions" scheme="https://d.moonfire.us/tags/" label="Flight of the Scions" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="gitlab" scheme="https://d.moonfire.us/tags/" label="GitLab" />
    <summary type="html">Two months ago, I was uncomfortable and in pain and distracted myself with migrating to a new forge. In the time since then, I've pretty much done the same thing: been in a lot of pain and migrating stuff.
</summary>
    <content type="html">&lt;p&gt;I made my &lt;span class="missing-link" data-path="/blog/2022/09/06/migrating-to-take-the-pain-away/"&gt;last post&lt;/span&gt; almost two months ago. I was uncomfortable and in pain and was distracting myself with migrating to a new forge. In the time since then, I've pretty much done the same thing: been in a lot of pain and migrating stuff.&lt;/p&gt;
&lt;h2&gt;Migration&lt;/h2&gt;
&lt;p&gt;Over the last month, I managed to get all of my stories and novels migrated over to &lt;a href="https://src.mfgames.com/fedran-sources/"&gt;my Git repository&lt;/a&gt;, switched to a private CI, and create EPUB/PDFs of every single story. That has been a relief, thought there is one blank page that annoys me. I'll fix it sooner or later, but it does mean in addition to my website, I will have EPUB and PDF versions of every story I wrote.&lt;/p&gt;
&lt;p&gt;It also gets me about 80% off &lt;a href="/tags/gitlab/"&gt;GitLab&lt;/a&gt;. The others will take longer, but it will happen sooner or later.&lt;/p&gt;
&lt;h2&gt;Flight of the Scions&lt;/h2&gt;
&lt;p&gt;I'm still on track with publishing &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt; on November 8th. I have a review request out, a guest post written and submitted, and it posted almost everywhere.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; the &lt;a href="https://fedran.com/flight-of-the-scions/"&gt;Fedran page&lt;/a&gt; has a problem and I can't fix it without spending a lot of hours fixing it. So my current goal is to get Fedran updated to use &lt;a href="/tags/nitride/"&gt;Nitride&lt;/a&gt; and let me post links to various stores. I was able to disable the later chapters, so if you didn't get a beta copy, it will be an indeterminate amount of time before I release the chapters again.&lt;/p&gt;
&lt;p&gt;Naturally, I'm nervous about publishing this book. It has taken me a lot of gets to get to this point and I really want folks to love it. I just really don't know how to get the word out about it, other than to be excited and try to promote myself.&lt;/p&gt;
&lt;h2&gt;Warning&lt;/h2&gt;
&lt;p&gt;The last section is about the complications from my surgery. No graphic details, but somewhat negative.&lt;/p&gt;
&lt;h2&gt;Surgery&lt;/h2&gt;
&lt;p&gt;At this point, I can safely say, the surgery did not go well like the &lt;a href="/tags/entanglement-2021/"&gt;rest of my luck&lt;/a&gt;. It started well and things seem to be right on track. Right before I ended up in the emergency room for excruciating pain for six to ten hours every day. Actually, went to the emergency room twice because when the nurse asked if I wanted to be hospitalized, there was a pressure to go home and &amp;ldquo;push through it&amp;rdquo; when I should have said yes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ray, when someone asks if you're a god, say &amp;ldquo;yes!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Little would I know that it was only the early stages of a series of cascading failures when it came to my surgery. I did end up being hospitalized overnight, which was a terrible experience of doctor's ignoring everything I said including when they would ask if I wanted something for the pain: I said yes, then they gave me nothing. Three times.&lt;/p&gt;
&lt;p&gt;I think much of it was that I also had asymptotic Covid. As things are when overworked people are only looking at paper, they focused purely on that and shoved me out the door as soon as I was able to walk around. It took another week or so before I recovered from that.&lt;/p&gt;
&lt;p&gt;As a lovely bit of foreshadowing, during the first emergency room visit, I had a CAT scan and they said there was a small fluid pocket from the surgery but &amp;ldquo;it probably wasn't going to be a problem.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It did end up being a problem, including more frantic doctor visits. My original surgeon went on a medical leave so I was tossed over to a second doctor who had no clue what had happened. It took a week of agony for him to get the baselines. Then more days until I could get a drain tube put in for a week, to have it go wrong, then back into the doctor's office.&lt;/p&gt;
&lt;p&gt;The second doctor decided the drain tube was enough and pulled it out. This ended up being premature, so then I had to get it lanced when it came back with a vengeance. Now, I'm going to the hospital for wound packing for a week now and probably will be going for few more weeks; but there are already signs that I will need another procedure because there are some additional complications beyond the weekend nurses not packing it correctly.&lt;/p&gt;
&lt;p&gt;Overall, there are were at least seven points where someone made a decision, with hindsight, was the wrong one. One thing I could handle, but this has been problem after problem after problem.&lt;/p&gt;
&lt;p&gt;Currently, the best part is that I'm only slightly uncomfortable (but always exhausted) as opposed to the days of agony that dominated my September.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Migrating to Take Away the Pain</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/09/06/migrating-to-take-away-the-pain/" />
    <updated>2022-09-06T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/09/06/migrating-to-take-away-the-pain/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="gitea" scheme="https://d.moonfire.us/tags/" label="Gitea" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <category term="flight-of-the-scions" scheme="https://d.moonfire.us/tags/" label="Flight of the Scions" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <summary type="html">I had surgery last Friday. It was and continues to be painful, so I needed to distract myself so I decided to finish up the process of migrating my C# projects from GitLab over to my Gitea instance.
</summary>
    <content type="html">&lt;p&gt;I had surgery last Friday. It was and continues to be painful, so I needed to distract myself so I decided to finish up the process of migrating my C# projects from &lt;a href="/tags/gitlab/"&gt;GitLab&lt;/a&gt; over to my &lt;a href="/tags/gitea/"&gt;Gitea&lt;/a&gt;: &lt;a href="https://src.mfgames.com/mfgames-cil/"&gt;https://src.mfgames.com/mfgames-cil/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point, all of my libraries had been moved over, CI changed over to &lt;a href="/tags/woodpecker-ci/"&gt;Woodpecker CI&lt;/a&gt; and deploying to the NuGet repository on the Gitea server. I also went through each of them and set up source mapping which means all of the &amp;ldquo;MfGames.*&amp;rdquo; will only point to my host and everything else will go to nuget.org as normal.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;packageSources&amp;gt;
    &amp;lt;clear /&amp;gt;
    &amp;lt;add key=&amp;quot;nuget.org&amp;quot; value=&amp;quot;https://api.nuget.org/v3/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
    &amp;lt;add key=&amp;quot;mfgames.com&amp;quot; value=&amp;quot;https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
  &amp;lt;/packageSources&amp;gt;
  &amp;lt;packageSourceMapping&amp;gt;
    &amp;lt;packageSource key=&amp;quot;nuget.org&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
    &amp;lt;packageSource key=&amp;quot;mfgames.com&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;MfGames.*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
  &amp;lt;/packageSourceMapping&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is still a lot more to do in process, but I feel good about having things over on my own system. I did love GitLab a lot, they have taken care of me for many years, they just went in a direction I can't follow.&lt;/p&gt;
&lt;p&gt;In specific, they made everything free and open in the beginning, but as they got success, they needed to start to make a profit. That's understandable, but I was unable to find a reasonable way of affording such services and their sales stopped talking to me when they saw how small I was (I have many organizations but but with only 1-2 users and they couldn't tell me how to send them money to allow that). So, my $20/month goes into hosting my instances instead.&lt;/p&gt;
&lt;p&gt;Most of the &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt; sources are already migrated, but they are not hooked up to Woodpecker CI so I don't really consider them &amp;ldquo;migrated&amp;rdquo;. Also, Woodpecker doesn't have a scheduled post so I can't do my daily update that I use pretty heavily so the websites haven't migrated.&lt;/p&gt;
&lt;p&gt;This also ties into my short-term plan over the next few months, so once I get this site (&lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt;) migrated, then I can start pushing to get fedran.com ready to host &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt; for November 8th.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>August in a Nutshell</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/08/27/august-in-a-nutshell/" />
    <updated>2022-08-27T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/08/27/august-in-a-nutshell/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <category term="allegro" scheme="https://d.moonfire.us/tags/" label="Allegro" />
    <category term="cabin" scheme="https://d.moonfire.us/tags/" label="Cabin" />
    <category term="covid" scheme="https://d.moonfire.us/tags/" label="Covid" />
    <category term="entanglement-2021" scheme="https://d.moonfire.us/tags/" label="Entanglement 2021" />
    <category term="flight-of-the-scions" scheme="https://d.moonfire.us/tags/" label="Flight of the Scions" />
    <category term="second-hand-dresses" scheme="https://d.moonfire.us/tags/" label="Second-Hand Dresses" />
    <category term="icon" scheme="https://d.moonfire.us/tags/" label="ICON" />
    <category term="pack-daughter" scheme="https://d.moonfire.us/tags/" label="Pack Daughter" />
    <category term="merger-of-evil" scheme="https://d.moonfire.us/tags/" label="Merger of Evil" />
    <category term="songbird-in-a-kitchen" scheme="https://d.moonfire.us/tags/" label="Songbird in a Kitchen" />
    <category term="nor-curse-be-found" scheme="https://d.moonfire.us/tags/" label="Nor Curse Be Found" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <category term="300-weeks" scheme="https://d.moonfire.us/tags/" label="300 Weeks" />
    <summary type="html">The last few weeks of August have been very busy, both in terms of obligations, deadlines, and events of note. Some of them are negative while others are positive, so give me a chance to tell how my month has been going.
</summary>
    <content type="html">&lt;p&gt;The last few weeks of August have been very busy, both in terms of obligations, deadlines, and events of note. Some of them are negative while others are positive, so give me a chance to tell how my month has been going.&lt;/p&gt;
&lt;h1&gt;The Trip&lt;/h1&gt;
&lt;p&gt;The month started with a trip to the &lt;a href="/tags/cabin/"&gt;family cabin&lt;/a&gt;. It was about ten days, mainly because Partner had things going on and was unable to attend (various reasons of their own, I don't force them to come to my family events). It ended up being ten days and fourteen hours of driving (I do not like driving long distance, period).&lt;/p&gt;
&lt;p&gt;My younger brother did a lot to spruce up the cabin and he brought the results of his multi-year project: a boat he had been restoring that my father had helped build. It was cool and nice to see a slice of my childhood in the water again.&lt;/p&gt;
&lt;p&gt;I didn't get on it. Not that I didn't want to, it just didn't work out. The younger cousins (my kids and his kids) were having fun with rafting and water skiing. That meant I was well suited for jumping into the water around the dock and occupying them as they took turns, helping guide the boat into position, and assisting in my kids (who don't know how to swim as well as his).&lt;/p&gt;
&lt;p&gt;I still had a ball. I like visiting family, seeing my brother, and just remaining in contact since I'm really terrible at initiating conversation outside of those events.&lt;/p&gt;
&lt;h1&gt;Broadband&lt;/h1&gt;
&lt;p&gt;One of the projects I started in August of last year was trying to get broadband into the cabin. The city had a large-scale installation and we got free installation of fiber (about twice as fast as I have at home). It was pretty much installed a year to the week, which worked out because my company stopped providing an access point and my phone doesn't work that far north†.&lt;/p&gt;
&lt;p&gt;Having that there means that there is a better chance I can have a &amp;ldquo;writing weekend&amp;rdquo; where I got up for a couple of days of writing and work since everything I do is remote. I just have to get a better monitor/keyboard set up there.&lt;/p&gt;
&lt;p&gt;Still, when it comes to reducing the chaos in my life, this was something that was hanging over me as one more item in the &lt;a href="/tags/entanglement-2021/"&gt;entanglement&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;† T-Mobile started saying &amp;ldquo;99% coverage of the population&amp;rdquo; which is a lot more accurate because 99% of &lt;a href="https://www.worldometers.info/world-population/us-population/"&gt;329.5 million&lt;/a&gt; is still missing out on 3.3 million people. As such, that is Verizon territory and my phone pretty much craps out the second I hit the interstate. (I won't buy non-GSM phones and Verizon screwed me over more than once because I hold onto phones for about a decade before buying a new one.)&lt;/p&gt;
&lt;h1&gt;Flood Five&lt;/h1&gt;
&lt;p&gt;Now, I wouldn't be mentioning the entanglement if there wasn't something that went wrong. When I came home and went into my basement office, I could smell moisture. That is rarely a good sign and something that is quickly triggering PTSD.&lt;/p&gt;
&lt;p&gt;Apparently, there was a major rain while I was gone and the sump pump had died so the entire basement flooded. Every scrap of carpet, soaked drywall, and ruined boxes were laid out for me to manage. Today, I got all of the carpet and padding out and into the garage; I'm planning on getting a dumpster in late October to get rid of it along with the rotted wood from the shelves and furniture and everything else that needs to get tossed.&lt;/p&gt;
&lt;p&gt;One thing I find fascinating is that of all the six floods I've had in the basement (first one didn't really count since that was the week I moved in), they have all had entire different sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flood Zero: Water main cracked&lt;/li&gt;
&lt;li&gt;Flood One: Derecho in 2020&lt;/li&gt;
&lt;li&gt;Flood Two: Cracked pipe inside the slab&lt;/li&gt;
&lt;li&gt;Flood Three: Refrigerator water line snapped&lt;/li&gt;
&lt;li&gt;Flood Four: Mother's Day toilet explosion&lt;/li&gt;
&lt;li&gt;Flood Five: Sump pump&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Floods two through five have all happened in the last twelve months. The end result is that I used to have a finished basement. At this point, I think the entire thing has been gutted enough to consider it no longer finished.&lt;/p&gt;
&lt;p&gt;With best estimates, I figured I'll get it back to speed in five to ten years. I'm still working in the basement, I just have everything on naked concrete.&lt;/p&gt;
&lt;h1&gt;Surgery&lt;/h1&gt;
&lt;p&gt;In 2020, I caught Covid. I suspect twice, once in February since I had all the symptoms and then in November where I had all the symptoms but even worse. During the first one, I coughed so hard I hurt myself but it was a minor hernia so the doctor's dismissed it (probably because I'm fat) until it became obvious that it was steadily getting worse.&lt;/p&gt;
&lt;p&gt;Fast foward to July: that is when we can change our benefits at work. I switched from high-deductible to PPO and then got the doctor to look at it with something more than &amp;ldquo;well, that looks painful, so how is everything else?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Next week, I get surgery to fix that and I have so much hopes that it will handle the nigh continuous pain I've been in for the last four months. Given that I'm hypersensitive to input, that has been a distraction for almost everything I do and prevented me from sleeping well.&lt;/p&gt;
&lt;p&gt;Of course, I have to get through the pre-op exam, the pre-pre-op tests, the pre-op phone call, and the pre-op billing statement first.&lt;/p&gt;
&lt;p&gt;This also put a knife edge on the deadline for the carpet removal from the flooding since I won't be able to pick up anything heavy for a while. Wet carpet is not&amp;hellip; light. Nor is moving water-logged furniture.&lt;/p&gt;
&lt;h1&gt;Allegro&lt;/h1&gt;
&lt;p&gt;A while ago, I contributed to a fund-raiser to send some writers to Clarion. Call it a wish fulfillment since I won't be able to go myself. One of the rewards was having another author read a book (or a chance to play with an AI novel writing tool). I decided to go with the reading and that meant I needed to finish &amp;ldquo;something&amp;rdquo; before September.&lt;/p&gt;
&lt;p&gt;Since I was stalled on &lt;a href="/tags/allegro/"&gt;Allegro&lt;/a&gt;, I committed to finishing that novel for September. Earlier this week, I got the first draft done and I'm so excited. For what is a glorified fan-fiction of &lt;a href="https://inv.riverside.rocks/watch?v=jvipPYFebWc"&gt;Lindsey Sterling's Roundtable Rival&lt;/a&gt; set in &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt;, I'm pretty happy with a 105k word novel coming from a four minute music video.&lt;/p&gt;
&lt;p&gt;It ended up being a much different story that Lindsey's video. It ties into &lt;a href="/tags/songbird-in-a-kitchen/"&gt;Songbird in a Kitchen&lt;/a&gt; and the general build up of stories to Fedran's world war (it's a Phase One story). Regardless, I'm really happy with the results and I hope I can polish it up to make it my sixth published novel (&lt;a href="/tags/second-hand-dresses/"&gt;Second-Hand Dresses&lt;/a&gt; will be my fifth and &lt;a href="/tags/nor-curse-be-found/"&gt;Nor Curse Be Found&lt;/a&gt; will probably be my seventh).&lt;/p&gt;
&lt;h1&gt;Flight of the Scions&lt;/h1&gt;
&lt;p&gt;Before the trip, I got the &amp;ldquo;release candidate&amp;rdquo; for &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt; done and sent to Ingram for a print version. It came while I was at the cabin. It looks beautiful and I'm really happy how the new covers turned out.&lt;/p&gt;
&lt;p&gt;I also botched the layout (missed one page of padding so everything has the wrong margins), so I need to order a new one. That also means I have another chance to look for typos. My dad sent me a bunch, I found a few, I'm just going to work on them in September because of &lt;em&gt;Allegro&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I think I'm on-track for the November 8th release of &lt;em&gt;Flight of the Scions&lt;/em&gt; and I'm so excited. I will have early copies at &lt;a href="/tags/icon/"&gt;ICON&lt;/a&gt; this year since I'll have a table in the merchant area along with Shannon Ryan's &lt;a href="https://weirdauthor.com/merger"&gt;Merger of Evil&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For a novel I've been working off and on for almost two decades, having this out will be both a relief and a source of constant anxiety because it doesn't have my full evolution of writing (I couldn't rewrite it one more time, I just couldn't). It also means I need to get off my rear and start writing &lt;a href="/tags/pack-daughter/"&gt;Pack Daughter&lt;/a&gt;, the second of four in the series.&lt;/p&gt;
&lt;h1&gt;Fedran Website&lt;/h1&gt;
&lt;p&gt;Of course, I want to show off since my books are available to read even before they are for sale. Unfortunately, the site generator that &lt;a href="https://fedran.com/"&gt;https://fedran.com/&lt;/a&gt; uses, &lt;a href="https://www.gatsbyjs.com/"&gt;Gatsby JS&lt;/a&gt;, proved to be too fragile for how I use it. Plus it doesn't play well with my goal of getting my site on &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That means I need to get it migrated over to &lt;a href="/tags/nitride/"&gt;Nitride&lt;/a&gt; as soon as reasonable. Until then, the site is pretty much static until I can do that. It also means I can't start posting weekly chapters for &lt;em&gt;Allergo&lt;/em&gt; until that comes out (even though everyone will be able to read the entire thing).&lt;/p&gt;
&lt;p&gt;I just like having that weekly post. It makes me feel good to see that steady signal. I have no clue if I'm going to try again for &lt;a href="/blog/2021/03/17/three-hundred-weeks/"&gt;three hundred consecutive weeks&lt;/a&gt; like before. In most regards, I just effectively took a year off from writing.&lt;/p&gt;
&lt;h1&gt;September&lt;/h1&gt;
&lt;p&gt;September is going to be a busy month for me. I have &lt;em&gt;Allegro&lt;/em&gt; to get out, &lt;em&gt;Flight&lt;/em&gt; to polish, and fix the little things that are needed to get the book out the door.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Semantic Release and Woodpecker CI</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/08/07/semantic-release-and-woodpecker-ci/" />
    <updated>2022-08-07T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/08/07/semantic-release-and-woodpecker-ci/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="semantic-release" scheme="https://d.moonfire.us/tags/" label="Semantic Release" />
    <category term="woodpecker-ci" scheme="https://d.moonfire.us/tags/" label="Woodpecker CI" />
    <category term="gitlab" scheme="https://d.moonfire.us/tags/" label="Gitlab" />
    <category term="gitea" scheme="https://d.moonfire.us/tags/" label="Gitea" />
    <category term="conventional-commits" scheme="https://d.moonfire.us/tags/" label="Conventional Commits" />
    <category term="gitversion" scheme="https://d.moonfire.us/tags/" label="GitVersion" />
    <category term="lefthook" scheme="https://d.moonfire.us/tags/" label="Lefthook" />
    <category term="sourcehut" scheme="https://d.moonfire.us/tags/" label="Sourcehut" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="project-layout" scheme="https://d.moonfire.us/tags/" label="Project Layout" />
    <summary type="html">In my migration from GitLab to Gitea, I've started moving my CI/CD server over to Woodpecker. Here is some of the struggles I've done through in the process of getting it to work.
</summary>
    <content type="html">&lt;p&gt;With the recent drama of &lt;a href="/tags/gitlab/"&gt;GitLab&lt;/a&gt;, both with the CI/CD changes and then more recent possible threat of deleting old repositories, I continue my migration to a local &lt;a href="/tags/gitea/"&gt;Gitea&lt;/a&gt; instance, &lt;a href="https://src.mfgames.com/"&gt;https://src.mfgames.com/&lt;/a&gt; for the bulk of my code and writing.&lt;/p&gt;
&lt;p&gt;For the most part, migrating is just a matter of shuffling data. I have a &lt;em&gt;lot&lt;/em&gt; of repositories, both active and inactive, and it will take me months to move them over. Plus I haven't decided if I'm going to purge them from my GitLab account so there is a single source of truth or just mirror back to them.&lt;/p&gt;
&lt;p&gt;Currently, the most difficult task was figuring out how to handle the build processing. I've mentioned previously that I use &lt;a href="/tags/conventional-commits/"&gt;Conventional Commits&lt;/a&gt; and &lt;a href="/tags/semantic-release/"&gt;Semantic Release&lt;/a&gt; fairly heavily. I've branched out a little from there using &lt;a href="/tags/lefthook/"&gt;Lefthook&lt;/a&gt; and my &lt;a href="/garden/project-layout/"&gt;project layout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently, the CI does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build the project&lt;/li&gt;
&lt;li&gt;Test various conditions including valid commit messages&lt;/li&gt;
&lt;li&gt;If the commits indicate a new build:
&lt;ol&gt;
&lt;li&gt;Tag it&lt;/li&gt;
&lt;li&gt;Build the release version&lt;/li&gt;
&lt;li&gt;Create a release on Gitea&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This changes over time, but it is the basic pattern.&lt;/p&gt;
&lt;h1&gt;Tags and Git Depth&lt;/h1&gt;
&lt;p&gt;Woodpecker does not automatically download the needed tags for &lt;code&gt;semantic-release&lt;/code&gt; (and &lt;a href="/tags/gitversion/"&gt;GitVersion&lt;/a&gt;). This means that the &lt;code&gt;.woodpecker.yml&lt;/code&gt; file needs to include tags.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;clone:
    git:
        image: woodpeckerci/plugin-git
        settings:
            tags: true
pipeline:
    # The pipeline elements
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unike GitLab, which only limits to the last ten commits, it appears that Woodpecker &lt;a href="https://woodpecker-ci.org/plugins/plugin-git"&gt;downloads the full repository&lt;/a&gt; by default which is also needed by GitVersion because it calculates every version. Not entirely sure about &lt;code&gt;semantic-release&lt;/code&gt; logging indicates it doesn't need the full repository, just enough back to find a version.&lt;/p&gt;
&lt;h1&gt;Building and Testing&lt;/h1&gt;
&lt;p&gt;To support task branches, I have a basic build and test code that runs on pushes and pull requests. This lets me identify bugs earlier and catch typos with my commits.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;build:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - nix develop --command scripts/build.sh
    when:
        # We need both &amp;quot;tag&amp;quot; for the next section.
        event: [push, pull_request, tag]
        tag: v*

test:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - nix develop --command scripts/test.sh
    when:
        event: [push, pull_request]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the build tasks, you can see that I'm using my current project layout which uses scripts in the &lt;code&gt;scripts/&lt;/code&gt; folder instead of &lt;code&gt;npm run&lt;/code&gt; or &lt;code&gt;dotnet run&lt;/code&gt;. This is to make it easier to work with polyglot plus works around the issue that I need to use &lt;code&gt;nix develop&lt;/code&gt; to get into my reproducible environment since the Docker image doesn't automatically do that. This is because both Gitlab and Woodpecker use the image which bypasses initialization files and I couldn't have it run &lt;code&gt;direnv allow&lt;/code&gt; automatically to set up environment variables.&lt;/p&gt;
&lt;p&gt;One thing that is missing is that Woodpecker doesn't have a clean mechanism for temporary build artifacts. I can't upload the build files and then download them so I can see the final results. Instead, I have to script it out or use a S3 plugin.&lt;/p&gt;
&lt;h1&gt;Building on Versions&lt;/h1&gt;
&lt;p&gt;With most cases, I build the release version of the project when the conventional commits indicate that there is a new version (&lt;code&gt;feat&lt;/code&gt; and &lt;code&gt;fix&lt;/code&gt;). This is an additional pipeline that comes after the &lt;code&gt;test:&lt;/code&gt; line.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;release-main:
    image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
    commands:
        - export DRONE=&amp;quot;true&amp;quot; # Required to convince `env-ci`
        # semantic-release needs this locally
        - git branch $DRONE_BRANCH origin/$DRONE_BRANCH
        - nix develop --command scripts/release.sh
    secrets:
        - gitea_token
        - git_credentials
    when:
        event: push
        branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a number of things in this block that took me a while. The first is the &lt;code&gt;event&lt;/code&gt; and &lt;code&gt;branch&lt;/code&gt;. We only do releases on the &lt;code&gt;main&lt;/code&gt; (I'm still moving away from &lt;code&gt;master&lt;/code&gt; as racist language).&lt;/p&gt;
&lt;p&gt;The second is the &lt;code&gt;export DRONE&lt;/code&gt; line. At the time I set this up, &lt;a href="https://www.npmjs.com/package/env-ci"&gt;env-ci&lt;/a&gt; wasn't aware of Woodpecker, but it was &lt;a href="https://github.com/woodpecker-ci/woodpecker/pull/1035"&gt;recently added&lt;/a&gt; thanks to 6543 on the Woodpecker Matrix channel, &lt;code&gt;#woodpecker-ci:matrix.org&lt;/code&gt;. I don't know when the latest &lt;code&gt;semantic-release&lt;/code&gt; will have it, but it shouldn't be needed soon, if not already.&lt;/p&gt;
&lt;p&gt;The third is the &lt;code&gt;git branch&lt;/code&gt; line in the above script. Woodpecker creates a detached head, as does Gitlab. But when it doesn't do is also create a local branch for the one being created. This causes a problem because the release process appears to &amp;ldquo;jump&amp;rdquo; to the branch to figure out the changes between the detached head (the commit being built) and the actual branch.&lt;/p&gt;
&lt;p&gt;Finally, we have the secrets. &lt;code&gt;semantic-release&lt;/code&gt; automatically picks up $GITEA_TOKEN for the release process but also needs $GIT_CREDENTIALS to verify Git access.&lt;/p&gt;
&lt;p&gt;The token is easy, that is what given by Gitea for the user.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/semantic-release/semantic-release/blob/master/docs/usage/ci-configuration.md"&gt;GIT_CREDENTIALS&lt;/a&gt; is slightly harder, it is a colon-separate tuple of the user name and the Gitea access token. From observations, the Gitea is basically just a bunch of URL-safe characters, so the URL-escaping isn't needed in my case (you need to URL-escape the left and right of the colon but not the colon itself).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;$ export GITEA_TOKEN=9fc6d72c72e4b149f07491a0b2d3ec9215d57caf
$ export GIT_CREDENTIALS=&amp;quot;dmoonfire:$GITEA_TOKEN&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These need to be set on a per-project basis since Woodpecker, unlike GitLab and &lt;a href="/tags/sourcehut/"&gt;sourcehut&lt;/a&gt;, there doesn't appear to be a good way of having a shared set of secrets for projects (GitLab has organization/group level secrets, sourcehut has the secret storage). This means I have to set the same GITEA_TOKEN and GIT_CREDENTIALS for all 80+ of my &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt; repositories†.&lt;/p&gt;
&lt;p&gt;† Woodpecker has a CLI, &lt;code&gt;woodpecker-cli&lt;/code&gt; which will let me automate that. I will use that.&lt;/p&gt;
&lt;h1&gt;Create Release on Tag&lt;/h1&gt;
&lt;p&gt;One thing I'm moving toward is creating a release entry on the forge. Since this happens after the build process and Woodpecker uses a different Docker image, I need it to be a post-release event so I hang it off the tagging process instead of the push to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;6543 came to my rescue again with this one, so that is why there are those two &amp;ldquo;tag&amp;rdquo; elements in the script above. I also have a new stanza for the release process:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;release-gitea:
    image: plugins/gitea-release
    settings:
        base_url: https://src.mfgames.com
        files:
            - &amp;quot;*.pdf&amp;quot;
            - &amp;quot;*.epub&amp;quot;
        api_key:
            from_secret: gitea_token
    when:
        event: tag
        tag: v*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I didn't have the &lt;code&gt;event:&lt;/code&gt; and &lt;code&gt;tag:&lt;/code&gt; in the &lt;code&gt;build:&lt;/code&gt; stanza, it wasn't working for the tags. This caused me some difficulties because I usually treat hashes as being unordered, but Woodpecker uses file order for processing pipelines. So, I needed to have the &lt;code&gt;build:&lt;/code&gt; target build the file (with the correct version because it was tagged) and then &lt;code&gt;release-gitea:&lt;/code&gt; to use that output for the release process. The &lt;code&gt;test:&lt;/code&gt; and &lt;code&gt;release-main:&lt;/code&gt; are skipped because they don't have those events listed.&lt;/p&gt;
&lt;p&gt;In addition, secrets are handled differently when done as a parameter for a plugin. That is why I have the &lt;code&gt;from_secret:&lt;/code&gt; element in the above script. This inconsistency threw me for a few days.&lt;/p&gt;
&lt;h1&gt;Putting it Together&lt;/h1&gt;
&lt;p&gt;If you want to see the final version, check out &lt;a href="https://src.mfgames.com/dmoonfire-garden/project-layout/src/branch/main/.woodpecker.yml"&gt;this example&lt;/a&gt; which has my current version as a single file.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;I'm happy to move over to Woodpecker (you know, except for the cost of hosting) both because of the control and the challenge. I also don't have a need for speed, so if it takes a while to get through the queue, I'm okay.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Project Layout (v4.0.0)</title>
    <link rel="alternate" href="https://d.moonfire.us/garden/project-layout/" />
    <updated>2022-07-20T00:34:17Z</updated>
    <id>https://d.moonfire.us/garden/project-layout/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <summary type="html">Methods for working with projects that use different frameworks (such as C# and TypeScript) including how to handle the different build systems and processes.</summary>
    <content type="html">&lt;p&gt;I'm a polyglot programmer. I work in a variety of languages but mostly in &lt;a href="//d.moonfire.us/tags/c-sharp/"&gt;C#&lt;/a&gt;, &lt;a href="//d.moonfire.us/tags/typescript/"&gt;Typescript&lt;/a&gt;, and &lt;a href="//d.moonfire.us/tags/rust/"&gt;Rust&lt;/a&gt;. Every few years, I try a new language to see if I can pick up new ideas or if one &amp;ldquo;fits&amp;rdquo; my current mental state better. This is also why I've done a lot dozens of other languages; I would say I know over thirty languages but I'm only a &amp;ldquo;master&amp;rdquo; in a handful.&lt;/p&gt;
&lt;p&gt;I also flit from project to project. I have my &lt;a href="//d.moonfire.us/categories/writing/"&gt;writing&lt;/a&gt; and games. I have little one-off programs and ones that I hope will become a major thing. But, like everything else in my life, I'm &amp;ldquo;gloriously unfocused&amp;rdquo; on my tasks which means I have to minimize the speed that I get into a project before the muse escapes me.&lt;/p&gt;
&lt;p&gt;Over the years, I've come up with a project structure that works across most languages. It is mostly &amp;ldquo;for me&amp;rdquo;, but I think there are aspects of that others may enjoy.&lt;/p&gt;
&lt;h2&gt;Conventions&lt;/h2&gt;
&lt;p&gt;Throughout this document, &lt;code&gt;//&lt;/code&gt; represents the root of the Git repository. So, &lt;code&gt;//src/just/build.just&lt;/code&gt; would be the &lt;code&gt;build.just&lt;/code&gt; in the &amp;lsquo;src/just&amp;rsquo; file under the root.&lt;/p&gt;
&lt;p&gt;When I'm referring to a directory, I will include a trailing lash such as &lt;code&gt;//src/&lt;/code&gt; to make a distinct between that and &lt;code&gt;//Justfile&lt;/code&gt; which is a file.&lt;/p&gt;
&lt;p&gt;Also, I use forward slashes regardless of the operating system, PowerShell can handle both and it doesn't require escaping in most languages.&lt;/p&gt;
&lt;p&gt;With HTML paths, I will use &lt;code&gt;//&lt;/code&gt; when a link has both a Gemini and HTML server associated with it or explictly say &lt;code&gt;https://&lt;/code&gt; if it is an HTML-only site.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./philosophy/"&gt;Philosophy&lt;/a&gt; is the general guidelines for setting up a project.&lt;/li&gt;
&lt;li&gt;&lt;a href="./just/"&gt;Just&lt;/a&gt; has the tool used to run scripts in projects.&lt;/li&gt;
&lt;li&gt;&lt;a href="./directories/"&gt;Directories&lt;/a&gt; describes the preferred directory layout.&lt;/li&gt;
&lt;li&gt;&lt;a href="./setup/"&gt;Setup&lt;/a&gt; for setting up a project to make it &amp;ldquo;easier&amp;rdquo; to get into it.&lt;/li&gt;
&lt;li&gt;&lt;a href="./history/"&gt;History&lt;/a&gt; describes the historical evolution of my project layout.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;License&lt;/h2&gt;
&lt;p&gt;This book is distributed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International license. More info can be found at &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;https://creativecommons.org/licenses/by-nc-sa/4.0/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The preferred attribution for this novel is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Project Layout&amp;rdquo; by D. Moonfire is licensed under CC BY-NC-SA 4.0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the above attribution, use the following links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project Layout: &lt;a href="https://d.moonfire.us/garden/project-layout/"&gt;https://d.moonfire.us/garden/project-layout/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;D. Moonfire: &lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CC BY-NC-SA 4.0: &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;https://creativecommons.org/licenses/by-nc-sa/4.0/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Source&lt;/h2&gt;
&lt;p&gt;The source of this project can be found on the &lt;a href="https://src.mfgames.com/dmoonfire-garden/project-layout"&gt;Moonfire Games forge&lt;/a&gt;. Feel free to report any issues, requests for expansion or clarifications. Alternately, you can &lt;a href="https://d.moonfire.us/contact/"&gt;contact me&lt;/a&gt; directly.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Nitride v0.3.4</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2022/06/06/nitride-0.3.4/" />
    <updated>2022-06-06T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2022/06/06/nitride-0.3.4/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="mfgames-writing" scheme="https://d.moonfire.us/tags/" label="MfGames Writing" />
    <category term="gallium" scheme="https://d.moonfire.us/tags/" label="Gallium" />
    <summary type="html">A minor milestone for Nitride, my static site generator, and a sweeping refactoring to make things consistent and easier to learn.
</summary>
    <content type="html">&lt;p&gt;Last week, I decided to work on &lt;a href="/tags/nitride/"&gt;Nitride&lt;/a&gt;, my static site generator written in C#. One of the outstanding tasks after I wrote it for &lt;a href="//d.moonfire.us/"&gt;this&lt;/a&gt; site to handle both HTML and Gemtext versions was to look at the various patterns that I had established and then simplify them.&lt;/p&gt;
&lt;p&gt;Cleaning up code is good when starting with something that potentially can get large. I had used &lt;a href="/tags/cobblestone/"&gt;Cobblestone&lt;/a&gt; for almost a decade between all my websites and I fully expected Nitride to last the name. That means I need to be comfortable with the API. Of course, &amp;ldquo;comfortable with&amp;rdquo; usually means &amp;ldquo;I like it until I stop using it for three months and then I want to rewrite it all.&amp;rdquo; However, I've managed to avoid that with Cobblestone and I'm &amp;ldquo;mostly&amp;rdquo; resisting that with &lt;span class="missing-link" data-path="/tags/mfgames-writing"&gt;MfGames Writing&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Side note: I'm planning on switching MfGames Writing to configuration-as-code instead of declarative files in the future, just not sure when.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The other plan I had was to write unit tests and examples for every option to basically help document the place. This was going to be injected into &lt;a href="https://mfgames.com/"&gt;https://mfgames.com/&lt;/a&gt; as a project, but I realized that I was trying to do too much and I needed to break it down.&lt;/p&gt;
&lt;p&gt;This week's effort was just pattern normalization. I've embraced using dependency injection for setting up the various pipelines, so there is no more mix cases of constructor injection and &lt;code&gt;new&lt;/code&gt; operations; it's all constructor injection. That way, I can offload the validation logic to &lt;code&gt;FluentValidators&lt;/code&gt; and change the signatures, such as to add logging, without breaking existing code.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public PagesMarkdownPipeline(
    ReadFiles readFiles,
    MoveToIndexPath moveToIndexPath,
    ParseYamlHeader&amp;lt;PageModel&amp;gt; parseYamlHeader,
    RemovePathPrefix removePathPrefix,
    IdentifyMarkdownFromPath identifyMarkdownFromPath)
{
    this.readFiles = readFiles.WithPattern(&amp;quot;/pages/**/*&amp;quot;);
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can tell from the example above, I'm also using the fluent pattern a lot more to handle configuration. I think this works since the initial object has mostly sane defaults and validations for the rest of them while the rest can be customized in the constructor.&lt;/p&gt;
&lt;p&gt;I also decided to keep the configuration in the constructor of the pipeline. That makes the actual operation of the pipeline much easier to understand.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;/// &amp;lt;inheritdoc /&amp;gt;
public override Task&amp;lt;IEnumerable&amp;lt;Entity&amp;gt;&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities)
{
    IEnumerable&amp;lt;Entity&amp;gt; files = this.readFiles.Run()
        .Run(this.removePathPrefix)
        .Run(this.moveToIndexPath)
        .Run(this.identifyMarkdownFromPath)
        .Run(this.parseYamlHeader)
        .ForEachEntity&amp;lt;PageModel&amp;gt;(this.SetInstantFromPageModel);

    return Task.FromResult(files);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can tell, it also pulls from my history with &lt;code&gt;gulp&lt;/code&gt; a lot since I think the chained &lt;code&gt;.Run()&lt;/code&gt; methods works. The &lt;code&gt;ForEachEntity&lt;/code&gt; is from &lt;a href="/tags/gallium/"&gt;Gallium&lt;/a&gt;, my simply entity component system that I wrote for this (but also want to use in other places).&lt;/p&gt;
&lt;p&gt;The other thing in the above example is that the pipelines (but not necessarily the individual operations) are async. I figured this gives a good mix of allowing async operations at the pipeline level but in most cases, it ends up being fairly synchronous.&lt;/p&gt;
&lt;p&gt;Thanks to the C#'s enumerations, the actual value sometimes isn't run until after the pipeline &amp;ldquo;runs&amp;rdquo;. There are exceptions, like when I need to create a category pages which requires knowing every other page in the website. Those are resolved immediately in the pipeline to process that, and then returned.&lt;/p&gt;
&lt;p&gt;Overall, it is moving forward nicely. I still see a lot of shuffling of method names and I want to break up the index creation pages (the annual, monthly, and daily blog listings) into separate components. But, once I get those, I should be ready to start converting the next site, &lt;a href="https://moonfire.us/"&gt;https://moonfire.us/&lt;/a&gt; and then into the next &amp;ldquo;big&amp;rdquo; one, &lt;a href="https://mfgames.com/"&gt;https://mfgames.com/&lt;/a&gt;. The &amp;ldquo;end goal,&amp;rdquo; as it were, is converting &lt;a href="https://fedran.com/"&gt;https://fedran.com/&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
</feed>
